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 cellSize = Number(doc.cellSize) || 25;
|
||||||
let viewerOffset = { x: 0, y: 0 };
|
let viewerOffset = { x: 0, y: 0 };
|
||||||
|
let selectedIndex = -1;
|
||||||
|
let selectedShape = null;
|
||||||
|
|
||||||
let ctx;
|
let ctx;
|
||||||
let dpr = 1;
|
let dpr = 1;
|
||||||
|
|
@ -617,6 +619,20 @@ function initGridWidget(root, opts = {}) {
|
||||||
ctx.translate(viewerOffset.x, viewerOffset.y);
|
ctx.translate(viewerOffset.x, viewerOffset.y);
|
||||||
}
|
}
|
||||||
shapes.forEach(drawShape);
|
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();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -734,6 +750,8 @@ function initGridWidget(root, opts = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentShape = null;
|
let currentShape = null;
|
||||||
|
let suppressNextClick = false;
|
||||||
|
|
||||||
const history = [structuredClone(shapes)];
|
const history = [structuredClone(shapes)];
|
||||||
let historyIndex = 0
|
let historyIndex = 0
|
||||||
|
|
||||||
|
|
@ -943,6 +961,101 @@ function initGridWidget(root, opts = {}) {
|
||||||
return dx * dx + dy * dy;
|
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) {
|
function pointToSegmentDist2(p, a, b) {
|
||||||
const vx = b.x - a.x, vy = b.y - a.y;
|
const vx = b.x - a.x, vy = b.y - a.y;
|
||||||
const wx = p.x - a.x, wy = p.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 (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;
|
currentShape = null;
|
||||||
renderAllWithPreview(null);
|
renderAllWithPreview(null);
|
||||||
|
|
@ -1344,6 +1463,57 @@ function initGridWidget(root, opts = {}) {
|
||||||
|
|
||||||
gridEl.addEventListener('pointerup', finishPointer);
|
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) => {
|
root.querySelectorAll('input[data-tool]').forEach((input) => {
|
||||||
input.addEventListener('change', () => {
|
input.addEventListener('change', () => {
|
||||||
if (input.checked) {
|
if (input.checked) {
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,12 @@
|
||||||
<div class="col" style="height: 80vh;">
|
<div class="col" style="height: 80vh;">
|
||||||
{{ draw.drawWidget('test1') }}
|
{{ draw.drawWidget('test1') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col" style="height: 80vh;">
|
<!-- div class="col" style="height: 80vh;">
|
||||||
I am testing a thing.
|
I am testing a thing.
|
||||||
{{ draw.viewWidget('test2', jsonImage) }}
|
{{ draw.viewWidget('test2', jsonImage) }}
|
||||||
{{ draw.viewWidget('test3', jsonImage2) }}
|
{{ draw.viewWidget('test3', jsonImage2) }}
|
||||||
The thing has been tested.
|
The thing has been tested.
|
||||||
</div>
|
</div -->
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue