-
-
@@ -201,6 +201,7 @@ const importButtonEl = document.getElementById('importButton');
const importEl = document.getElementById('import');
const gridSizeEl = document.getElementById('gridSize');
const gridWrapEl = document.getElementById('gridWrap');
+const toolBarEl = document.getElementById('toolBar');
let doc = loadDoc();
let gridSize = Number(doc.gridSize) || 25;
@@ -234,10 +235,27 @@ resizeAndSetupCanvas();
window.addEventListener('resize', resizeAndSetupCanvas);
setGrid();
+scheduleSnappedGridSize();
+
+function isFiniteNum(n) { return Number.isFinite(Number(n)); }
+
+function sanitizeShapes(list) {
+ return list.filter(s => {
+ if (!s || typeof s !== 'object') return false;
+ if (!['rect','ellipse','line'].include(s.type)) return false;
+ if (!s.color) s.color = '#000000';
+
+ 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]));
+ }
+ });
+}
function loadDoc() {
- try { return JSON.parse(localStorage.getItem("gridDoc")) || DEFAULT_DOC; }
- catch { return DEFAULT_DOC; }
+ try { return JSON.parse(localStorage.getItem("gridDoc")) || structuredClone(DEFAULT_DOC); }
+ catch { return structuredClone(DEFAULT_DOC); }
}
function saveDoc(nextDoc = doc) {
@@ -267,7 +285,10 @@ function applySnappedGridSize() {
gridEl.style.width = `${snappedW}px`;
gridEl.style.height = `${snappedH}px`;
- requestAnimationFrame(resizeAndSetupCanvas);
+ toolBarEl.style.width = `${snappedW}px`;
+
+ gridEl.getBoundingClientRect();
+ resizeAndSetupCanvas();
}
function scheduleSnappedGridSize() {
@@ -280,8 +301,7 @@ function applyGridSize(newSize) {
if (!Number.isFinite(n) || n < 1) return;
gridSize = n;
- doc.gridSize = gridSize;
- saveDoc({ ...doc, shapes });
+ saveDoc({ ...doc, shapes, gridSize });
dotSize = Math.floor(Math.max(gridSize * 1.25, 32));
@@ -324,11 +344,9 @@ function setActiveType(typeId) {
function snapToGrid(x, y) {
/*
- For portability, we do not allow pixel coordinates in the data model
- and only use pixels for rendering. We display both spaces on the screen
- to ensure no matter the mode, the user is reasoning in the same two
- coordinate spaces as they need. Thus, snapping will happen even if
- the tool doesn't use it.
+ Shapes are stored in grid units (document units), not pixels.
+ 1 unit renders as gridSize pixels, so changing gridSize rescales (zooms) the whole drawing.
+ Grid modes only affect snapping/visuals; storage is always in document units for portability.
*/
const rect = gridEl.getBoundingClientRect();
@@ -362,7 +380,9 @@ function snapToGrid(x, y) {
ix,
iy,
x: snapX,
- y: snapY
+ y: snapY,
+ localX,
+ localY
};
}
@@ -401,10 +421,15 @@ function normalizeLine(shape) {
function resizeAndSetupCanvas() {
dpr = window.devicePixelRatio || 1;
- const rect = canvasEl.getBoundingClientRect();
- canvasEl.width = rect.width * dpr;
- canvasEl.height = rect.height * dpr;
+ const w = gridEl.clientWidth;
+ const h = gridEl.clientHeight;
+
+ canvasEl.width = Math.round(w * dpr);
+ canvasEl.height = Math.round(h * dpr);
+
+ canvasEl.style.width = `${w}px`;
+ canvasEl.style.height = `${h}px`;
ctx = canvasEl.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
@@ -538,6 +563,7 @@ document.querySelectorAll('input[name="gridType"]').forEach(input => {
localStorage.setItem('gridType', input.id);
}
setGrid();
+ redrawAll();
});
});
@@ -563,7 +589,7 @@ importEl.addEventListener('change', (e) => {
const loadedShapes = Array.isArray(data) ? data : data.shapes;
if (!Array.isArray(loadedShapes)) return;
- shapes = loadedShapes;
+ shapes = sanitizeShapes(loadedShapes);
doc = {
version: Number(data?.version) || 1,
@@ -571,7 +597,7 @@ importEl.addEventListener('change', (e) => {
shapes
};
- saveDoc(data);
+ saveDoc(doc);
redrawAll();
} catch {
toastMessage('Failed to load data from JSON file.', 'danger');
@@ -598,9 +624,7 @@ exportEl.addEventListener('click', () => {
clearEl.addEventListener('click', () => {
gridSize = 25;
shapes = [];
- doc.gridSize = gridSize;
- doc.shapes = shapes;
- saveDoc(doc);
+ saveDoc({...doc, shapes, gridSize});
gridSizeEl.value = 25;
applyGridSize(25);
redrawAll();
@@ -621,7 +645,7 @@ document.addEventListener('keydown', (e) => {
e.preventDefault();
if (shapes.length > 0) {
shapes.pop();
- saveShapes();
+ saveDoc({ ...doc, shapes, gridSize });
redrawAll();
}
}
@@ -632,10 +656,20 @@ document.addEventListener('keydown', (e) => {
}
});
+gridEl.addEventListener('pointercancel', () => {
+ currentShape = null;
+ redrawAll();
+});
+
+gridEl.addEventListener('lostpointercapture', () => {
+ currentShape = null;
+ redrawAll();
+});
+
gridEl.addEventListener('pointermove', (e) => {
if (!ctx) return;
- const { ix, iy, x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
+ const { ix, iy, x: snapX, y: snapY, localX, localY } = snapToGrid(e.clientX, e.clientY);
const renderX = snapX - dotSize / 2;
@@ -649,7 +683,11 @@ gridEl.addEventListener('pointermove', (e) => {
dotEl.style.left = `${renderX}px`;
}
- coordsEl.innerText = `(x=${ix} (${snapX}px) y=${iy} (${snapY}px) )`;
+ if (getActiveType() == 'noGrid') {
+ coordsEl.innerText = `(px x=${Math.round(localX)} y=${Math.round(localY)})`;
+ } else {
+ coordsEl.innerText = `(x=${ix} (${snapX}px) y=${iy} (${snapY}px))`;
+ }
if (currentShape) {
const tool = currentShape.tool;
@@ -725,7 +763,7 @@ gridEl.addEventListener('pointerdown', (e) => {
x2: snapX,
y2: snapY,
color: selectedColor,
- fill: document.getElementById('filled').checked
+ fill: (tool === 'filled' || tool === 'filledEllipse')
};
} else if (tool === 'outlineEllipse' || tool === 'filledEllipse') {
currentShape = {
@@ -773,8 +811,7 @@ window.addEventListener('pointerup', (e) => {
if (finalShape) {
shapes.push(finalShape);
- doc.shapes = shapes;
- saveDoc(doc);
+ saveDoc({...doc, shapes, gridSize});
}
clearCanvas();