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