diff --git a/inventory/static/css/components/draw.css b/inventory/static/css/components/draw.css deleted file mode 100644 index 36d6bc0..0000000 --- a/inventory/static/css/components/draw.css +++ /dev/null @@ -1,40 +0,0 @@ -.grid-widget .grid-wrap { - width: 100%; - height: 80vh; - position: relative; -} - -.grid-widget [data-grid] { - position: relative; - cursor: crosshair; - width: 100%; - height: 100%; - touch-action: none; - margin: 0 auto; -} - -.grid-widget [data-toolbar]::-webkit-scrollbar { - height: 8px; -} - -.grid-widget [data-coords] { - bottom: 10px; - pointer-events: none; - left: 10px; -} - -.grid-widget [data-canvas] { - z-index: 9999; - pointer-events: none; - inset: 0; -} - -.grid-widget [data-toolbar] { - margin: 0 auto; -} - -.grid-widget [data-dot] { - transform: translate(-50%, -50%); - z-index: 10000; - pointer-events: none; -} \ No newline at end of file diff --git a/inventory/static/js/components/draw.js b/inventory/static/js/components/draw.js deleted file mode 100644 index b968cc7..0000000 --- a/inventory/static/js/components/draw.js +++ /dev/null @@ -1,877 +0,0 @@ -document.querySelectorAll('[data-grid-widget]').forEach((root, index) => { - initGridWidget(root, { storageKey: `gridDoc:${index}` }); -}); - - -let activeGridWidget = null; - -document.addEventListener('keydown', (e) => { - if (!activeGridWidget) return; - activeGridWidget.handleKeyDown(e); -}); - - -function initGridWidget(root, opts = {}) { - const DEFAULT_DOC = { version: 1, cellSize: 25, shapes: [] }; - - const canvasEl = root.querySelector('[data-canvas]'); - const clearEl = root.querySelector('[data-clear]'); - const colorEl = root.querySelector('[data-color]'); - const coordsEl = root.querySelector('[data-coords]'); - const dotEl = root.querySelector('[data-dot]'); - const dotSVGEl = root.querySelector('[data-dot-svg]'); - const exportEl = root.querySelector('[data-export]'); - const gridEl = root.querySelector('[data-grid]'); - const importButtonEl = root.querySelector('[data-import-button]'); - const importEl = root.querySelector('[data-import]'); - const cellSizeEl = root.querySelector('[data-cell-size]'); - const gridWrapEl = root.querySelector('[data-grid-wrap]'); - const toolBarEl = root.querySelector('[data-toolbar]'); - const fillOpacityEl = root.querySelector('[data-fill-opacity]'); - const strokeOpacityEl = root.querySelector('[data-stroke-opacity]'); - const strokeWidthEl = root.querySelector('[data-stroke-width]'); - - const storageKey = opts.storageKey ?? 'gridDoc'; - - let doc = loadDoc(); - let shapes = sanitizeShapes(Array.isArray(doc.shapes) ? doc.shapes : []); - saveDoc({ ...doc, shapes }); - - const savedTool = localStorage.getItem(`${storageKey}:tool`); - if (savedTool) setActiveTool(savedTool); - - const savedType = localStorage.getItem(`${storageKey}:gridType`); - if (savedType) setActiveType(savedType); - - let cellSize = Number(doc.cellSize) || 25; - cellSizeEl.value = cellSize; - let dotSize = Math.floor(Math.max(cellSize * 1.25, 32)); - - let ctx; - let dpr = 1; - let selectedColor; - let currentFillOpacity = clamp01(fillOpacityEl?.value ?? 1, 1); - let currentStrokeOpacity = clamp01(strokeOpacityEl?.value ?? 1, 1); - let currentStrokeWidth = Number(strokeWidthEl?.value ?? 0.12) || 0.12; - - let currentShape = null; - const history = [structuredClone(shapes)]; - let historyIndex = 0 - - let sizingRAF = 0; - let lastApplied = { w: 0, h: 0 }; - - const ro = new ResizeObserver(scheduleSnappedCellSize); - ro.observe(gridWrapEl); - - resizeAndSetupCanvas(); - - setGrid(); - scheduleSnappedCellSize(); - - let activePointerId = null; - - const api = { - handleKeyDown(e) { - const key = e.key.toLowerCase(); - const t = e.target; - - const isTextField = t && root.contains(t) && (t.matches('input, textarea, select') || t.isContentEditable); - - if (isTextField) { - const isUndo = (e.ctrlKey || e.metaKey) && key === 'z'; - const isRedo = (e.ctrlKey || e.metaKey) && (key === 'y' || (key === 'z' && e.shiftKey)); - - if (!isUndo && !isRedo) return; - } - - if ((e.ctrlKey || e.metaKey) && key === 'z') { - e.preventDefault(); - if (e.shiftKey) redo(); - else undo(); - return; - } - - if ((e.ctrlKey || e.metaKey) && key === 'y') { - e.preventDefault(); - redo(); - return; - } - - if (key === 'escape' && currentShape) { - e.preventDefault(); - currentShape = null; - redrawAll(); - } - } - }; - - root.addEventListener('focusin', () => { activeGridWidget = api; }); - - root.addEventListener('pointerdown', () => { - activeGridWidget = api; - }, { capture: true }); - - function finishPointer(e) { - if (!currentShape) return; - if (activePointerId !== null && e.pointerId !== activePointerId) return; - - onPointerUp(e); - activePointerId = null; - } - - 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; - const y = Math.min(Math.max(clientY, rect.top), rect.bottom) - rect.top; - return { x: pxToGrid(x), y: pxToGrid(y) }; - } - - function dist2(a, b) { - const dx = a.x - b.x, dy = a.y - b.y; - return dx * dx + dy * dy - } - - function undo() { - if (historyIndex <= 0) return; - historyIndex--; - shapes = structuredClone(history[historyIndex]); - saveDoc({ ...doc, shapes, cellSize }); - redrawAll(); - } - - function redo() { - if (historyIndex >= history.length - 1) return; - historyIndex++; - shapes = structuredClone(history[historyIndex]); - saveDoc({ ...doc, shapes, cellSize }); - redrawAll(); - } - - function commit(nextShapes) { - history.splice(historyIndex + 1); - history.push(structuredClone(nextShapes)); - historyIndex++; - shapes = nextShapes; - - saveDoc({ ...doc, shapes, cellSize }); - redrawAll(); - } - - function clamp01(n, fallback = 1) { - const x = Number(n); - return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : fallback; - } - - function isFiniteNum(n) { return Number.isFinite(Number(n)); } - - function sanitizeShapes(list) { - const allowed = new Set(['rect', 'ellipse', 'line', 'path']); - - const normStroke = (v, fallback = 0.12) => { - const n = Number(v); - if (!Number.isFinite(n)) return fallback; - return Math.max(0, n); - }; - - return list.flatMap((s) => { - if (!s || typeof s !== 'object' || !allowed.has(s.type)) return []; - - const color = typeof s.color === 'string' ? s.color : '#000000'; - const fillOpacity = clamp01(s.fillOpacity, 1); - const strokeOpacity = clamp01(s.strokeOpacity, 1); - - if (s.type === 'line') { - if (!['x1', 'y1', 'x2', 'y2'].every(k => isFiniteNum(s[k]))) return []; - return [{ - type: 'line', - x1: +s.x1, y1: +s.y1, x2: +s.x2, y2: +s.y2, - color, - strokeWidth: normStroke(s.strokeWidth), - strokeOpacity - }]; - } - - if (s.type === 'path') { - if (!Array.isArray(s.points) || s.points.length < 2) return []; - const points = s.points.flatMap(p => { - if (!p || !isFiniteNum(p.x) || !isFiniteNum(p.y)) return []; - return [{ x: +p.x, y: +p.y }]; - }); - if (points.length < 2) return []; - - return [{ - type: 'path', - points, - color, - width: normStroke(s.width, 0.12), - strokeOpacity - }]; - } - - if (!['x', 'y', 'w', 'h'].every(k => isFiniteNum(s[k]))) return []; - return [{ - type: s.type, - x: +s.x, y: +s.y, w: +s.w, h: +s.h, - color, - fill: !!s.fill, - fillOpacity, - strokeOpacity, - strokeWidth: normStroke(s.strokeWidth) - }]; - }); - } - - function loadDoc() { - try { return JSON.parse(localStorage.getItem(storageKey)) || structuredClone(DEFAULT_DOC); } - catch { return structuredClone(DEFAULT_DOC); } - } - - function saveDoc(nextDoc = doc) { - doc = nextDoc; - try { localStorage.setItem(storageKey, JSON.stringify(nextDoc)); } catch { } - } - - function snapDown(n, step) { - return Math.floor(n / step) * step; - } - - function applySnappedCellSize() { - sizingRAF = 0; - - const grid = cellSize; - if (!Number.isFinite(grid) || grid < 1) return; - - const w = gridWrapEl.clientWidth; - const h = gridWrapEl.clientHeight; - - const snappedW = snapDown(w, grid); - const snappedH = snapDown(h, grid); - - if (snappedW === lastApplied.w && snappedH === lastApplied.h) return; - lastApplied = { w: snappedW, h: snappedH }; - - gridEl.style.width = `${snappedW}px`; - gridEl.style.height = `${snappedH}px`; - - toolBarEl.style.width = `${snappedW}px`; - - gridEl.getBoundingClientRect(); - resizeAndSetupCanvas(); - } - - function scheduleSnappedCellSize() { - if (sizingRAF) return; - sizingRAF = requestAnimationFrame(applySnappedCellSize); - } - - function applyCellSize(newSize) { - const n = Number(newSize); - if (!Number.isFinite(n) || n < 1) return; - - cellSize = n; - saveDoc({ ...doc, shapes, cellSize }); - - dotSize = Math.floor(Math.max(cellSize * 1.25, 32)); - - dotSVGEl.setAttribute('width', dotSize); - dotSVGEl.setAttribute('height', dotSize); - - setGrid(); - scheduleSnappedCellSize(); - } - - function pxToGrid(v) { - return v / cellSize; - } - - function getActiveTool() { - const checked = root.querySelector('input[data-tool]:checked'); - return checked ? checked.value : 'pen'; - } - - function setActiveTool(toolValue) { - const el = root.querySelector(`input[data-tool][value="${CSS.escape(toolValue)}"]`); - if (el) el.checked = true; - } - - function getActiveType() { - const checked = root.querySelector('input[data-gridtype]:checked'); - return checked ? checked.value : 'noGrid'; - } - - function setActiveType(typeValue) { - const el = root.querySelector(`input[data-gridtype][value="${CSS.escape(typeValue)}"]`); - if (el) el.checked = true; - } - - function snapToGrid(x, y) { - /* - Shapes are stored in grid units (document units), not pixels. - 1 unit renders as cellSize pixels, so changing cellSize rescales (zooms) the whole drawing. - Grid modes only affect snapping/visuals; storage is always in document units for portability. - */ - - const rect = gridEl.getBoundingClientRect(); - const clampedX = Math.min(Math.max(x, rect.left), rect.right); - const clampedY = Math.min(Math.max(y, rect.top), rect.bottom); - - const localX = clampedX - rect.left; - const localY = clampedY - rect.top; - - const grid = cellSize; - const maxIx = Math.floor(rect.width / grid); - const maxIy = Math.floor(rect.height / grid); - - const ix = Math.min(Math.max(Math.round(localX / grid), 0), maxIx); - const iy = Math.min(Math.max(Math.round(localY / grid), 0), maxIy); - - const type = getActiveType(); - - let snapX = localX; - let snapY = localY; - - if (type === 'fullGrid' || type === 'verticalGrid') { - snapX = Math.min(ix * grid, rect.width); - } - - if (type === 'fullGrid' || type === 'horizontalGrid') { - snapY = Math.min(iy * grid, rect.height); - } - - return { - ix, - iy, - x: snapX, - y: snapY, - localX, - localY - }; - } - - function normalizeRect(shape) { - const x1 = pxToGrid(shape.x1); - const y1 = pxToGrid(shape.y1); - const x2 = pxToGrid(shape.x2); - const y2 = pxToGrid(shape.y2); - - return { - type: 'rect', - x: Math.min(x1, x2), - y: Math.min(y1, y2), - w: Math.abs(x2 - x1), - h: Math.abs(y2 - y1), - color: shape.color, - fill: shape.fill, - fillOpacity: clamp01(shape.fillOpacity, 1), - strokeOpacity: clamp01(shape.strokeOpacity, 1), - strokeWidth: isFiniteNum(shape.strokeWidth) ? Math.max(0, +shape.strokeWidth) : 0.12 - }; - } - - function normalizeEllipse(shape) { - const r = normalizeRect(shape); - return { ...r, type: 'ellipse' }; - } - - function normalizeLine(shape) { - return { - type: 'line', - x1: pxToGrid(shape.x1), - y1: pxToGrid(shape.y1), - x2: pxToGrid(shape.x2), - y2: pxToGrid(shape.y2), - color: shape.color, - strokeWidth: isFiniteNum(shape.strokeWidth) ? Math.max(0, +shape.strokeWidth) : 0.12, - strokeOpacity: clamp01(shape.strokeOpacity) - }; - } - - function resizeAndSetupCanvas() { - dpr = window.devicePixelRatio || 1; - - 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); - - selectedColor = colorEl.value || '#000000'; - const circle = dotSVGEl.querySelector('circle'); - if (circle) { - circle.setAttribute('fill', selectedColor); - } - - redrawAll(); - } - - function redrawAll() { - if (!ctx || !shapes) return; - - clearCanvas(); - shapes.forEach(drawShape); - } - - function drawShape(shape) { - if (!ctx) return; - const toPx = (v) => v * cellSize; - - ctx.save(); - ctx.strokeStyle = shape.color || '#000000'; - ctx.lineWidth = Math.max(1, toPx(shape.strokeWidth ?? 0.12)); - - if (shape.type === 'rect' || shape.type === 'ellipse') { - const x = toPx(shape.x); - const y = toPx(shape.y); - const w = toPx(shape.w); - const h = toPx(shape.h); - - ctx.globalAlpha = clamp01(shape.strokeOpacity, 1); - if (shape.type === 'rect') { - ctx.strokeRect(x, y, w, h); - } else { - const cx = x + w / 2; - const cy = y + h / 2; - ctx.beginPath(); - ctx.ellipse(cx, cy, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2); - ctx.stroke(); - } - ctx.globalAlpha = 1; - - if (shape.fill) { - ctx.globalAlpha = clamp01(shape.fillOpacity, 1); - ctx.fillStyle = shape.color; - if (shape.type === 'rect') { - ctx.fillRect(x, y, w, h); - } else { - const cx = x + w / 2; - const cy = y + h / 2; - ctx.beginPath(); - ctx.ellipse(cx, cy, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2); - ctx.fill() - } - ctx.globalAlpha = 1; - } - - } else if (shape.type === 'line') { - const x1 = toPx(shape.x1); - const y1 = toPx(shape.y1); - const x2 = toPx(shape.x2); - const y2 = toPx(shape.y2); - - ctx.globalAlpha = clamp01(shape.strokeOpacity, 1); - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.stroke(); - ctx.globalAlpha = 1; - } else if (shape.type === 'path') { - const toPx = (v) => v * cellSize; - - ctx.globalAlpha = clamp01(shape.strokeOpacity, 1); - - ctx.lineWidth = Math.max(1, toPx(shape.width ?? 0.12)); - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; - - const pts = shape.points; - ctx.beginPath(); - ctx.moveTo(toPx(pts[0].x), toPx(pts[0].y)); - for (let i = 1; i < pts.length; i++) { - ctx.lineTo(toPx(pts[i].x), toPx(pts[i].y)); - } - ctx.stroke(); - } - - ctx.restore(); - } - - function clearCanvas() { - if (!ctx) return; - ctx.clearRect(0, 0, canvasEl.width / dpr, canvasEl.height / dpr); - } - - function setGrid() { - const type = getActiveType(); - - gridEl.style.backgroundImage = ""; - gridEl.style.backgroundSize = ""; - gridEl.style.backgroundPosition = ""; - gridEl.style.boxShadow = "none"; - dotEl.classList.add('d-none'); - - // Minor dots - const dotPx = Math.max(1, Math.round(cellSize * 0.08)); - const minorColor = '#ddd'; - - // Major dots (every 5 cells) - const majorStep = cellSize * 5; - const majorDotPx = Math.max(dotPx + 1, Math.round(cellSize * 0.12)); - const majorColor = '#c4c4c4'; - - const minorLayer = `radial-gradient(circle, ${minorColor} ${dotPx}px, transparent ${dotPx}px)`; - const majorLayer = `radial-gradient(circle, ${majorColor} ${majorDotPx}px, transparent ${majorDotPx}px)`; - - 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"; - - } 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 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', finishPointer); - window.addEventListener('pointerup', finishPointer); - window.addEventListener('pointercancel', finishPointer); - - root.querySelectorAll('input[data-tool]').forEach((input) => { - input.addEventListener('change', () => { - if (input.checked) { - localStorage.setItem(`${storageKey}:tool`, input.value); - } - }); - }); - - root.querySelectorAll('input[data-gridtype]').forEach((input) => { - input.addEventListener('change', () => { - if (input.checked) { - localStorage.setItem(`${storageKey}:gridType`, input.value); - } - setGrid(); - redrawAll(); - }); - }); - - - cellSizeEl.addEventListener('input', () => applyCellSize(cellSizeEl.value)); - cellSizeEl.addEventListener('change', () => applyCellSize(cellSizeEl.value)); - - importButtonEl.addEventListener('click', () => importEl.click()); - - importEl.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = () => { - try { - const data = JSON.parse(reader.result); - - if (Number.isFinite(Number(data.cellSize)) && Number(data.cellSize) >= 1) { - cellSizeEl.value = data.cellSize; - applyCellSize(data.cellSize); - } - - const loadedShapes = Array.isArray(data) ? data : data.shapes; - if (!Array.isArray(loadedShapes)) return; - - shapes = sanitizeShapes(loadedShapes); - - doc = { - version: Number(data?.version) || 1, - cellSize: Number(data?.cellSize) || cellSize, - shapes - }; - - saveDoc(doc); - - history.length = 0; - history.push(structuredClone(shapes)); - historyIndex = 0; - - redrawAll(); - } catch { - toastMessage('Failed to load data from JSON file.', 'danger'); - } - }; - reader.readAsText(file); - }); - - exportEl.addEventListener('click', () => { - const payload = { - version: 1, - cellSize: cellSize, - shapes - }; - const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'grid-shapes.json'; - a.click(); - URL.revokeObjectURL(url); - }); - - clearEl.addEventListener('click', () => { - cellSize = 25; - cellSizeEl.value = 25; - applyCellSize(25); - commit([]); - }); - - colorEl.addEventListener('input', () => { - selectedColor = colorEl.value || '#000000'; - const circle = dotSVGEl.querySelector('circle'); - if (circle) { - circle.setAttribute('fill', selectedColor); - } - }); - - fillOpacityEl?.addEventListener('input', () => { - currentFillOpacity = clamp01(fillOpacityEl.value, 1); - }); - - fillOpacityEl?.addEventListener('change', () => { - currentFillOpacity = clamp01(fillOpacityEl.value, 1); - }); - - strokeOpacityEl?.addEventListener('input', () => { - currentStrokeOpacity = clamp01(strokeOpacityEl.value, 1); - }); - - strokeOpacityEl?.addEventListener('change', () => { - currentStrokeOpacity = clamp01(strokeOpacityEl.value, 1); - }); - - strokeWidthEl?.addEventListener('input', () => { - currentStrokeWidth = Math.max(0, Number(strokeWidthEl.value) || 0.12); - }); - - strokeWidthEl?.addEventListener('change', () => { - currentStrokeWidth = Math.max(0, Number(strokeWidthEl.value) || 0.12); - }); - - 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, 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 { - dotEl.classList.add('d-none'); - } - - 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) return; - - // 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({ - x1: currentShape.x1, - y1: currentShape.y1, - x2: snapX, - y2: snapY, - color: currentShape.color, - strokeWidth: currentShape.strokeWidth, - strokeOpacity: currentShape.strokeOpacity - }); - } 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) => { - coordsEl.classList.add('d-none'); - dotEl.classList.add('d-none'); - }); - - gridEl.addEventListener('pointerdown', (e) => { - if (e.button !== 0) return; - - if (e.target.closest('[data-toolbar]')) return; - - e.preventDefault(); - activePointerId = e.pointerId; - try { - gridEl.setPointerCapture(e.pointerId); - } catch { - // ignore: some browsers / scenarios won't allow capture - } - - const { x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY); - const tool = getActiveTool(); - - if (tool === 'line') { - currentShape = { - tool, - type: 'line', - x1: snapX, - y1: snapY, - x2: snapX, - y2: snapY, - color: selectedColor, - strokeWidth: currentStrokeWidth, - strokeOpacity: currentStrokeOpacity - }; - } else if (tool === 'outline' || tool === 'filled') { - currentShape = { - tool, - x1: snapX, - y1: snapY, - x2: snapX, - y2: snapY, - color: selectedColor, - fill: (tool === 'filled'), - fillOpacity: currentFillOpacity, - strokeOpacity: currentStrokeOpacity, - strokeWidth: currentStrokeWidth - }; - } else if (tool === 'outlineEllipse' || tool === 'filledEllipse') { - currentShape = { - tool, - x1: snapX, - y1: snapY, - x2: snapX, - y2: snapY, - color: selectedColor, - fill: (tool === 'filledEllipse'), - fillOpacity: currentFillOpacity, - strokeOpacity: currentStrokeOpacity, - strokeWidth: currentStrokeWidth - }; - } else if (tool === 'pen') { - const p = pxToDocPoint(e.clientX, e.clientY); - currentShape = { - tool, - type: 'path', - points: [p], - color: selectedColor, - width: currentStrokeWidth, - strokeOpacity: currentStrokeOpacity - }; - } - }); -} \ No newline at end of file diff --git a/inventory/templates/components/draw.html b/inventory/templates/components/draw.html deleted file mode 100644 index 8b7af44..0000000 --- a/inventory/templates/components/draw.html +++ /dev/null @@ -1,177 +0,0 @@ -{% macro drawWidget(uid) %} -