diff --git a/assets/toolbox/sprites/js/index.js b/assets/toolbox/sprites/js/index.js index 7f92105d7..41ff3bdc4 100644 --- a/assets/toolbox/sprites/js/index.js +++ b/assets/toolbox/sprites/js/index.js @@ -152,6 +152,7 @@ function applySnapshot(snapshot) { } function updateDraftStatus() { + applyPaintedPixelsToGrid(); const status = document.querySelector("[data-sprites-draft-status]"); if (status) { status.textContent = draftStatusText(); @@ -217,6 +218,8 @@ function setZoomLevel(zoomLevel) { if (status) { status.textContent = `Canvas zoom display: ${normalizedZoomLevel * 100}%.`; } + applyPaintedPixelsToGrid(); + renderPreview(); } function pickCellColor(cell) { @@ -232,6 +235,8 @@ function pickCellColor(cell) { if (status) { status.textContent = `Picker selected ${colorLabel(colorKey)} from the unsaved editor canvas.`; } + applyPaintedPixelsToGrid(); + renderPreview(); } function setPixel(row, column, colorKey) { diff --git a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs index e1539d1c1..e68de5145 100644 --- a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs +++ b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs @@ -201,6 +201,38 @@ function collectPageFailures(page) { return { consoleErrors, failedRequests, pageErrors }; } +function spriteCell(page, row, column) { + return page.locator(`[data-sprite-pixel-row="${row}"][data-sprite-pixel-column="${column}"]`); +} + +async function previewCellHasPaint(page, row, column) { + const gridSize = Number(await page.locator("[data-sprites-pixel-grid]").getAttribute("data-sprites-grid-size")); + return page.locator("[data-sprites-preview-canvas]").evaluate((canvas, coordinates) => { + const context = canvas.getContext("2d"); + if (!context || !Number.isFinite(coordinates.gridSize) || coordinates.gridSize <= 0) { + return false; + } + const cellSize = canvas.width / coordinates.gridSize; + const x = Math.max(0, Math.min(canvas.width - 1, Math.floor((coordinates.column - 0.5) * cellSize))); + const y = Math.max(0, Math.min(canvas.height - 1, Math.floor((coordinates.row - 0.5) * cellSize))); + return context.getImageData(x, y, 1, 1).data[3] > 0; + }, { column, gridSize, row }); +} + +async function expectCenterAndPreviewPainted(page, row, column, colorClass = null) { + const cell = spriteCell(page, row, column); + await expect(cell).toHaveClass(/is-painted/); + if (colorClass) { + await expect(cell).toHaveClass(new RegExp(colorClass)); + } + expect(await previewCellHasPaint(page, row, column)).toBe(true); +} + +async function expectCenterAndPreviewEmpty(page, row, column) { + await expect(spriteCell(page, row, column)).not.toHaveClass(/is-painted/); + expect(await previewCellHasPaint(page, row, column)).toBe(false); +} + test("Sprite Creator shell loads with visible tool, canvas, details, and status regions", async ({ page }) => { const server = await startSpriteShellTestServer(); const failures = collectPageFailures(page); @@ -366,3 +398,72 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status await server.close(); } }); + +test("Sprite Creator keeps center canvas and right preview in sync", async ({ page }) => { + const server = await startSpriteShellTestServer(); + const failures = collectPageFailures(page); + + try { + await page.goto(`${server.baseUrl}/toolbox/sprites/index.html`, { waitUntil: "networkidle" }); + + await spriteCell(page, 1, 1).click(); + await expectCenterAndPreviewPainted(page, 1, 1, "sprite-canvas-cell--ink"); + + await page.getByRole("button", { name: "Eraser tool" }).click(); + await spriteCell(page, 1, 1).click(); + await expectCenterAndPreviewEmpty(page, 1, 1); + + await page.getByRole("button", { name: "Blue editor color" }).click(); + await page.getByRole("button", { name: "Fill tool" }).click(); + await expectCenterAndPreviewPainted(page, 1, 1, "sprite-canvas-cell--blue"); + + await page.getByRole("button", { name: "Clear Canvas" }).click(); + await expectCenterAndPreviewEmpty(page, 1, 1); + + await page.getByRole("button", { name: "Undo" }).click(); + await expectCenterAndPreviewPainted(page, 1, 1, "sprite-canvas-cell--blue"); + + await page.getByRole("button", { name: "Redo" }).click(); + await expectCenterAndPreviewEmpty(page, 1, 1); + + await page.getByRole("button", { name: "Gold editor color" }).click(); + await page.getByRole("button", { name: "Pencil tool" }).click(); + await spriteCell(page, 2, 2).click(); + await expectCenterAndPreviewPainted(page, 2, 2, "sprite-canvas-cell--gold"); + await page.getByRole("button", { name: "Blue editor color" }).click(); + await page.getByRole("button", { name: "Picker tool" }).click(); + await spriteCell(page, 2, 2).click(); + await expect(page.locator("[data-sprites-palette-status]")).toContainText("Active editor color: Gold"); + await expectCenterAndPreviewPainted(page, 2, 2, "sprite-canvas-cell--gold"); + + await page.getByRole("button", { name: "Zoom tool" }).click(); + await page.getByRole("button", { name: "200%" }).click(); + await expect(page.locator("[data-sprites-grid-shell]")).toHaveAttribute("data-sprites-zoom-level", "2"); + await expectCenterAndPreviewPainted(page, 2, 2, "sprite-canvas-cell--gold"); + + await page.getByRole("button", { name: "Clear Canvas" }).click(); + await page.getByRole("button", { name: "Green editor color" }).click(); + await page.getByRole("button", { name: "Line tool" }).click(); + await spriteCell(page, 1, 1).click(); + await spriteCell(page, 1, 4).click(); + await expectCenterAndPreviewPainted(page, 1, 2, "sprite-canvas-cell--green"); + + await page.getByRole("button", { name: "Clear Canvas" }).click(); + await page.getByRole("button", { name: "Rectangle tool" }).click(); + await spriteCell(page, 2, 2).click(); + await spriteCell(page, 4, 4).click(); + await expectCenterAndPreviewPainted(page, 2, 3, "sprite-canvas-cell--green"); + + await page.getByRole("button", { name: "Clear Canvas" }).click(); + await page.getByRole("button", { name: "Circle tool" }).click(); + await spriteCell(page, 8, 8).click(); + await spriteCell(page, 8, 11).click(); + await expectCenterAndPreviewPainted(page, 8, 11, "sprite-canvas-cell--green"); + + expect(failures.failedRequests).toEqual([]); + expect(failures.pageErrors).toEqual([]); + expect(failures.consoleErrors).toEqual([]); + } finally { + await server.close(); + } +}); diff --git a/docs_build/dev/reports/PR_26179_CHARLIE_033-sprites-canvas-preview-sync.md b/docs_build/dev/reports/PR_26179_CHARLIE_033-sprites-canvas-preview-sync.md new file mode 100644 index 000000000..4426f9ea2 --- /dev/null +++ b/docs_build/dev/reports/PR_26179_CHARLIE_033-sprites-canvas-preview-sync.md @@ -0,0 +1,75 @@ +# PR_26179_CHARLIE_033-sprites-canvas-preview-sync + +Team: CHARLIE + +Mode: Batch governance stacked feature workflow + +Base branch: PR_26179_CHARLIE_032-sprites-shape-tools + +Branch: PR_26179_CHARLIE_033-sprites-canvas-preview-sync + +## Summary + +Fixed the Sprite Creator center-canvas and right-preview sync path so both render from the same page-session editor state. The center grid is repainted from `editorState.paintedPixels` whenever draft status/preview refreshes, and picker/zoom paths also trigger a repaint even though they do not mutate pixels. + +## Branch Validation + +PASS + +- Built from `PR_26179_CHARLIE_032-sprites-shape-tools`. +- Scope stayed limited to canvas/preview synchronization and regression coverage. +- No DB/API/schema changes. +- No persistence or browser-owned authoritative product data added. +- No start_of_day files changed. +- ZIP package created at the requested path. + +## Requirement Checklist + +PASS - Right preview updates from page-session editor state. + +PASS - Center editor canvas updates from the same page-session editor state. + +PASS - Drawing repaints center canvas immediately. + +PASS - Erase repaints center canvas immediately. + +PASS - Fill repaints center canvas immediately. + +PASS - Clear repaints center canvas immediately. + +PASS - Undo repaints center canvas immediately. + +PASS - Redo repaints center canvas immediately. + +PASS - Picker repaints center canvas and preview without mutating pixels. + +PASS - Zoom repaints center canvas and preview without mutating pixels. + +PASS - Shape tools repaint center canvas immediately. + +PASS - Playwright regression coverage added for center canvas and preview sync. + +## Validation Lane + +PASS - `node --check assets/toolbox/sprites/js/index.js` + +PASS - `node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs` + +PASS - `git diff --check -- assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs` + +PASS - Runtime guard scan found no prohibited inline style/script/event-handler or stale placeholder text in touched runtime files. + +PASS - `npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list` + +## Manual Validation Notes + +1. Open `toolbox/sprites/index.html`. +2. Draw a pixel and confirm the center grid and right preview both update. +3. Erase the pixel and confirm both surfaces clear. +4. Fill, clear, undo, and redo; confirm both surfaces stay matched. +5. Use Picker and Zoom; confirm both surfaces remain matched. +6. Draw Line, Rectangle, and Circle shapes; confirm center grid and right preview both show the shape. + +## ZIP + +`dev/workspace/zip/PR_26179_CHARLIE_033-sprites-canvas-preview-sync_delta.zip` diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index c90cf54c4..9a7840b1a 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,6 +1,5 @@ assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs -docs_build/dev/reports/PR_26179_CHARLIE_032-sprites-shape-tools.md +docs_build/dev/reports/PR_26179_CHARLIE_033-sprites-canvas-preview-sync.md docs_build/dev/reports/codex_changed_files.txt docs_build/dev/reports/codex_review.diff -toolbox/sprites/index.html diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index ba223247c..5a6019b5f 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,331 +1,205 @@ diff --git a/assets/toolbox/sprites/js/index.js b/assets/toolbox/sprites/js/index.js -index 594a91d85..7f92105d7 100644 +index 7f92105d7..41ff3bdc4 100644 --- a/assets/toolbox/sprites/js/index.js +++ b/assets/toolbox/sprites/js/index.js -@@ -1,7 +1,8 @@ - const DEFAULT_GRID_SIZE = 16; - const SUPPORTED_GRID_SIZES = Object.freeze([16, 32]); - const SUPPORTED_ZOOM_LEVELS = Object.freeze([1, 2, 4]); --const EDITOR_TOOLS = Object.freeze(["pencil", "eraser", "fill", "picker", "zoom"]); -+const SHAPE_TOOLS = Object.freeze(["line", "rectangle", "circle"]); -+const EDITOR_TOOLS = Object.freeze(["pencil", "eraser", "fill", "picker", "zoom", ...SHAPE_TOOLS]); - const EDITOR_COLOR_KEYS = Object.freeze(["ink", "orange", "gold", "green", "blue"]); - const EDITOR_COLOR_CSS_VARIABLES = Object.freeze({ - blue: "--electric-blue", -@@ -16,6 +17,7 @@ const editorState = { - activeColor: "ink", - gridSize: DEFAULT_GRID_SIZE, - paintedPixels: new Map(), -+ shapeAnchor: null, - zoomLevel: 1, - }; - const editorHistory = { -@@ -35,6 +37,17 @@ function pixelKey(row, column) { - return `${row}:${column}`; +@@ -152,6 +152,7 @@ function applySnapshot(snapshot) { } -+function cellCoordinates(cell) { -+ return { -+ column: Number(cell.dataset.spritePixelColumn), -+ row: Number(cell.dataset.spritePixelRow), -+ }; -+} -+ -+function isInsideGrid(row, column) { -+ return row >= 1 && row <= editorState.gridSize && column >= 1 && column <= editorState.gridSize; -+} -+ - function stateSnapshot() { - return { - gridSize: editorState.gridSize, -@@ -132,6 +145,7 @@ function applyPaintedPixelsToGrid() { - function applySnapshot(snapshot) { - setGridSize(snapshot.gridSize, { recordHistory: false }); - editorState.paintedPixels = new Map(snapshot.paintedPixels); -+ editorState.shapeAnchor = null; - applyPaintedPixelsToGrid(); - updateDraftStatus(); - updateHistoryControls(); -@@ -168,6 +182,7 @@ function setActiveTool(toolName) { - return; - } - editorState.activeTool = toolName; -+ editorState.shapeAnchor = null; - document.querySelectorAll("[data-sprite-tool-button]").forEach((button) => { - const isActive = button.dataset.spriteToolButton === toolName; - button.classList.toggle("primary", isActive); -@@ -175,7 +190,11 @@ function setActiveTool(toolName) { - }); - const status = document.querySelector("[data-sprites-tool-status]"); + function updateDraftStatus() { ++ applyPaintedPixelsToGrid(); + const status = document.querySelector("[data-sprites-draft-status]"); if (status) { -- status.textContent = `${toolName[0].toUpperCase()}${toolName.slice(1)} is active. Drawing stays in unsaved editor state for this page session only.`; -+ if (SHAPE_TOOLS.includes(toolName)) { -+ status.textContent = `${toolName[0].toUpperCase()}${toolName.slice(1)} is active. Choose a start pixel, then an end pixel. Drawing stays in unsaved editor state for this page session only.`; -+ } else { -+ status.textContent = `${toolName[0].toUpperCase()}${toolName.slice(1)} is active. Drawing stays in unsaved editor state for this page session only.`; -+ } + status.textContent = draftStatusText(); +@@ -217,6 +218,8 @@ function setZoomLevel(zoomLevel) { + if (status) { + status.textContent = `Canvas zoom display: ${normalizedZoomLevel * 100}%.`; } ++ applyPaintedPixelsToGrid(); ++ renderPreview(); } -@@ -215,6 +234,109 @@ function pickCellColor(cell) { + function pickCellColor(cell) { +@@ -232,6 +235,8 @@ function pickCellColor(cell) { + if (status) { + status.textContent = `Picker selected ${colorLabel(colorKey)} from the unsaved editor canvas.`; } ++ applyPaintedPixelsToGrid(); ++ renderPreview(); } -+function setPixel(row, column, colorKey) { -+ if (!isInsideGrid(row, column)) { -+ return; -+ } -+ const key = pixelKey(row, column); -+ const normalizedColorKey = normalizeColorKey(colorKey); -+ const cell = document.querySelector(`[data-sprite-pixel-row="${row}"][data-sprite-pixel-column="${column}"]`); -+ editorState.paintedPixels.set(key, normalizedColorKey); -+ if (cell) { -+ setCellColor(cell, normalizedColorKey); -+ } + function setPixel(row, column, colorKey) { +diff --git a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs +index e1539d1c1..e68de5145 100644 +--- a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs ++++ b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs +@@ -201,6 +201,38 @@ function collectPageFailures(page) { + return { consoleErrors, failedRequests, pageErrors }; + } + ++function spriteCell(page, row, column) { ++ return page.locator(`[data-sprite-pixel-row="${row}"][data-sprite-pixel-column="${column}"]`); +} + -+function drawLine(start, end) { -+ let row = start.row; -+ let column = start.column; -+ const rowStep = Math.sign(end.row - start.row); -+ const columnStep = Math.sign(end.column - start.column); -+ const rowDistance = Math.abs(end.row - start.row); -+ const columnDistance = Math.abs(end.column - start.column); -+ let error = columnDistance - rowDistance; -+ -+ while (true) { -+ setPixel(row, column, editorState.activeColor); -+ if (row === end.row && column === end.column) { -+ break; -+ } -+ const doubledError = error * 2; -+ if (doubledError > -rowDistance) { -+ error -= rowDistance; -+ column += columnStep; -+ } -+ if (doubledError < columnDistance) { -+ error += columnDistance; -+ row += rowStep; ++async function previewCellHasPaint(page, row, column) { ++ const gridSize = Number(await page.locator("[data-sprites-pixel-grid]").getAttribute("data-sprites-grid-size")); ++ return page.locator("[data-sprites-preview-canvas]").evaluate((canvas, coordinates) => { ++ const context = canvas.getContext("2d"); ++ if (!context || !Number.isFinite(coordinates.gridSize) || coordinates.gridSize <= 0) { ++ return false; + } -+ } ++ const cellSize = canvas.width / coordinates.gridSize; ++ const x = Math.max(0, Math.min(canvas.width - 1, Math.floor((coordinates.column - 0.5) * cellSize))); ++ const y = Math.max(0, Math.min(canvas.height - 1, Math.floor((coordinates.row - 0.5) * cellSize))); ++ return context.getImageData(x, y, 1, 1).data[3] > 0; ++ }, { column, gridSize, row }); +} + -+function drawRectangle(start, end) { -+ const minRow = Math.min(start.row, end.row); -+ const maxRow = Math.max(start.row, end.row); -+ const minColumn = Math.min(start.column, end.column); -+ const maxColumn = Math.max(start.column, end.column); -+ for (let column = minColumn; column <= maxColumn; column += 1) { -+ setPixel(minRow, column, editorState.activeColor); -+ setPixel(maxRow, column, editorState.activeColor); -+ } -+ for (let row = minRow; row <= maxRow; row += 1) { -+ setPixel(row, minColumn, editorState.activeColor); -+ setPixel(row, maxColumn, editorState.activeColor); ++async function expectCenterAndPreviewPainted(page, row, column, colorClass = null) { ++ const cell = spriteCell(page, row, column); ++ await expect(cell).toHaveClass(/is-painted/); ++ if (colorClass) { ++ await expect(cell).toHaveClass(new RegExp(colorClass)); + } ++ expect(await previewCellHasPaint(page, row, column)).toBe(true); +} + -+function drawCircle(start, end) { -+ const radius = Math.max(Math.abs(end.row - start.row), Math.abs(end.column - start.column)); -+ if (radius === 0) { -+ setPixel(start.row, start.column, editorState.activeColor); -+ return; -+ } -+ for (let rowOffset = -radius; rowOffset <= radius; rowOffset += 1) { -+ for (let columnOffset = -radius; columnOffset <= radius; columnOffset += 1) { -+ const distance = Math.sqrt(rowOffset * rowOffset + columnOffset * columnOffset); -+ if (Math.abs(distance - radius) <= 0.6) { -+ setPixel(start.row + rowOffset, start.column + columnOffset, editorState.activeColor); -+ } -+ } -+ } ++async function expectCenterAndPreviewEmpty(page, row, column) { ++ await expect(spriteCell(page, row, column)).not.toHaveClass(/is-painted/); ++ expect(await previewCellHasPaint(page, row, column)).toBe(false); +} + -+function drawShape(start, end, toolName) { -+ if (toolName === "line") { -+ drawLine(start, end); -+ } else if (toolName === "rectangle") { -+ drawRectangle(start, end); -+ } else if (toolName === "circle") { -+ drawCircle(start, end); -+ } -+} + test("Sprite Creator shell loads with visible tool, canvas, details, and status regions", async ({ page }) => { + const server = await startSpriteShellTestServer(); + const failures = collectPageFailures(page); +@@ -366,3 +398,72 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status + await server.close(); + } + }); + -+function useShapeTool(cell) { -+ const coordinates = cellCoordinates(cell); -+ const status = document.querySelector("[data-sprites-tool-status]"); -+ if (!Number.isFinite(coordinates.row) || !Number.isFinite(coordinates.column)) { -+ return; -+ } -+ if (!editorState.shapeAnchor || editorState.shapeAnchor.toolName !== editorState.activeTool) { -+ editorState.shapeAnchor = { ...coordinates, toolName: editorState.activeTool }; -+ if (status) { -+ status.textContent = `${editorState.activeTool[0].toUpperCase()}${editorState.activeTool.slice(1)} start selected at row ${coordinates.row}, column ${coordinates.column}. Choose an end pixel.`; -+ } -+ return; -+ } ++test("Sprite Creator keeps center canvas and right preview in sync", async ({ page }) => { ++ const server = await startSpriteShellTestServer(); ++ const failures = collectPageFailures(page); + -+ pushUndoSnapshot(); -+ drawShape(editorState.shapeAnchor, coordinates, editorState.activeTool); -+ if (status) { -+ status.textContent = `${editorState.activeTool[0].toUpperCase()}${editorState.activeTool.slice(1)} added to the unsaved editor draft.`; -+ } -+ editorState.shapeAnchor = null; -+ updateDraftStatus(); -+} ++ try { ++ await page.goto(`${server.baseUrl}/toolbox/sprites/index.html`, { waitUntil: "networkidle" }); ++ ++ await spriteCell(page, 1, 1).click(); ++ await expectCenterAndPreviewPainted(page, 1, 1, "sprite-canvas-cell--ink"); ++ ++ await page.getByRole("button", { name: "Eraser tool" }).click(); ++ await spriteCell(page, 1, 1).click(); ++ await expectCenterAndPreviewEmpty(page, 1, 1); ++ ++ await page.getByRole("button", { name: "Blue editor color" }).click(); ++ await page.getByRole("button", { name: "Fill tool" }).click(); ++ await expectCenterAndPreviewPainted(page, 1, 1, "sprite-canvas-cell--blue"); ++ ++ await page.getByRole("button", { name: "Clear Canvas" }).click(); ++ await expectCenterAndPreviewEmpty(page, 1, 1); ++ ++ await page.getByRole("button", { name: "Undo" }).click(); ++ await expectCenterAndPreviewPainted(page, 1, 1, "sprite-canvas-cell--blue"); ++ ++ await page.getByRole("button", { name: "Redo" }).click(); ++ await expectCenterAndPreviewEmpty(page, 1, 1); ++ ++ await page.getByRole("button", { name: "Gold editor color" }).click(); ++ await page.getByRole("button", { name: "Pencil tool" }).click(); ++ await spriteCell(page, 2, 2).click(); ++ await expectCenterAndPreviewPainted(page, 2, 2, "sprite-canvas-cell--gold"); ++ await page.getByRole("button", { name: "Blue editor color" }).click(); ++ await page.getByRole("button", { name: "Picker tool" }).click(); ++ await spriteCell(page, 2, 2).click(); ++ await expect(page.locator("[data-sprites-palette-status]")).toContainText("Active editor color: Gold"); ++ await expectCenterAndPreviewPainted(page, 2, 2, "sprite-canvas-cell--gold"); ++ ++ await page.getByRole("button", { name: "Zoom tool" }).click(); ++ await page.getByRole("button", { name: "200%" }).click(); ++ await expect(page.locator("[data-sprites-grid-shell]")).toHaveAttribute("data-sprites-zoom-level", "2"); ++ await expectCenterAndPreviewPainted(page, 2, 2, "sprite-canvas-cell--gold"); + - function paintCell(cell) { - const key = pixelKey(cell.dataset.spritePixelRow, cell.dataset.spritePixelColumn); - if (editorState.activeTool === "picker") { -@@ -224,6 +346,10 @@ function paintCell(cell) { - if (editorState.activeTool === "zoom") { - return; - } -+ if (SHAPE_TOOLS.includes(editorState.activeTool)) { -+ useShapeTool(cell); -+ return; -+ } - if (editorState.activeTool === "eraser") { - if (!editorState.paintedPixels.has(key)) { - return; -@@ -247,6 +373,7 @@ function fillGrid() { - if (!grid) { - return; - } -+ editorState.shapeAnchor = null; - pushUndoSnapshot(); - editorState.paintedPixels.clear(); - grid.querySelectorAll("[data-sprite-pixel-row]").forEach((cell) => { -@@ -259,6 +386,7 @@ function fillGrid() { - - function clearCanvas(options = {}) { - const grid = document.querySelector("[data-sprites-pixel-grid]"); -+ editorState.shapeAnchor = null; - if (options.recordHistory !== false && editorState.paintedPixels.size > 0) { - pushUndoSnapshot(); - } -@@ -347,6 +475,7 @@ function setGridSize(size, options = {}) { - grid.replaceChildren(); - editorState.gridSize = size; - editorState.paintedPixels.clear(); -+ editorState.shapeAnchor = null; - grid.dataset.spritesGridSize = String(size); - grid.setAttribute("aria-label", gridLabel(size)); - -diff --git a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs -index 40812b4af..e1539d1c1 100644 ---- a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs -+++ b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs -@@ -10,9 +10,6 @@ const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - const repoRoot = path.resolve(__dirname, "..", "..", "..", ".."); - const SPRITE_TOOLBAR_PLACEHOLDERS = [ -- "Line", -- "Rectangle", -- "Circle", - "Move", - ]; - -@@ -228,6 +225,9 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status - await expect(page.getByRole("button", { name: "Pencil tool" })).toBeEnabled(); - await expect(page.getByRole("button", { name: "Eraser tool" })).toBeEnabled(); - await expect(page.getByRole("button", { name: "Fill tool" })).toBeEnabled(); -+ await expect(page.getByRole("button", { name: "Line tool" })).toBeEnabled(); -+ await expect(page.getByRole("button", { name: "Rectangle tool" })).toBeEnabled(); -+ await expect(page.getByRole("button", { name: "Circle tool" })).toBeEnabled(); - await expect(page.getByRole("button", { name: "Picker tool" })).toBeEnabled(); - await expect(page.getByRole("button", { name: "Zoom tool" })).toBeEnabled(); - for (const toolName of SPRITE_TOOLBAR_PLACEHOLDERS) { -@@ -315,11 +315,38 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status - await expect(page.locator("[data-sprites-zoom-status]")).toContainText("400%"); - await page.getByRole("button", { name: "100%" }).click(); - await expect(page.locator("[data-sprites-grid-shell]")).toHaveAttribute("data-sprites-zoom-level", "1"); -+ const gridCells = page.locator("[data-sprites-pixel-grid] [role='gridcell']"); + await page.getByRole("button", { name: "Clear Canvas" }).click(); + await page.getByRole("button", { name: "Green editor color" }).click(); + await page.getByRole("button", { name: "Line tool" }).click(); -+ await gridCells.nth(0).click(); -+ await expect(page.locator("[data-sprites-tool-status]")).toContainText("Line start selected"); -+ await gridCells.nth(5).click(); -+ await expect(page.locator("[data-sprites-tool-status]")).toContainText("Line added"); -+ await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(6); ++ await spriteCell(page, 1, 1).click(); ++ await spriteCell(page, 1, 4).click(); ++ await expectCenterAndPreviewPainted(page, 1, 2, "sprite-canvas-cell--green"); ++ + await page.getByRole("button", { name: "Clear Canvas" }).click(); + await page.getByRole("button", { name: "Rectangle tool" }).click(); -+ await gridCells.nth(0).click(); -+ await gridCells.nth(34).click(); -+ await expect(page.locator("[data-sprites-tool-status]")).toContainText("Rectangle added"); -+ await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(8); ++ await spriteCell(page, 2, 2).click(); ++ await spriteCell(page, 4, 4).click(); ++ await expectCenterAndPreviewPainted(page, 2, 3, "sprite-canvas-cell--green"); ++ + await page.getByRole("button", { name: "Clear Canvas" }).click(); + await page.getByRole("button", { name: "Circle tool" }).click(); -+ await gridCells.nth(68).click(); -+ await gridCells.nth(71).click(); -+ await expect(page.locator("[data-sprites-tool-status]")).toContainText("Circle added"); -+ const circlePaintedCount = await page.locator("[data-sprites-pixel-grid] .is-painted").count(); -+ expect(circlePaintedCount).toBeGreaterThan(8); - await expect(page.locator("[data-sprites-preview-canvas]")).toBeVisible(); - const previewHasPaint = await page.locator("[data-sprites-preview-canvas]").evaluate((canvas) => { - const context = canvas.getContext("2d"); -- const pixel = context.getImageData(1, 1, 1, 1).data; -- return pixel[3] > 0; -+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height).data; -+ for (let index = 3; index < imageData.length; index += 4) { -+ if (imageData[index] > 0) { -+ return true; -+ } -+ } -+ return false; - }); - expect(previewHasPaint).toBe(true); - const downloadPromise = page.waitForEvent("download"); -diff --git a/docs_build/dev/reports/PR_26179_CHARLIE_032-sprites-shape-tools.md b/docs_build/dev/reports/PR_26179_CHARLIE_032-sprites-shape-tools.md ++ await spriteCell(page, 8, 8).click(); ++ await spriteCell(page, 8, 11).click(); ++ await expectCenterAndPreviewPainted(page, 8, 11, "sprite-canvas-cell--green"); ++ ++ expect(failures.failedRequests).toEqual([]); ++ expect(failures.pageErrors).toEqual([]); ++ expect(failures.consoleErrors).toEqual([]); ++ } finally { ++ await server.close(); ++ } ++}); +diff --git a/docs_build/dev/reports/PR_26179_CHARLIE_033-sprites-canvas-preview-sync.md b/docs_build/dev/reports/PR_26179_CHARLIE_033-sprites-canvas-preview-sync.md new file mode 100644 -index 000000000..200ac1ce8 +index 000000000..4426f9ea2 --- /dev/null -+++ b/docs_build/dev/reports/PR_26179_CHARLIE_032-sprites-shape-tools.md -@@ -0,0 +1,71 @@ -+# PR_26179_CHARLIE_032-sprites-shape-tools ++++ b/docs_build/dev/reports/PR_26179_CHARLIE_033-sprites-canvas-preview-sync.md +@@ -0,0 +1,75 @@ ++# PR_26179_CHARLIE_033-sprites-canvas-preview-sync + +Team: CHARLIE + +Mode: Batch governance stacked feature workflow + -+Base branch: PR_26179_CHARLIE_031-sprites-picker-zoom ++Base branch: PR_26179_CHARLIE_032-sprites-shape-tools + -+Branch: PR_26179_CHARLIE_032-sprites-shape-tools ++Branch: PR_26179_CHARLIE_033-sprites-canvas-preview-sync + +## Summary + -+Implemented simple pixel-art shape tools for Sprite Creator. Line, Rectangle, and Circle now use a two-click start/end interaction and write only to the unsaved page-session editor draft. Move remains disabled/deferred. No persistence, database, API, schema, browser-owned authoritative product data, or start_of_day files were changed. ++Fixed the Sprite Creator center-canvas and right-preview sync path so both render from the same page-session editor state. The center grid is repainted from `editorState.paintedPixels` whenever draft status/preview refreshes, and picker/zoom paths also trigger a repaint even though they do not mutate pixels. + +## Branch Validation + +PASS + -+- Built on the previous stacked Sprite branch. -+- Scope stayed limited to shape tools. -+- Move remains disabled/deferred. ++- Built from `PR_26179_CHARLIE_032-sprites-shape-tools`. ++- Scope stayed limited to canvas/preview synchronization and regression coverage. +- No DB/API/schema changes. -+- No product save/load or authoritative browser-owned product data was added. -+- No stale PR #219-#228 code was copied. -+- ZIP package created at the user-requested batch path. ++- No persistence or browser-owned authoritative product data added. ++- No start_of_day files changed. ++- ZIP package created at the requested path. + +## Requirement Checklist + -+PASS - Line tool implemented with simple pixel-art behavior. ++PASS - Right preview updates from page-session editor state. ++ ++PASS - Center editor canvas updates from the same page-session editor state. ++ ++PASS - Drawing repaints center canvas immediately. ++ ++PASS - Erase repaints center canvas immediately. ++ ++PASS - Fill repaints center canvas immediately. + -+PASS - Rectangle tool implemented with outline behavior. ++PASS - Clear repaints center canvas immediately. + -+PASS - Circle tool implemented with outline behavior. ++PASS - Undo repaints center canvas immediately. + -+PASS - Shape tools use unsaved page-session editor state only. ++PASS - Redo repaints center canvas immediately. + -+PASS - No persistence added. ++PASS - Picker repaints center canvas and preview without mutating pixels. + -+PASS - No DB/API/schema changes. ++PASS - Zoom repaints center canvas and preview without mutating pixels. + -+PASS - No start_of_day changes. ++PASS - Shape tools repaint center canvas immediately. + -+PASS - Targeted Playwright coverage updated. ++PASS - Playwright regression coverage added for center canvas and preview sync. + +## Validation Lane + @@ -333,7 +207,7 @@ index 000000000..200ac1ce8 + +PASS - `node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs` + -+PASS - `git diff --check -- toolbox/sprites/index.html assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs` ++PASS - `git diff --check -- assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs` + +PASS - Runtime guard scan found no prohibited inline style/script/event-handler or stale placeholder text in touched runtime files. + @@ -342,45 +216,24 @@ index 000000000..200ac1ce8 +## Manual Validation Notes + +1. Open `toolbox/sprites/index.html`. -+2. Select Line, click a start pixel, then click an end pixel. -+3. Confirm a straight pixel-art line appears in the unsaved draft. -+4. Clear the canvas. -+5. Select Rectangle, click opposite corners, and confirm an outline appears. -+6. Clear the canvas. -+7. Select Circle, click a center pixel, then click a radius pixel. -+8. Confirm a simple pixel-art circle outline appears. -+9. Confirm no save, API, database, or product persistence workflow is introduced. ++2. Draw a pixel and confirm the center grid and right preview both update. ++3. Erase the pixel and confirm both surfaces clear. ++4. Fill, clear, undo, and redo; confirm both surfaces stay matched. ++5. Use Picker and Zoom; confirm both surfaces remain matched. ++6. Draw Line, Rectangle, and Circle shapes; confirm center grid and right preview both show the shape. + +## ZIP + -+`dev/workspace/zip/PR_26179_CHARLIE_032-sprites-shape-tools_delta.zip` ++`dev/workspace/zip/PR_26179_CHARLIE_033-sprites-canvas-preview-sync_delta.zip` diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt -index 9947f86b8..c90cf54c4 100644 +index c90cf54c4..9a7840b1a 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt -@@ -1,7 +1,6 @@ --assets/theme-v2/css/gamefoundrystudio.css +@@ -1,6 +1,5 @@ assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs --docs_build/dev/reports/PR_26179_CHARLIE_031-sprites-picker-zoom.md -+docs_build/dev/reports/PR_26179_CHARLIE_032-sprites-shape-tools.md +-docs_build/dev/reports/PR_26179_CHARLIE_032-sprites-shape-tools.md ++docs_build/dev/reports/PR_26179_CHARLIE_033-sprites-canvas-preview-sync.md docs_build/dev/reports/codex_changed_files.txt docs_build/dev/reports/codex_review.diff - toolbox/sprites/index.html -diff --git a/toolbox/sprites/index.html b/toolbox/sprites/index.html -index e4d4bfe59..3605cce9b 100644 ---- a/toolbox/sprites/index.html -+++ b/toolbox/sprites/index.html -@@ -38,9 +38,9 @@ - - - -- -- -- -+ -+ -+ - - - +-toolbox/sprites/index.html