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

View file

@ -143,6 +143,11 @@ function initGridWidget(root, opts = {}) {
activeGridWidget = api;
}, { 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) {
const sync = () => { labelEl.textContent = format(inputEl.value); };
inputEl.addEventListener('input', sync);
@ -187,7 +192,62 @@ function initGridWidget(root, opts = {}) {
function dist2(a, b) {
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() {
@ -315,18 +375,29 @@ function initGridWidget(root, opts = {}) {
const snappedW = snapDown(w, 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 };
// critical: don't let observer see our own updates as layout input
ro.disconnect();
gridEl.style.width = `${snappedW}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();
}
function scheduleSnappedCellSize() {
if (sizingRAF) return;
sizingRAF = requestAnimationFrame(applySnappedCellSize);
@ -536,6 +607,8 @@ function initGridWidget(root, opts = {}) {
const y2 = toPx(shape.y2);
ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
@ -633,17 +706,24 @@ function initGridWidget(root, opts = {}) {
const pts = currentShape.points;
if (pts.length >= 2) {
const simplified = [pts[0]];
const minStep = 0.03;
const coarse = [pts[0]];
const minStep = 0.02;
for (let i = 1; i < pts.length; i++) {
if (dist2(pts[i], simplified[simplified.length - 1]) >= minStep * minStep) {
simplified.push(pts[i]);
if (dist2(pts[i], coarse[coarse.length - 1]) >= minStep * minStep) {
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) {
finalShape = { ...currentShape, points: simplified };
}
}
}
} else if (currentShape.tool === 'line') {
const line = normalizeLine(currentShape);
if (line.x1 !== line.x2 || line.y1 !== line.y2) finalShape = line;
@ -797,24 +877,32 @@ function initGridWidget(root, opts = {}) {
gridEl.addEventListener('pointermove', (e) => {
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 tool = getActiveTool();
if (!drawing && !inside) {
coordsEl.classList.add('d-none');
dotEl.classList.add('d-none');
} else {
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;
const offsetX = rect.left - wrapRect.left;
const offsetY = rect.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)})`;

View file

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