Line simplification!!!
This commit is contained in:
parent
a1cf260072
commit
429e993009
3 changed files with 141 additions and 45 deletions
|
|
@ -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] {
|
||||||
|
|
|
||||||
|
|
@ -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)})`;
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue