Selection!

This commit is contained in:
Yaro Kasear 2026-01-12 10:59:26 -06:00
parent 4e2cd2b0e5
commit 5a2f480ef7
2 changed files with 174 additions and 4 deletions

View file

@ -47,6 +47,8 @@ function initGridWidget(root, opts = {}) {
);
let cellSize = Number(doc.cellSize) || 25;
let viewerOffset = { x: 0, y: 0 };
let selectedIndex = -1;
let selectedShape = null;
let ctx;
let dpr = 1;
@ -617,6 +619,20 @@ function initGridWidget(root, opts = {}) {
ctx.translate(viewerOffset.x, viewerOffset.y);
}
shapes.forEach(drawShape);
if (selectedShape) {
ctx.save();
ctx.globalAlpha = 1;
ctx.setLineDash([6, 4]);
drawShape({
...selectedShape,
fill: false,
strokeWidth: Math.max(selectedShape.strokeWidth ?? 0.12, 0.12) + (2 / cellSize),
strokeOpacity: 1
});
ctx.restore();
}
ctx.restore();
}
@ -734,6 +750,8 @@ function initGridWidget(root, opts = {}) {
}
let currentShape = null;
let suppressNextClick = false;
const history = [structuredClone(shapes)];
let historyIndex = 0
@ -943,6 +961,101 @@ function initGridWidget(root, opts = {}) {
return dx * dx + dy * dy;
}
function pickShapeAt(docPt, shapes, cellSize, opts = {}) {
const pxTol = opts.pxTol ?? 6;
const tol = pxTol / cellSize;
const tol2 = tol * tol;
for (let i = shapes.length - 1; i >= 0; i--) {
const s = shapes[i];
if (!s) continue;
if (hitShape(docPt, s, tol, tol2)) {
return { index: i, shape: s };
}
}
return null;
}
function hitShape(p, s, tol, tol2) {
if (s.type === 'line') {
const a = { x: s.x1, y: s.y1 };
const b = { x: s.x2, y: s.y2 };
const sw = Math.max(0, Number(s.strokeWidth) || 0) / 2;
const t = tol + sw;
return pointToSegmentDist2(p, a, b) <= (t * t);
}
if (s.type === 'path') {
const pts = (s.renderPoints?.length >= 2) ? s.renderPoints : s.points;
if (!pts || pts.length < 2) return false;
const sw = Math.max(0, Number(s.strokeWidth) || 0) / 2;
const t = tol + sw;
for (let i = 0; i < pts.length - 1; i++) {
if (pointToSegmentDist2(p, pts[i], pts[i + 1]) <= (t * t)) return true;
}
}
if (s.type === 'rect') {
return hitRect(p, s, tol);
}
if (s.type === 'ellipse') {
return hitEllipse(p, s, tol);
}
return false;
}
function hitRect(p, r, tol) {
const x1 = r.x, y1 = r.y, x2 = r.x + r.w, y2 = r.y + r.h;
const minX = Math.min(x1, x2), maxX = Math.max(x1, x2);
const minY = Math.min(y1, y2), maxY = Math.max(y1, y2);
const inside = (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY);
if (r.fill) {
return (p.x >= minX - tol && p.x <= maxX + tol && p.y >= minY - tol && p.y <= maxY + tol);
}
if (!inside) {
if (p.x < minX - tol || p.x > maxX + tol || p.y < minY - tol || p.y > maxY + tol) return false;
}
const nearLeft = Math.abs(p.x - minX) <= tol && p.y >= minY - tol && p.y <= maxY + tol;
const nearRight = Math.abs(p.x - maxX) <= tol && p.y >= minY - tol && p.y <= maxY + tol;
const nearTop = Math.abs(p.y - minY) <= tol && p.x >= minX - tol && p.x <= maxX + tol;
const nearBottom = Math.abs(p.y - minX) <= tol && p.x >= minX - tol && p.x <= maxX + tol;
return nearLeft || nearRight || nearTop || nearBottom;
}
function hitEllipse(p, e, tol) {
const cx = e.x + e.w / 2;
const cy = e.y + e.h / 2;
const rx = Math.abs(e.w / 2);
const ry = Math.abs(e.h / 2);
if (rx <= 0 || ry <= 0) return false;
const nx = (p.x - cx) / rx;
const ny = (p.y - cy) / ry;
const d = nx * nx + ny * ny;
if (e.fill) {
const rx2 = (rx + tol);
const ry2 = (ry + tol);
const nnx = (p.x - cx) / rx2;
const nny = (p.y - cy) / ry2;
return (nnx * nnx + nny * nny) <= 1;
}
const minR = Math.max(1e-6, Math.min(rx, ry));
const band = tol / minR;
return Math.abs(d - 1) <= Math.max(0.02, band);
}
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;
@ -1336,7 +1449,13 @@ function initGridWidget(root, opts = {}) {
if (ellipse.w > 0 && ellipse.h > 0) finalShape = ellipse;
}
if (finalShape) commit([...shapes, finalShape]);
if (finalShape) {
commit([...shapes, finalShape]);
suppressNextClick = true;
setTimeout(() => { suppressNextClick = false; }, 0);
}
currentShape = null;
renderAllWithPreview(null);
@ -1344,6 +1463,57 @@ function initGridWidget(root, opts = {}) {
gridEl.addEventListener('pointerup', finishPointer);
function setSelection(hit) {
if (!hit) {
selectedIndex = -1;
selectedShape = null;
redrawAll();
return;
}
selectedIndex = hit.index;
selectedShape = hit.shape;
redrawAll();
}
gridEl.addEventListener('click', (e) => {
if (suppressNextClick) {
suppressNextClick = false;
return;
}
if (currentShape) return;
if (e.target.closest('[data-toolbar]')) return;
const docPt = pxToDocPoint(e.clientX, e.clientY);
const hit = pickShapeAt(docPt, shapes, cellSize, { pxTol: 7 });
setSelection(hit);
if (hit) root.dispatchEvent(new CustomEvent('shape:click', { detail: hit }));
});
gridEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
if (currentShape) return;
const docPt = pxToDocPoint(e.clientX, e.clientY);
const hit = pickShapeAt(docPt, shapes, cellSize, { pxTol: 7 });
setSelection(hit);
root.dispatchEvent(new CustomEvent('shape:contextmenu', {
detail: { hit, clientX: e.clientX, clientY: e.clientY }
}));
});
gridEl.addEventListener('dblclick', (e) => {
if (currentShape) return;
if (e.target.closest('[data-toolbar]')) return;
const docPt = pxToDocPoint(e.clientX, e.clientY);
const hit = pickShapeAt(docPt, shapes, cellSize, { pxTol: 7 });
setSelection(hit);
if (hit) root.dispatchEvent(new CustomEvent('shape:dblclick', { detail: hit }));
});
root.querySelectorAll('input[data-tool]').forEach((input) => {
input.addEventListener('change', () => {
if (input.checked) {

View file

@ -17,12 +17,12 @@
<div class="col" style="height: 80vh;">
{{ draw.drawWidget('test1') }}
</div>
<div class="col" style="height: 80vh;">
<!-- div class="col" style="height: 80vh;">
I am testing a thing.
{{ draw.viewWidget('test2', jsonImage) }}
{{ draw.viewWidget('test3', jsonImage2) }}
The thing has been tested.
</div>
</div -->
</div>
{% endblock %}