Add undo/redo.

This commit is contained in:
Conrad Nelson 2025-12-17 09:59:32 -06:00
parent 641ae1470d
commit e8d9d1a330

View file

@ -205,6 +205,9 @@ const toolBarEl = document.getElementById('toolBar');
const fillOpacityEl = document.getElementById('fillOpacity');
let doc = loadDoc();
let shapes = sanitizeShapes(Array.isArray(doc.shapes) ? doc.shapes : []);
saveDoc({ ...doc, shapes });
let cellSize = Number(doc.cellSize) || 25;
cellSizeEl.value = cellSize;
let dotSize = Math.floor(Math.max(cellSize * 1.25, 32));
@ -212,10 +215,11 @@ let dotSize = Math.floor(Math.max(cellSize * 1.25, 32));
let ctx;
let dpr = 1;
let selectedColor;
let currentOpacity = clamp01(fillOpacity?.value ?? 0.15, 0.15);
let currentOpacity = clamp01(fillOpacityEl?.value ?? 0.15, 0.15);
let currentShape = null;
let shapes = Array.isArray(doc.shapes) ? doc.shapes : [];
const history = [structuredClone(shapes)];
let historyIndex = 0
let sizingRAF = 0;
let lastApplied = { w: 0, h: 0 };
@ -234,11 +238,36 @@ if (savedType) {
}
resizeAndSetupCanvas();
window.addEventListener('resize', resizeAndSetupCanvas);
setGrid();
scheduleSnappedcellSize();
function undo() {
if (historyIndex <= 0) return;
historyIndex--;
shapes = structuredClone(history[historyIndex]);
saveDoc({ ...doc, shapes, cellSize });
redrawAll();
}
function redo() {
if (historyIndex >= history.length - 1) return;
historyIndex++;
shapes = structuredClone(history[historyIndex]);
saveDoc({ ...doc, shapes, cellSize });
redrawAll();
}
function commit(nextShapes) {
history.splice(historyIndex + 1);
history.push(structuredClone(nextShapes));
historyIndex++;
shapes = nextShapes;
saveDoc({ ...doc, shapes, cellSize });
redrawAll();
}
function clamp01(n, fallback = 0.15) {
const x = Number(n);
return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : fallback;
@ -247,23 +276,27 @@ function clamp01(n, fallback = 0.15) {
function isFiniteNum(n) { return Number.isFinite(Number(n)); }
function sanitizeShapes(list) {
const allowed = ['rect', 'ellipse', 'line'];
const allowed = new Set(['rect','ellipse','line']);
return list.filter(s => {
if (!s || typeof s !== 'object') return false;
if (!allowed.includes(s.type)) return false;
return list.flatMap((s) => {
if (!s || typeof s !== 'object' || !allowed.has(s.type)) return [];
const color = typeof s.color === 'string' ? s.color : '#000000';
const opacity = clamp01(s.opacity, 0.15);
if (!s.color) s.color = '#000000';
if (s.type === 'line') {
if (!['x1','y1','x2','y2'].every(k => isFiniteNum(s[k]))) return [];
return [{ type:'line', x1:+s.x1, y1:+s.y1, x2:+s.x2, y2:+s.y2, color }];
}
if (s.opacity == null) s.opacity = 0.15;
s.opacity = clamp01(s.opacity, 0.15);
if (s.type === 'line') {
return ['x1','y1','x2','y2'].every(k => isFiniteNum(s[k]));
} else {
return ['x','y','w','h'].every(k => isFiniteNum(s[k]));
}
});
if (!['x','y','w','h'].every(k => isFiniteNum(s[k]))) return [];
return [{
type: s.type,
x:+s.x, y:+s.y, w:+s.w, h:+s.h,
color,
fill: !!s.fill,
opacity
}];
});
}
function loadDoc() {
@ -382,11 +415,11 @@ function snapToGrid(x, y) {
let snapY = localY;
if (type === 'fullGrid' || type === 'verticalGrid') {
snapX = ix * grid;
snapX = Math.min(ix * grid, rect.width);
}
if (type === 'fullGrid' || type === 'horizontalGrid') {
snapY = iy * grid;
snapY = Math.min(iy * grid, rect.height);
}
return {
@ -637,11 +670,9 @@ exportEl.addEventListener('click', () => {
clearEl.addEventListener('click', () => {
cellSize = 25;
shapes = [];
saveDoc({...doc, shapes, cellSize});
cellSizeEl.value = 25;
applycellSize(25);
redrawAll();
commit([]);
});
colorEl.addEventListener('input', () => {
@ -657,11 +688,11 @@ document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && key === 'z') {
e.preventDefault();
if (shapes.length > 0) {
shapes.pop();
saveDoc({ ...doc, shapes, cellSize });
redrawAll();
}
undo();
}
if ((e.ctrlKey || e.metaKey) && (key === 'y' || (e.shiftKey && key === 'z'))) {
e.preventDefault(); redo();
}
if (key === 'escape' && currentShape) {
@ -833,10 +864,7 @@ window.addEventListener('pointerup', (e) => {
if (ellipse.w > 0 && ellipse.h > 0) finalShape = ellipse;
}
if (finalShape) {
shapes.push(finalShape);
saveDoc({...doc, shapes, cellSize});
}
if (finalShape) commit([...shapes, finalShape]);
clearCanvas();
shapes.forEach(drawShape);