Selection!
This commit is contained in:
parent
4e2cd2b0e5
commit
5a2f480ef7
2 changed files with 174 additions and 4 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue