diff --git a/inventory/static/css/components/draw.css b/inventory/static/css/components/draw.css index 470078e..c225373 100644 --- a/inventory/static/css/components/draw.css +++ b/inventory/static/css/components/draw.css @@ -13,7 +13,6 @@ .grid-widget .grid-wrap { flex: 1 1 auto; width: 100%; - height: 100%; position: relative; overflow: hidden; min-height: 375px; @@ -47,28 +46,28 @@ } /* WIDE: 1 row */ -@container (min-width: 725px){ - .grid-widget [data-toolbar].toolbar{ - display: flex !important; - flex-wrap: nowrap; - align-items: center; - gap: .5rem; - overflow-x: auto; - overflow-y: hidden; - } +@container (min-width: 750px) { + .grid-widget [data-toolbar].toolbar { + display: flex !important; + flex-wrap: nowrap; + align-items: center; + gap: .5rem; + overflow-x: auto; + overflow-y: hidden; + } - .grid-widget [data-toolbar] .toolbar-row{ - display: contents; - } + .grid-widget [data-toolbar] .toolbar-row { + display: contents; + } - .grid-widget [data-toolbar] .toolbar-row--primary, - .grid-widget [data-toolbar] .toolbar-row--secondary{ - overflow: visible; - } + .grid-widget [data-toolbar] .toolbar-row--primary, + .grid-widget [data-toolbar] .toolbar-row--secondary { + overflow: visible; + } } .grid-widget [data-grid] { - position: relative; + position: absolute; cursor: crosshair; width: 100%; height: 100%; @@ -76,6 +75,13 @@ margin: 0 auto; max-width: 100%; z-index: 0; + inset: 0; +} + +.grid-widget [data-canvas], +.grid-widget [data-coords], +.grid-widget [data-dot] { + position: absolute; } .grid-widget [data-toolbar]::-webkit-scrollbar { @@ -97,6 +103,11 @@ z-index: 1; pointer-events: none; inset: 0; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: block; } .grid-widget [data-dot] { diff --git a/inventory/static/js/components/draw.js b/inventory/static/js/components/draw.js index db7e514..a7fb387 100644 --- a/inventory/static/js/components/draw.js +++ b/inventory/static/js/components/draw.js @@ -143,6 +143,11 @@ function initGridWidget(root, opts = {}) { activeGridWidget = api; }, { capture: true }); + function isInsideRect(clientX, clientY, rect) { + return clientX >= rect.left && clientX <= rect.right && + clientY >= rect.top && clientY <= rect.bottom; + } + function bindRangeWithLabel(inputEl, labelEl, format = (v) => v) { const sync = () => { labelEl.textContent = format(inputEl.value); }; inputEl.addEventListener('input', sync); @@ -187,7 +192,62 @@ function initGridWidget(root, opts = {}) { function dist2(a, b) { const dx = a.x - b.x, dy = a.y - b.y; - return dx * dx + dy * dy + return dx * dx + dy * dy; + } + + 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; + + const c1 = vx * wx + vy * wy; + if (c1 <= 0) return dist2(p, a); + + const c2 = vx * vx + vy * vy; + if (c2 <= c1) return dist2(p, b); + + const t = c1 / c2; + const proj = { x: a.x + t * vx, y: a.y + t * vy }; + return dist2(p, proj); + } + + function simplifyRDP(points, epsilon) { + if (!Array.isArray(points) || points.length < 3) return points || []; + const eps2 = epsilon * epsilon; + + function rdp(first, last, out) { + let maxD2 = 0; + let idx = -1; + + const a = points[first]; + const b = points[last]; + + for (let i = first + 1; i < last; ++i) { + const d2 = pointToSegmentDist2(points[i], a, b); + if (d2 > maxD2) { + maxD2 = d2; + idx = i; + } + } + + if (maxD2 > eps2 && idx !== -1) { + rdp(first, idx, out); + out.pop(); + rdp(idx, last, out); + } else { + out.push(a, b); + } + } + + const out = []; + rdp(0, points.length - 1, out); + + const deduped = [out[0]]; + for (let i = 1; i < out.length; i++) { + const prev = deduped[deduped.length - 1]; + const cur = out[i]; + if (prev.x !== cur.x || prev.y !== cur.y) deduped.push(cur); + } + return deduped; } function undo() { @@ -315,18 +375,29 @@ function initGridWidget(root, opts = {}) { const snappedW = snapDown(w, grid); const snappedH = snapDown(h, grid); - if (snappedW === lastApplied.w && snappedH === lastApplied.h) return; + // Only touch width-related CSS if width changed + const wChanged = snappedW !== lastApplied.w; + const hChanged = snappedH !== lastApplied.h; + if (!wChanged && !hChanged) return; + lastApplied = { w: snappedW, h: snappedH }; + // critical: don't let observer see our own updates as layout input + ro.disconnect(); + gridEl.style.width = `${snappedW}px`; gridEl.style.height = `${snappedH}px`; - toolBarEl.style.setProperty('--grid-maxw', `${snappedW}px`); + if (wChanged) { + root.style.setProperty('--grid-maxw', `${snappedW}px`); + } + + ro.observe(gridWrapEl); - gridEl.getBoundingClientRect(); resizeAndSetupCanvas(); } + function scheduleSnappedCellSize() { if (sizingRAF) return; sizingRAF = requestAnimationFrame(applySnappedCellSize); @@ -536,6 +607,8 @@ function initGridWidget(root, opts = {}) { const y2 = toPx(shape.y2); ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity); + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); @@ -633,15 +706,22 @@ function initGridWidget(root, opts = {}) { const pts = currentShape.points; if (pts.length >= 2) { - const simplified = [pts[0]]; - const minStep = 0.03; + const coarse = [pts[0]]; + const minStep = 0.02; for (let i = 1; i < pts.length; i++) { - if (dist2(pts[i], simplified[simplified.length - 1]) >= minStep * minStep) { - simplified.push(pts[i]); + if (dist2(pts[i], coarse[coarse.length - 1]) >= minStep * minStep) { + coarse.push(pts[i]); } } - if (simplified.length >= 2) { - finalShape = { ...currentShape, points: simplified }; + + if (coarse.length >= 2) { + const epsilon = Math.max(0.01, (currentShape.strokeWidth ?? 0.12) * 0.75); + + const simplified = simplifyRDP(coarse, epsilon); + + if (simplified.length >= 2) { + finalShape = { ...currentShape, points: simplified }; + } } } } else if (currentShape.tool === 'line') { @@ -797,23 +877,31 @@ function initGridWidget(root, opts = {}) { gridEl.addEventListener('pointermove', (e) => { if (!ctx) return; + const rect = gridEl.getBoundingClientRect(); + const inside = isInsideRect(e.clientX, e.clientY, rect); + const drawing = !!currentShape; + const { ix, iy, x: snapX, y: snapY, localX, localY } = snapToGrid(e.clientX, e.clientY); const tool = getActiveTool(); - coordsEl.classList.remove('d-none'); - - if (getActiveType() !== 'noGrid' && tool !== 'pen') { - dotEl.classList.remove('d-none'); - - 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 { + if (!drawing && !inside) { + coordsEl.classList.add('d-none'); dotEl.classList.add('d-none'); + } else { + coordsEl.classList.remove('d-none'); + + if (getActiveType() !== 'noGrid' && tool !== 'pen') { + dotEl.classList.remove('d-none'); + + const wrapRect = gridWrapEl.getBoundingClientRect(); + const offsetX = rect.left - wrapRect.left; + const offsetY = rect.top - wrapRect.top; + + dotEl.style.left = `${offsetX + snapX}px`; + dotEl.style.top = `${offsetY + snapY}px`; + } else { + dotEl.classList.add('d-none'); + } } if (getActiveType() == 'noGrid') { diff --git a/inventory/templates/testing.html b/inventory/templates/testing.html index 2f41707..713f4d2 100644 --- a/inventory/templates/testing.html +++ b/inventory/templates/testing.html @@ -8,12 +8,9 @@ {% block main %}
-
+
{{ draw.drawWidget('test1') }}
-
- {{ draw.drawWidget('test2') }} -
{% endblock %}