From 5dfc2691e904dc2982423647306a8b6dc8ac5dbf Mon Sep 17 00:00:00 2001 From: Conrad Nelson Date: Wed, 17 Dec 2025 12:29:58 -0600 Subject: [PATCH] Fix to finalizing shape behavior. --- inventory/templates/testing.html | 224 +++++++++++++++++++------------ 1 file changed, 136 insertions(+), 88 deletions(-) diff --git a/inventory/templates/testing.html b/inventory/templates/testing.html index 8010eb9..6e66761 100644 --- a/inventory/templates/testing.html +++ b/inventory/templates/testing.html @@ -256,6 +256,26 @@ resizeAndSetupCanvas(); setGrid(); scheduleSnappedCellSize(); +function renderAllWithPreview(previewShape = null, dashed = true) { + if (!ctx) return; + clearCanvas(); + shapes.forEach(drawShape); + + if (!previewShape) return; + + ctx.save(); + if (dashed) ctx.setLineDash([5, 3]); + drawShape(previewShape); + ctx.restore(); +} + +function penAddPoint(shape, clientX, clientY, minStep = 0.02) { + const p = pxToDocPoint(clientX, clientY); + const pts = shape.points; + const last = pts[pts.length - 1]; + if (!last || dist2(p, last) >= minStep * minStep) pts.push(p); +} + function pxToDocPoint(clientX, clientY) { const rect = gridEl.getBoundingClientRect(); const x = Math.min(Math.max(clientX, rect.left), rect.right) - rect.left; @@ -618,44 +638,103 @@ function clearCanvas() { } function setGrid() { - const type = getActiveType(); + const type = getActiveType(); - gridEl.style.backgroundImage = ""; - gridEl.style.backgroundSize = ""; - gridEl.style.boxShadow = "none"; - dotEl.classList.add('d-none'); + gridEl.style.backgroundImage = ""; + gridEl.style.backgroundSize = ""; + gridEl.style.backgroundPosition = ""; + gridEl.style.boxShadow = "none"; + dotEl.classList.add('d-none'); - if (type === 'fullGrid') { - gridEl.style.backgroundImage = - "linear-gradient(to right, #ccc 1px, transparent 1px)," + - "linear-gradient(to bottom, #ccc 1px, transparent 1px)"; - gridEl.style.backgroundSize = `${cellSize}px ${cellSize}px`; - gridEl.style.boxShadow = "inset 0 0 0 1px #ccc"; // full frame + // Minor dots + const dotPx = Math.max(1, Math.round(cellSize * 0.08)); + const minorColor = '#ddd'; - } else if (type === 'horizontalGrid') { - gridEl.style.backgroundImage = - "linear-gradient(to bottom, #ccc 1px, transparent 1px)"; - gridEl.style.backgroundSize = `100% ${cellSize}px`; + // Major dots (every 5 cells) + const majorStep = cellSize * 5; + const majorDotPx = Math.max(dotPx + 1, Math.round(cellSize * 0.12)); + const majorColor = '#c4c4c4'; - // left + right borders only - gridEl.style.boxShadow = - "inset 1px 0 0 0 #ccc, inset -1px 0 0 0 #ccc"; + const minorLayer = `radial-gradient(circle, ${minorColor} ${dotPx}px, transparent ${dotPx}px)`; + const majorLayer = `radial-gradient(circle, ${majorColor} ${majorDotPx}px, transparent ${majorDotPx}px)`; - } else if (type === 'verticalGrid') { - gridEl.style.backgroundImage = - "linear-gradient(to right, #ccc 1px, transparent 1px)"; - gridEl.style.backgroundSize = `${cellSize}px 100%`; + if (type === 'fullGrid') { + gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`; + gridEl.style.backgroundSize = `${majorStep}px ${majorStep}px, ${cellSize}px ${cellSize}px`; + gridEl.style.backgroundPosition = + `${majorStep / 2}px ${majorStep / 2}px, ${cellSize / 2}px ${cellSize / 2}px`; + gridEl.style.boxShadow = "inset 0 0 0 1px #ccc"; - // top + bottom borders only - gridEl.style.boxShadow = - "inset 0 1px 0 0 #ccc, inset 0 -1px 0 0 #ccc"; + } else if (type === 'verticalGrid') { + gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`; + gridEl.style.backgroundSize = `${majorStep}px 100%, ${cellSize}px 100%`; + gridEl.style.backgroundPosition = + `${majorStep / 2}px 0px, ${cellSize / 2}px 0px`; + gridEl.style.boxShadow = "inset 0 1px 0 0 #ccc, inset 0 -1px 0 0 #ccc"; - } else { // noGrid - gridEl.style.boxShadow = "inset 0 0 0 1px #ccc"; - dotEl.classList.add('d-none'); - } + } else if (type === 'horizontalGrid') { + gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`; + gridEl.style.backgroundSize = `100% ${majorStep}px, 100% ${cellSize}px`; + gridEl.style.backgroundPosition = + `0px ${majorStep / 2}px, 0px ${cellSize / 2}px`; + gridEl.style.boxShadow = "inset 1px 0 0 0 #ccc, inset -1px 0 0 0 #ccc"; + + } else { // noGrid + gridEl.style.boxShadow = "inset 0 0 0 1px #ccc"; + } } +function onPointerUp(e) { + if (!currentShape) return; + + // Only finalize if this pointer is the captured one (or we failed to capture, sigh) + if (gridEl.hasPointerCapture?.(e.pointerId)) { + gridEl.releasePointerCapture(e.pointerId); + } + + const { x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY); + + currentShape.x2 = snapX; + currentShape.y2 = snapY; + + let finalShape = null; + + if (currentShape.tool === 'pen') { + const pts = currentShape.points; + + if (pts.length >= 2) { + const simplified = [pts[0]]; + const minStep = 0.03; + for (let i = 1; i < pts.length; i++) { + if (dist2(pts[i], simplified[simplified.length - 1]) >= minStep * minStep) { + simplified.push(pts[i]); + } + } + if (simplified.length >= 2) { + finalShape = { ...currentShape, points: simplified }; + } + } + } else if (currentShape.tool === 'line') { + const line = normalizeLine(currentShape); + if (line.x1 !== line.x2 || line.y1 !== line.y2) finalShape = line; + + } else if (currentShape.tool === 'filled' || currentShape.tool === 'outline') { + const rect = normalizeRect(currentShape); + if (rect.w > 0 && rect.h > 0) finalShape = rect; + + } else if (currentShape.tool === 'filledEllipse' || currentShape.tool === 'outlineEllipse') { + const ellipse = normalizeEllipse(currentShape); + if (ellipse.w > 0 && ellipse.h > 0) finalShape = ellipse; + } + + if (finalShape) commit([...shapes, finalShape]); + + currentShape = null; + renderAllWithPreview(null); // clean final render +} + +gridEl.addEventListener('pointerup', onPointerUp); +window.addEventListener('pointerup', onPointerUp, { capture: true }); document.querySelectorAll('input[name="tool"]').forEach(input => { input.addEventListener('change', () => { @@ -793,12 +872,13 @@ gridEl.addEventListener('pointermove', (e) => { const gridRect = gridEl.getBoundingClientRect(); const wrapRect = gridWrapEl.getBoundingClientRect(); - const offsetX = gridRect.left - wrapRect.left; const offsetY = gridRect.top - wrapRect.top; dotEl.style.left = `${offsetX + snapX}px`; dotEl.style.top = `${offsetY + snapY}px`; + } else { + dotEl.classList.add('d-none'); } if (getActiveType() == 'noGrid') { @@ -807,66 +887,34 @@ gridEl.addEventListener('pointermove', (e) => { coordsEl.innerText = `(x=${ix} (${snapX}px) y=${iy} (${snapY}px))`; } - if (currentShape) { - const tool = currentShape.tool; + if (!currentShape) return; - clearCanvas(); - shapes.forEach(drawShape); - - ctx.save(); - ctx.setLineDash([5, 3]); - - if (currentShape.tool === 'pen') { - const p = pxToDocPoint(e.clientX, e.clientY); - - const pts = currentShape.points; - const last = pts[pts.length - 1]; - const minStep = 0.02; - if (!last || dist2(p, last) >= minStep * minStep) { - pts.push(p); - } - - clearCanvas(); - shapes.forEach(drawShape); - - ctx.save(); - ctx.setLineDash([5, 3]); - drawShape(currentShape); - ctx.setLineDash([]); - ctx.restore(); - - return; - } - - if (tool === 'line') { - const previewLine = normalizeLine({ - type: 'line', - x1: currentShape.x1, - y1: currentShape.y1, - x2: snapX, - y2: snapY, - color: currentShape.color - }); - drawShape(previewLine); - } else if (tool === 'filled' || tool === 'outline') { - const previewRect = normalizeRect({ - ...currentShape, - x2: snapX, - y2: snapY - }); - drawShape(previewRect); - } else if (tool === 'filledEllipse' || tool === 'outlineEllipse') { - const previewEllipse = normalizeEllipse({ - ...currentShape, - x2: snapX, - y2: snapY - }); - drawShape(previewEllipse); - } - - ctx.setLineDash([]); - ctx.restore(); + // PEN: mutate points and preview the same shape object + if (currentShape.tool === 'pen') { + penAddPoint(currentShape, e.clientX, e.clientY, 0.02); + renderAllWithPreview(currentShape, true); + return; } + + // Other tools: build a normalized preview shape + let preview = null; + + if (currentShape.tool === 'line') { + preview = normalizeLine({ + type: 'line', + x1: currentShape.x1, + y1: currentShape.y1, + x2: snapX, + y2: snapY, + color: currentShape.color + }); + } else if (currentShape.tool === 'filled' || currentShape.tool === 'outline') { + preview = normalizeRect({ ...currentShape, x2: snapX, y2: snapY }); + } else if (currentShape.tool === 'filledEllipse' || currentShape.tool === 'outlineEllipse') { + preview = normalizeEllipse({ ...currentShape, x2: snapX, y2: snapY }); + } + + renderAllWithPreview(preview, true); }); gridEl.addEventListener('pointerleave', (e) => {