From 5a2f480ef7467858afdb6b8081a41b8971f45fd0 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Mon, 12 Jan 2026 10:59:26 -0600 Subject: [PATCH] Selection! --- inventory/static/js/components/draw.js | 174 ++++++++++++++++++++++++- inventory/templates/testing.html | 4 +- 2 files changed, 174 insertions(+), 4 deletions(-) diff --git a/inventory/static/js/components/draw.js b/inventory/static/js/components/draw.js index b182231..aff7874 100644 --- a/inventory/static/js/components/draw.js +++ b/inventory/static/js/components/draw.js @@ -47,6 +47,8 @@ function initGridWidget(root, opts = {}) { ); let cellSize = Number(doc.cellSize) || 25; let viewerOffset = { x: 0, y: 0 }; + let selectedIndex = -1; + let selectedShape = null; let ctx; let dpr = 1; @@ -239,7 +241,7 @@ function initGridWidget(root, opts = {}) { newShape[key] = Math.round(shape[key] * 100); } }); - return {...shape, ...newShape}; + return { ...shape, ...newShape }; }); } @@ -617,6 +619,20 @@ function initGridWidget(root, opts = {}) { ctx.translate(viewerOffset.x, viewerOffset.y); } shapes.forEach(drawShape); + + if (selectedShape) { + ctx.save(); + ctx.globalAlpha = 1; + ctx.setLineDash([6, 4]); + drawShape({ + ...selectedShape, + fill: false, + strokeWidth: Math.max(selectedShape.strokeWidth ?? 0.12, 0.12) + (2 / cellSize), + strokeOpacity: 1 + }); + ctx.restore(); + } + ctx.restore(); } @@ -734,6 +750,8 @@ function initGridWidget(root, opts = {}) { } let currentShape = null; + let suppressNextClick = false; + const history = [structuredClone(shapes)]; let historyIndex = 0 @@ -943,6 +961,101 @@ function initGridWidget(root, opts = {}) { return dx * dx + dy * dy; } + function pickShapeAt(docPt, shapes, cellSize, opts = {}) { + const pxTol = opts.pxTol ?? 6; + const tol = pxTol / cellSize; + const tol2 = tol * tol; + + for (let i = shapes.length - 1; i >= 0; i--) { + const s = shapes[i]; + if (!s) continue; + + if (hitShape(docPt, s, tol, tol2)) { + return { index: i, shape: s }; + } + } + return null; + } + + function hitShape(p, s, tol, tol2) { + if (s.type === 'line') { + const a = { x: s.x1, y: s.y1 }; + const b = { x: s.x2, y: s.y2 }; + const sw = Math.max(0, Number(s.strokeWidth) || 0) / 2; + const t = tol + sw; + return pointToSegmentDist2(p, a, b) <= (t * t); + } + + if (s.type === 'path') { + const pts = (s.renderPoints?.length >= 2) ? s.renderPoints : s.points; + if (!pts || pts.length < 2) return false; + + const sw = Math.max(0, Number(s.strokeWidth) || 0) / 2; + const t = tol + sw; + + for (let i = 0; i < pts.length - 1; i++) { + if (pointToSegmentDist2(p, pts[i], pts[i + 1]) <= (t * t)) return true; + } + } + + if (s.type === 'rect') { + return hitRect(p, s, tol); + } + + if (s.type === 'ellipse') { + return hitEllipse(p, s, tol); + } + + return false; + } + + function hitRect(p, r, tol) { + const x1 = r.x, y1 = r.y, x2 = r.x + r.w, y2 = r.y + r.h; + const minX = Math.min(x1, x2), maxX = Math.max(x1, x2); + const minY = Math.min(y1, y2), maxY = Math.max(y1, y2); + + const inside = (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY); + + if (r.fill) { + return (p.x >= minX - tol && p.x <= maxX + tol && p.y >= minY - tol && p.y <= maxY + tol); + } + + if (!inside) { + if (p.x < minX - tol || p.x > maxX + tol || p.y < minY - tol || p.y > maxY + tol) return false; + } + + const nearLeft = Math.abs(p.x - minX) <= tol && p.y >= minY - tol && p.y <= maxY + tol; + const nearRight = Math.abs(p.x - maxX) <= tol && p.y >= minY - tol && p.y <= maxY + tol; + const nearTop = Math.abs(p.y - minY) <= tol && p.x >= minX - tol && p.x <= maxX + tol; + const nearBottom = Math.abs(p.y - minX) <= tol && p.x >= minX - tol && p.x <= maxX + tol; + + return nearLeft || nearRight || nearTop || nearBottom; + } + + function hitEllipse(p, e, tol) { + const cx = e.x + e.w / 2; + const cy = e.y + e.h / 2; + const rx = Math.abs(e.w / 2); + const ry = Math.abs(e.h / 2); + if (rx <= 0 || ry <= 0) return false; + + const nx = (p.x - cx) / rx; + const ny = (p.y - cy) / ry; + const d = nx * nx + ny * ny; + + if (e.fill) { + const rx2 = (rx + tol); + const ry2 = (ry + tol); + const nnx = (p.x - cx) / rx2; + const nny = (p.y - cy) / ry2; + return (nnx * nnx + nny * nny) <= 1; + } + + const minR = Math.max(1e-6, Math.min(rx, ry)); + const band = tol / minR; + return Math.abs(d - 1) <= Math.max(0.02, band); + } + function pointToSegmentDist2(p, a, b) { const vx = b.x - a.x, vy = b.y - a.y; const wx = p.x - a.x, wy = p.y - a.y; @@ -1336,7 +1449,13 @@ function initGridWidget(root, opts = {}) { if (ellipse.w > 0 && ellipse.h > 0) finalShape = ellipse; } - if (finalShape) commit([...shapes, finalShape]); + if (finalShape) { + commit([...shapes, finalShape]); + + + suppressNextClick = true; + setTimeout(() => { suppressNextClick = false; }, 0); + } currentShape = null; renderAllWithPreview(null); @@ -1344,6 +1463,57 @@ function initGridWidget(root, opts = {}) { gridEl.addEventListener('pointerup', finishPointer); + function setSelection(hit) { + if (!hit) { + selectedIndex = -1; + selectedShape = null; + redrawAll(); + return; + } + selectedIndex = hit.index; + selectedShape = hit.shape; + redrawAll(); + } + + gridEl.addEventListener('click', (e) => { + if (suppressNextClick) { + suppressNextClick = false; + return; + } + if (currentShape) return; + if (e.target.closest('[data-toolbar]')) return; + + const docPt = pxToDocPoint(e.clientX, e.clientY); + const hit = pickShapeAt(docPt, shapes, cellSize, { pxTol: 7 }); + setSelection(hit); + + if (hit) root.dispatchEvent(new CustomEvent('shape:click', { detail: hit })); + }); + + gridEl.addEventListener('contextmenu', (e) => { + e.preventDefault(); + if (currentShape) return; + + const docPt = pxToDocPoint(e.clientX, e.clientY); + const hit = pickShapeAt(docPt, shapes, cellSize, { pxTol: 7 }); + setSelection(hit); + + root.dispatchEvent(new CustomEvent('shape:contextmenu', { + detail: { hit, clientX: e.clientX, clientY: e.clientY } + })); + }); + + gridEl.addEventListener('dblclick', (e) => { + if (currentShape) return; + if (e.target.closest('[data-toolbar]')) return; + + const docPt = pxToDocPoint(e.clientX, e.clientY); + const hit = pickShapeAt(docPt, shapes, cellSize, { pxTol: 7 }); + setSelection(hit); + + if (hit) root.dispatchEvent(new CustomEvent('shape:dblclick', { detail: hit })); + }); + root.querySelectorAll('input[data-tool]').forEach((input) => { input.addEventListener('change', () => { if (input.checked) { diff --git a/inventory/templates/testing.html b/inventory/templates/testing.html index dd7804b..73f42e2 100644 --- a/inventory/templates/testing.html +++ b/inventory/templates/testing.html @@ -17,12 +17,12 @@
{{ draw.drawWidget('test1') }}
-
+
{% endblock %}