Line simplification!!!

This commit is contained in:
Yaro Kasear 2026-01-06 10:01:25 -06:00
parent a1cf260072
commit 429e993009
3 changed files with 141 additions and 45 deletions

View file

@ -13,7 +13,6 @@
.grid-widget .grid-wrap { .grid-widget .grid-wrap {
flex: 1 1 auto; flex: 1 1 auto;
width: 100%; width: 100%;
height: 100%;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
min-height: 375px; min-height: 375px;
@ -47,7 +46,7 @@
} }
/* WIDE: 1 row */ /* WIDE: 1 row */
@container (min-width: 725px){ @container (min-width: 750px) {
.grid-widget [data-toolbar].toolbar { .grid-widget [data-toolbar].toolbar {
display: flex !important; display: flex !important;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -68,7 +67,7 @@
} }
.grid-widget [data-grid] { .grid-widget [data-grid] {
position: relative; position: absolute;
cursor: crosshair; cursor: crosshair;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -76,6 +75,13 @@
margin: 0 auto; margin: 0 auto;
max-width: 100%; max-width: 100%;
z-index: 0; 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 { .grid-widget [data-toolbar]::-webkit-scrollbar {
@ -97,6 +103,11 @@
z-index: 1; z-index: 1;
pointer-events: none; pointer-events: none;
inset: 0; inset: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
} }
.grid-widget [data-dot] { .grid-widget [data-dot] {

View file

@ -143,6 +143,11 @@ function initGridWidget(root, opts = {}) {
activeGridWidget = api; activeGridWidget = api;
}, { capture: true }); }, { 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) { function bindRangeWithLabel(inputEl, labelEl, format = (v) => v) {
const sync = () => { labelEl.textContent = format(inputEl.value); }; const sync = () => { labelEl.textContent = format(inputEl.value); };
inputEl.addEventListener('input', sync); inputEl.addEventListener('input', sync);
@ -187,7 +192,62 @@ function initGridWidget(root, opts = {}) {
function dist2(a, b) { function dist2(a, b) {
const dx = a.x - b.x, dy = a.y - b.y; 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() { function undo() {
@ -315,18 +375,29 @@ function initGridWidget(root, opts = {}) {
const snappedW = snapDown(w, grid); const snappedW = snapDown(w, grid);
const snappedH = snapDown(h, 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 }; 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.width = `${snappedW}px`;
gridEl.style.height = `${snappedH}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(); resizeAndSetupCanvas();
} }
function scheduleSnappedCellSize() { function scheduleSnappedCellSize() {
if (sizingRAF) return; if (sizingRAF) return;
sizingRAF = requestAnimationFrame(applySnappedCellSize); sizingRAF = requestAnimationFrame(applySnappedCellSize);
@ -536,6 +607,8 @@ function initGridWidget(root, opts = {}) {
const y2 = toPx(shape.y2); const y2 = toPx(shape.y2);
ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity); ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x1, y1); ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2); ctx.lineTo(x2, y2);
@ -633,17 +706,24 @@ function initGridWidget(root, opts = {}) {
const pts = currentShape.points; const pts = currentShape.points;
if (pts.length >= 2) { if (pts.length >= 2) {
const simplified = [pts[0]]; const coarse = [pts[0]];
const minStep = 0.03; const minStep = 0.02;
for (let i = 1; i < pts.length; i++) { for (let i = 1; i < pts.length; i++) {
if (dist2(pts[i], simplified[simplified.length - 1]) >= minStep * minStep) { if (dist2(pts[i], coarse[coarse.length - 1]) >= minStep * minStep) {
simplified.push(pts[i]); coarse.push(pts[i]);
} }
} }
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) { if (simplified.length >= 2) {
finalShape = { ...currentShape, points: simplified }; finalShape = { ...currentShape, points: simplified };
} }
} }
}
} else if (currentShape.tool === 'line') { } else if (currentShape.tool === 'line') {
const line = normalizeLine(currentShape); const line = normalizeLine(currentShape);
if (line.x1 !== line.x2 || line.y1 !== line.y2) finalShape = line; if (line.x1 !== line.x2 || line.y1 !== line.y2) finalShape = line;
@ -797,24 +877,32 @@ function initGridWidget(root, opts = {}) {
gridEl.addEventListener('pointermove', (e) => { gridEl.addEventListener('pointermove', (e) => {
if (!ctx) return; 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 { ix, iy, x: snapX, y: snapY, localX, localY } = snapToGrid(e.clientX, e.clientY);
const tool = getActiveTool(); const tool = getActiveTool();
if (!drawing && !inside) {
coordsEl.classList.add('d-none');
dotEl.classList.add('d-none');
} else {
coordsEl.classList.remove('d-none'); coordsEl.classList.remove('d-none');
if (getActiveType() !== 'noGrid' && tool !== 'pen') { if (getActiveType() !== 'noGrid' && tool !== 'pen') {
dotEl.classList.remove('d-none'); dotEl.classList.remove('d-none');
const gridRect = gridEl.getBoundingClientRect();
const wrapRect = gridWrapEl.getBoundingClientRect(); const wrapRect = gridWrapEl.getBoundingClientRect();
const offsetX = gridRect.left - wrapRect.left; const offsetX = rect.left - wrapRect.left;
const offsetY = gridRect.top - wrapRect.top; const offsetY = rect.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 { } else {
dotEl.classList.add('d-none'); dotEl.classList.add('d-none');
} }
}
if (getActiveType() == 'noGrid') { if (getActiveType() == 'noGrid') {
coordsEl.innerText = `(px x=${Math.round(localX)} y=${Math.round(localY)})`; coordsEl.innerText = `(px x=${Math.round(localX)} y=${Math.round(localY)})`;

View file

@ -8,12 +8,9 @@
{% block main %} {% block main %}
<div class="row"> <div class="row">
<div class="col"> <div class="col" style="min-height: 80vh">
{{ draw.drawWidget('test1') }} {{ draw.drawWidget('test1') }}
</div> </div>
<div class="col">
{{ draw.drawWidget('test2') }}
</div>
</div> </div>
{% endblock %} {% endblock %}