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) => {