Add undo/redo.
This commit is contained in:
parent
641ae1470d
commit
e8d9d1a330
1 changed files with 59 additions and 31 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue