diff --git a/inventory/templates/testing.html b/inventory/templates/testing.html index 314635d..2b22eda 100644 --- a/inventory/templates/testing.html +++ b/inventory/templates/testing.html @@ -17,13 +17,6 @@ margin: 0 auto; } -#toolBar { - top: 10px; - transform: translateX(-50%); - z-index: 10000; - max-width: calc(100% - 20px); -} - #toolBar::-webkit-scrollbar { height: 8px; } @@ -39,142 +32,149 @@ pointer-events: none; inset: 0; } + +#toolBar { + margin: 0 auto; +} + {% endblock %} {% block main %}
+
+
+ Grid Size: + + + Fill Opacity: + +
+
+ +
+
+ + + + + + + + + + + + + + +
+
+
+ + + + + + + + +
+
+
+ + + + +
+
-
-
- Grid Size: - -
-
- -
-
- - - - - - - - - - - - - -
-
-
- - - - - - - - -
-
-
- - - - -
-
- + @@ -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();