Fix to finalizing shape behavior.

This commit is contained in:
Conrad Nelson 2025-12-17 12:29:58 -06:00
parent b4c448d572
commit 5dfc2691e9

View file

@ -256,6 +256,26 @@ resizeAndSetupCanvas();
setGrid(); setGrid();
scheduleSnappedCellSize(); 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) { function pxToDocPoint(clientX, clientY) {
const rect = gridEl.getBoundingClientRect(); const rect = gridEl.getBoundingClientRect();
const x = Math.min(Math.max(clientX, rect.left), rect.right) - rect.left; const x = Math.min(Math.max(clientX, rect.left), rect.right) - rect.left;
@ -618,44 +638,103 @@ function clearCanvas() {
} }
function setGrid() { function setGrid() {
const type = getActiveType(); const type = getActiveType();
gridEl.style.backgroundImage = ""; gridEl.style.backgroundImage = "";
gridEl.style.backgroundSize = ""; gridEl.style.backgroundSize = "";
gridEl.style.boxShadow = "none"; gridEl.style.backgroundPosition = "";
dotEl.classList.add('d-none'); gridEl.style.boxShadow = "none";
dotEl.classList.add('d-none');
if (type === 'fullGrid') { // Minor dots
gridEl.style.backgroundImage = const dotPx = Math.max(1, Math.round(cellSize * 0.08));
"linear-gradient(to right, #ccc 1px, transparent 1px)," + const minorColor = '#ddd';
"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
} else if (type === 'horizontalGrid') { // Major dots (every 5 cells)
gridEl.style.backgroundImage = const majorStep = cellSize * 5;
"linear-gradient(to bottom, #ccc 1px, transparent 1px)"; const majorDotPx = Math.max(dotPx + 1, Math.round(cellSize * 0.12));
gridEl.style.backgroundSize = `100% ${cellSize}px`; const majorColor = '#c4c4c4';
// left + right borders only const minorLayer = `radial-gradient(circle, ${minorColor} ${dotPx}px, transparent ${dotPx}px)`;
gridEl.style.boxShadow = const majorLayer = `radial-gradient(circle, ${majorColor} ${majorDotPx}px, transparent ${majorDotPx}px)`;
"inset 1px 0 0 0 #ccc, inset -1px 0 0 0 #ccc";
} else if (type === 'verticalGrid') { if (type === 'fullGrid') {
gridEl.style.backgroundImage = gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`;
"linear-gradient(to right, #ccc 1px, transparent 1px)"; gridEl.style.backgroundSize = `${majorStep}px ${majorStep}px, ${cellSize}px ${cellSize}px`;
gridEl.style.backgroundSize = `${cellSize}px 100%`; 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 } else if (type === 'verticalGrid') {
gridEl.style.boxShadow = gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`;
"inset 0 1px 0 0 #ccc, inset 0 -1px 0 0 #ccc"; 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 } else if (type === 'horizontalGrid') {
gridEl.style.boxShadow = "inset 0 0 0 1px #ccc"; gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`;
dotEl.classList.add('d-none'); 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 => { document.querySelectorAll('input[name="tool"]').forEach(input => {
input.addEventListener('change', () => { input.addEventListener('change', () => {
@ -793,12 +872,13 @@ gridEl.addEventListener('pointermove', (e) => {
const gridRect = gridEl.getBoundingClientRect(); const gridRect = gridEl.getBoundingClientRect();
const wrapRect = gridWrapEl.getBoundingClientRect(); const wrapRect = gridWrapEl.getBoundingClientRect();
const offsetX = gridRect.left - wrapRect.left; const offsetX = gridRect.left - wrapRect.left;
const offsetY = gridRect.top - wrapRect.top; const offsetY = gridRect.top - wrapRect.top;
dotEl.style.left = `${offsetX + snapX}px`; dotEl.style.left = `${offsetX + snapX}px`;
dotEl.style.top = `${offsetY + snapY}px`; dotEl.style.top = `${offsetY + snapY}px`;
} else {
dotEl.classList.add('d-none');
} }
if (getActiveType() == 'noGrid') { if (getActiveType() == 'noGrid') {
@ -807,66 +887,34 @@ gridEl.addEventListener('pointermove', (e) => {
coordsEl.innerText = `(x=${ix} (${snapX}px) y=${iy} (${snapY}px))`; coordsEl.innerText = `(x=${ix} (${snapX}px) y=${iy} (${snapY}px))`;
} }
if (currentShape) { if (!currentShape) return;
const tool = currentShape.tool;
clearCanvas(); // PEN: mutate points and preview the same shape object
shapes.forEach(drawShape); if (currentShape.tool === 'pen') {
penAddPoint(currentShape, e.clientX, e.clientY, 0.02);
ctx.save(); renderAllWithPreview(currentShape, true);
ctx.setLineDash([5, 3]); return;
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();
} }
// 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) => { gridEl.addEventListener('pointerleave', (e) => {