Fix to finalizing shape behavior.
This commit is contained in:
parent
b4c448d572
commit
5dfc2691e9
1 changed files with 136 additions and 88 deletions
|
|
@ -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) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue