mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-11-04 04:30:40 +00:00
Integrate pixel-editor undo/redo with the top-level toolbar
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 <[email protected]>
This commit is contained in:
1 parent
f9f800985d
commit
335afdab4b
1 file changed
+102
-13
+102
-13
@@ -3433,7 +3433,10 @@
|
||||
<button type="button" class="pixelToolBtn" data-ptool="rect" title="Rectangle (R)">□</button>
|
||||
<button type="button" class="pixelToolBtn" data-ptool="ellipse" title="Ellipse (O)">○</button>
|
||||
</div>
|
||||
<button type="button" id="pixelUndoBtn" class="pixelBarBtn" title="Undo">↶</button>
|
||||
<button type="button" class="pixelToolBtn" data-ptransform="flipH" title="Flip horizontal">⇄</button>
|
||||
<button type="button" class="pixelToolBtn" data-ptransform="flipV" title="Flip vertical">⇅</button>
|
||||
<button type="button" class="pixelToolBtn" data-ptransform="rotCCW" title="Rotate 90° left">↺</button>
|
||||
<button type="button" class="pixelToolBtn" data-ptransform="rotCW" title="Rotate 90° right">↻</button>
|
||||
<span class="pixelBarDivider"></span>
|
||||
<label class="pixelBarToggle"><input type="checkbox" id="pixelGridToggle" checked /> Grid</label>
|
||||
<label class="pixelBarToggle"><input type="checkbox" id="pixelMirrorX" /> Mirror X</label>
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user