From 335afdab4b34dae012bf31b5bc7dd25004c502ff Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 25 May 2026 15:37:19 -0400 Subject: [PATCH] Integrate pixel-editor undo/redo with the top-level toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pixel editor undo/redo were a local stack with no redo and no connection to the global design-history stack. Several gaps: - No redo in the pixel editor. - The pixel snapshot only captured `indices`, not `w`/`h` — so undoing a rotate left the editor with swapped dims and stale data. - Standalone pixelart mode never pushed global history, so leaving the editor (or switching templates) lost the ability to undo pixel work from the global stack. - The pixel toolbar had its own ↶ Undo button duplicating the top-level toolbar's ↶. Changes: - `pixelSnapshot` now captures `{ indices, w, h }` and clears the redo stack on every new mutation. `_pixelRestoreState` updates the dims badge alongside indices, so rotate-then-undo is correct. - Added `pixelRedo` mirroring `pixelUndo`. `_pixelEdit.redo` lives on both editor inits. - Top-level `#undoBtn` / `#redoBtn` are now context-aware: while the pixel editor is open they route to `pixelUndo` / `pixelRedo`; otherwise they route to the global `undo` / `redo`. Tooltips and disabled state flip accordingly via `updateUndoRedoUI`. - Pixel-editor keydown handles Ctrl+Shift+Z and Ctrl+Y for redo. - `pixelCommitIfStandalone` pushes one debounced global history entry per stroke / shape / transform (600ms window), guarded by `_pixelHistorySuppress` so pixel undo/redo and serialize-for- publish don't pollute the global stack. - Removed the redundant local ↶ button from the pixel toolbar (the top-level button now drives it). Co-Authored-By: Claude Opus 4.7 --- src/index.html | 115 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 13 deletions(-) diff --git a/src/index.html b/src/index.html index 764ee98..9b740e1 100644 --- a/src/index.html +++ b/src/index.html @@ -3433,7 +3433,10 @@ - + + + + @@ -5768,6 +5771,15 @@ function updateUndoRedoUI() { const u = $('undoBtn'); const r = $('redoBtn'); + // While the pixel editor is open, the top-level Undo/Redo buttons drive + // the pixel editor's local undo/redo instead of the global design history. + if (_pixelEditActive && _pixelEdit) { + const canUndo = _pixelEdit.undo.length > 0; + const canRedo = _pixelEdit.redo.length > 0; + if (u) { u.disabled = !canUndo; u.title = canUndo ? 'Undo pixel edit (Ctrl+Z)' : 'Nothing to undo'; } + if (r) { r.disabled = !canRedo; r.title = canRedo ? 'Redo pixel edit (Ctrl+Shift+Z)' : 'Nothing to redo'; } + return; + } if (u) { u.disabled = _undoStack.length === 0; const last = _undoStack[_undoStack.length - 1]; @@ -8623,7 +8635,7 @@ tool: 'pencil', zoom: 12, gridOn: true, mirrorX: false, mirrorY: false, shapeFill: false, - undo: [], last: null, shapeBackup: null, shapeStart: null + undo: [], redo: [], last: null, shapeBackup: null, shapeStart: null }; _pixelEditActive = true; document.body.classList.add('pixel-editing'); @@ -8647,6 +8659,7 @@ const dims = $('pixelEditorDims'); if (dims) dims.textContent = `${w} × ${h} px`; renderPixelPalette(); renderPixelGrid(); + updateUndoRedoUI(); } function closePixelEditor(commit = true) { @@ -8670,6 +8683,7 @@ _pixelEdit = null; document.body.classList.remove('pixel-editing'); render(); + updateUndoRedoUI(); } function renderPixelGrid() { @@ -8730,16 +8744,43 @@ return { x, y }; } + let _pixelHistorySuppress = false; + function _pixelCloneState() { + return { + indices: _pixelEdit.indices.slice(), + w: _pixelEdit.w, h: _pixelEdit.h, + }; + } + function _pixelRestoreState(s) { + _pixelEdit.indices = s.indices; + _pixelEdit.w = s.w; + _pixelEdit.h = s.h; + const dims = $('pixelEditorDims'); + if (dims) dims.textContent = `${s.w} × ${s.h} px`; + renderPixelGrid(); + } function pixelSnapshot() { if (!_pixelEdit) return; - _pixelEdit.undo.push(_pixelEdit.indices.slice()); + _pixelEdit.undo.push(_pixelCloneState()); if (_pixelEdit.undo.length > 40) _pixelEdit.undo.shift(); + _pixelEdit.redo.length = 0; + updateUndoRedoUI(); } function pixelUndo() { if (!_pixelEdit || !_pixelEdit.undo.length) return; - _pixelEdit.indices = _pixelEdit.undo.pop(); - renderPixelGrid(); - pixelCommitIfStandalone(); + _pixelEdit.redo.push(_pixelCloneState()); + _pixelRestoreState(_pixelEdit.undo.pop()); + _pixelHistorySuppress = true; + try { pixelCommitIfStandalone(); } finally { _pixelHistorySuppress = false; } + updateUndoRedoUI(); + } + function pixelRedo() { + if (!_pixelEdit || !_pixelEdit.redo.length) return; + _pixelEdit.undo.push(_pixelCloneState()); + _pixelRestoreState(_pixelEdit.redo.pop()); + _pixelHistorySuppress = true; + try { pixelCommitIfStandalone(); } finally { _pixelHistorySuppress = false; } + updateUndoRedoUI(); } function pixelPaintCell(x, y, idx) { @@ -8814,6 +8855,40 @@ else if (tool === 'ellipse') pixelDrawEllipse(start.x, start.y, end.x, end.y, idx, fill); } + function pixelTransformOp(op) { + if (!_pixelEdit) return; + pixelSnapshot(); + const { w, h, indices } = _pixelEdit; + if (op === 'flipH') { + const out = new Uint8Array(w * h); + for (let y = 0; y < h; y++) + for (let x = 0; x < w; x++) out[y * w + (w - 1 - x)] = indices[y * w + x]; + _pixelEdit.indices = out; + } else if (op === 'flipV') { + const out = new Uint8Array(w * h); + for (let y = 0; y < h; y++) + for (let x = 0; x < w; x++) out[(h - 1 - y) * w + x] = indices[y * w + x]; + _pixelEdit.indices = out; + } else if (op === 'rotCW' || op === 'rotCCW') { + const newW = h, newH = w; + const out = new Uint8Array(newW * newH); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const nx = op === 'rotCW' ? h - 1 - y : y; + const ny = op === 'rotCW' ? x : w - 1 - x; + out[ny * newW + nx] = indices[y * w + x]; + } + } + _pixelEdit.w = newW; + _pixelEdit.h = newH; + _pixelEdit.indices = out; + const dims = $('pixelEditorDims'); + if (dims) dims.textContent = `${newW} × ${newH} px`; + } + renderPixelGrid(); + pixelCommitIfStandalone(); + } + function pixelFloodFill(x, y, idx) { const { w, h, indices } = _pixelEdit; const target = indices[y * w + x]; @@ -8901,6 +8976,11 @@ // Commit the standalone pixel editor's working grid into state.pixelArt. function pixelCommitIfStandalone() { if (!_pixelEdit || !_pixelEdit.standalone) return; + if (!_pixelHistorySuppress) { + // Debounced: a rapid sequence of strokes collapses to one global + // history entry; the first call in the window captures pre-edit state. + pushHistoryDebounced('pixelEditStandalone', 'edit pixel art', 600); + } state.pixelArt = { width: _pixelEdit.w, height: _pixelEdit.h, palette: _pixelEdit.palette.slice(), @@ -8929,6 +9009,7 @@ // Drop any live screen-share stream when the pixel editor closes — // the user gets a fresh permission dialog next time they enter it. stopScreenShareStream(); + updateUndoRedoUI(); } // Standalone Pixel Art mode: the #pixelEditorView is the whole canvas @@ -8946,7 +9027,7 @@ indices: decodePixelRLE(frame, w, h), palette, current: Math.min(1, palette.length - 1), tool: 'pencil', zoom: 12, gridOn: true, mirrorX: false, mirrorY: false, shapeFill: false, - undo: [], last: null, shapeBackup: null, shapeStart: null + undo: [], redo: [], last: null, shapeBackup: null, shapeStart: null }; _pixelEditActive = true; pixelSyncToolButtons(); @@ -8956,6 +9037,7 @@ const dims = $('pixelEditorDims'); if (dims) dims.textContent = `${w} × ${h} px`; renderPixelPalette(); renderPixelGrid(); + updateUndoRedoUI(); requestAnimationFrame(() => { if (!_pixelEdit || !_pixelEdit.standalone) return; const wrap = $('pixelEditorCanvasWrap'); @@ -13181,7 +13263,8 @@ // A standalone pixel-art design — one grid, published as casewrap-pixelart. function pixelArtPayload() { - pixelCommitIfStandalone(); + _pixelHistorySuppress = true; + try { pixelCommitIfStandalone(); } finally { _pixelHistorySuppress = false; } return { version: SCHEMA_VERSION, templateMode: 'pixelart', @@ -19106,7 +19189,6 @@ $('designObjectDoneBtn')?.addEventListener('click', () => finishDesignObjectEdit(true)); $('designObjectCancelBtn')?.addEventListener('click', () => finishDesignObjectEdit(false)); $('pixelEditorDoneBtn')?.addEventListener('click', () => closePixelEditor(true)); - $('pixelUndoBtn')?.addEventListener('click', pixelUndo); $('pixelZoomIn')?.addEventListener('click', () => pixelStepZoom(1)); $('pixelZoomOut')?.addEventListener('click', () => pixelStepZoom(-1)); $('pixelGridToggle')?.addEventListener('change', (e) => { @@ -19252,13 +19334,19 @@ pixelExportMenuToggle(false); }); } - document.querySelectorAll('.pixelToolBtn').forEach((btn) => { + document.querySelectorAll('.pixelToolBtn[data-ptool]').forEach((btn) => { btn.addEventListener('click', () => { if (!_pixelEdit) return; _pixelEdit.tool = btn.dataset.ptool; pixelSyncToolButtons(); }); }); + document.querySelectorAll('.pixelToolBtn[data-ptransform]').forEach((btn) => { + btn.addEventListener('click', () => { + if (!_pixelEdit) return; + pixelTransformOp(btn.dataset.ptransform); + }); + }); const _pgc = $('pixelGridCanvas'); if (_pgc) { let _painting = false; @@ -19291,7 +19379,8 @@ document.addEventListener('keydown', (e) => { if (!_pixelEditActive) return; const k = (e.key || '').toLowerCase(); - if ((e.ctrlKey || e.metaKey) && k === 'z') { e.preventDefault(); e.stopPropagation(); pixelUndo(); } + if ((e.ctrlKey || e.metaKey) && k === 'z') { e.preventDefault(); e.stopPropagation(); e.shiftKey ? pixelRedo() : pixelUndo(); } + else if ((e.ctrlKey || e.metaKey) && k === 'y') { e.preventDefault(); e.stopPropagation(); pixelRedo(); } else if (k === 'escape' && _pixelEdit && !_pixelEdit.standalone) { e.preventDefault(); e.stopPropagation(); closePixelEditor(true); } else if (!e.ctrlKey && !e.metaKey && !isEditableFocus()) { const map = { p: 'pencil', e: 'eraser', f: 'fill', i: 'eyedropper', l: 'line', r: 'rect', o: 'ellipse' }; @@ -20462,8 +20551,8 @@ }); // ESC closes whichever overlay modal is on top. - on('undoBtn', 'click', undo); - on('redoBtn', 'click', redo); + on('undoBtn', 'click', () => { (_pixelEditActive && _pixelEdit) ? pixelUndo() : undo(); }); + on('redoBtn', 'click', () => { (_pixelEditActive && _pixelEdit) ? pixelRedo() : redo(); }); // Tool palette + tool option wiring. for (const t of EDITOR_TOOLS) {