From 61ba815fb6eedc444d90eaefedcb5dc77f94ec7e Mon Sep 17 00:00:00 2001 From: simonbethke Date: Wed, 6 Nov 2024 23:36:00 +0100 Subject: [PATCH 1/3] lasso-selection --- src/main.ts | 3 + src/tools/lasso-selection.ts | 179 +++++++++++++++++++++++++++++++++++ src/ui/bottom-toolbar.ts | 18 ++-- 3 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 src/tools/lasso-selection.ts diff --git a/src/main.ts b/src/main.ts index c57c91e0..8c20ffb2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,6 +18,7 @@ import { RotateTool } from './tools/rotate-tool'; import { ScaleTool } from './tools/scale-tool'; import { Shortcuts } from './shortcuts'; import { Events } from './events'; +import { LassoSelection } from './tools/lasso-selection'; declare global { interface LaunchParams { @@ -72,6 +73,7 @@ const initShortcuts = (events: Events) => { shortcuts.register(['F', 'f'], { event: 'camera.focus' }); shortcuts.register(['R', 'r'], { event: 'tool.rectSelection', sticky: true }); shortcuts.register(['P', 'p'], { event: 'tool.polygonSelection', sticky: true }); + shortcuts.register(['L', 'l'], { event: 'tool.lassoSelection', sticky: true }); shortcuts.register(['B', 'b'], { event: 'tool.brushSelection', sticky: true }); shortcuts.register(['A', 'a'], { event: 'select.all' }); shortcuts.register(['A', 'a'], { event: 'select.none', shift: true }); @@ -189,6 +191,7 @@ const main = async () => { toolManager.register('rectSelection', new RectSelection(events, editorUI.toolsContainer.dom)); toolManager.register('brushSelection', new BrushSelection(events, editorUI.toolsContainer.dom, mask)); toolManager.register('polygonSelection', new PolygonSelection(events, editorUI.toolsContainer.dom, mask)); + toolManager.register('lassoSelection', new LassoSelection(events, editorUI.toolsContainer.dom, mask)); toolManager.register('sphereSelection', new SphereSelection(events, scene, editorUI.canvasContainer)); toolManager.register('move', new MoveTool(events, scene)); toolManager.register('rotate', new RotateTool(events, scene)); diff --git a/src/tools/lasso-selection.ts b/src/tools/lasso-selection.ts new file mode 100644 index 00000000..0581738f --- /dev/null +++ b/src/tools/lasso-selection.ts @@ -0,0 +1,179 @@ +import { Events } from "../events"; + +type Point = { x: number, y: number }; + +class LassoSelection { + activate: () => void; + deactivate: () => void; + + constructor(events: Events, parent: HTMLElement, mask: { canvas: HTMLCanvasElement, context: CanvasRenderingContext2D }) { + let points: Point[] = []; + let currentPoint: Point = null; + let lastPointTime = 0; + + // create svg + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.id = 'lasso-select-svg'; + svg.classList.add('select-svg'); + + // create polyline element + const polyline = document.createElementNS(svg.namespaceURI, 'polyline') as SVGPolylineElement; + polyline.setAttribute('fill', 'none'); + polyline.setAttribute('stroke-width', '1'); + polyline.setAttribute('stroke-dasharray', '5, 5'); + polyline.setAttribute('stroke-dashoffset', '0'); + + // create canvas + const { canvas, context } = mask; + + const paint = () => { + polyline.setAttribute('points', [...points, currentPoint].reduce((prev, current) => prev + `${current.x}, ${current.y} `, "")); + polyline.setAttribute('stroke', isClosed() ? '#fa6' : '#f60'); + }; + + const dist = (a: Point, b: Point) => { + return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2); + }; + + const isClosed = () => { + return points.length > 1 && dist(currentPoint, points[0]) < 8; + }; + + let dragId: number | undefined; + + const update = (e: PointerEvent) => { + currentPoint = {x: e.offsetX, y: e.offsetY}; + + const distance = points.length === 0 ? 0 : dist(currentPoint, points[points.length - 1]); + const millis = Date.now() - lastPointTime; + const preventCorners = distance > 20; + const slowNarrowSpacing = millis > 500 && distance > 2; + const fasterMediumSpacing = millis > 200 && distance > 10; + const firstPoints = points.length === 0; + + + if (dragId !== undefined && (preventCorners || slowNarrowSpacing || fasterMediumSpacing || firstPoints)) { + points.push(currentPoint); + lastPointTime = Date.now(); + paint(); + } + }; + + const pointerdown = (e: PointerEvent) => { + if (dragId === undefined && (e.pointerType === 'mouse' ? e.button === 0 : e.isPrimary)) { + e.preventDefault(); + e.stopPropagation(); + + dragId = e.pointerId; + parent.setPointerCapture(dragId); + + // initialize canvas + if (canvas.width !== parent.clientWidth || canvas.height !== parent.clientHeight) { + canvas.width = parent.clientWidth; + canvas.height = parent.clientHeight; + } + + // clear canvas + context.clearRect(0, 0, canvas.width, canvas.height); + + // display it + canvas.style.display = 'inline'; + + update(e); + } + }; + + const pointermove = (e: PointerEvent) => { + if (dragId !== undefined) { + e.preventDefault(); + e.stopPropagation(); + } + + update(e); + }; + + const dragEnd = () => { + parent.releasePointerCapture(dragId); + dragId = undefined; + canvas.style.display = 'none'; + }; + + const pointerup = (e: PointerEvent) => { + if (e.pointerId === dragId) { + e.preventDefault(); + e.stopPropagation(); + + dragEnd(); + + commitSelection(e); + + events.fire( + 'select.byMask', + e.shiftKey ? 'add' : (e.ctrlKey ? 'remove' : 'set'), + canvas, + context + ); + } + }; + + const commitSelection = (e: PointerEvent) => { + // initialize canvas + if (canvas.width !== parent.clientWidth || canvas.height !== parent.clientHeight) { + canvas.width = parent.clientWidth; + canvas.height = parent.clientHeight; + } + + // clear canvas + context.clearRect(0, 0, canvas.width, canvas.height); + + context.beginPath(); + context.fillStyle = '#f60'; + context.beginPath(); + points.forEach((p, idx) => { + if (idx === 0) { + context.moveTo(p.x, p.y); + } + else { + context.lineTo(p.x, p.y); + } + }); + context.closePath(); + context.fill(); + + events.fire( + 'select.byMask', + e.shiftKey ? 'add' : (e.ctrlKey ? 'remove' : 'set'), + canvas, + context + ); + + points = []; + paint(); + }; + + this.activate = () => { + svg.style.display = 'inline'; + parent.style.display = 'block'; + parent.addEventListener('pointerdown', pointerdown); + parent.addEventListener('pointermove', pointermove); + parent.addEventListener('pointerup', pointerup); + }; + + this.deactivate = () => { + // cancel active operation + if (dragId !== undefined) { + dragEnd(); + } + svg.style.display = 'none'; + parent.style.display = 'none'; + parent.removeEventListener('pointerdown', pointerdown); + parent.removeEventListener('pointermove', pointermove); + parent.removeEventListener('pointerup', pointerup); + }; + + svg.appendChild(polyline); + parent.appendChild(svg); + } +} + +export { LassoSelection }; diff --git a/src/ui/bottom-toolbar.ts b/src/ui/bottom-toolbar.ts index 70b2dd1a..c6ceddf6 100644 --- a/src/ui/bottom-toolbar.ts +++ b/src/ui/bottom-toolbar.ts @@ -9,7 +9,7 @@ import pickerSvg from './svg/select-picker.svg'; import brushSvg from './svg/select-brush.svg'; import polygonSvg from './svg/select-poly.svg'; import sphereSvg from './svg/select-sphere.svg'; -// import lassoSvg from './svg/select-lasso.svg'; +import lassoSvg from './svg/select-lasso.svg'; // import cropSvg from './svg/crop.svg'; const createSvg = (svgString: string) => { @@ -57,10 +57,10 @@ class BottomToolbar extends Container { class: 'bottom-toolbar-tool' }); - // const lasso = new Button({ - // id: 'bottom-toolbar-lasso', - // class: ['bottom-toolbar-tool', 'disabled'] - // }); + const lasso = new Button({ + id: 'bottom-toolbar-lasso', + class: 'bottom-toolbar-tool' + }); const sphere = new Button({ id: 'bottom-toolbar-sphere', @@ -102,16 +102,16 @@ class BottomToolbar extends Container { polygon.dom.appendChild(createSvg(polygonSvg)); brush.dom.appendChild(createSvg(brushSvg)); sphere.dom.appendChild(createSvg(sphereSvg)); - // lasso.dom.appendChild(createSvg(lassoSvg)); + lasso.dom.appendChild(createSvg(lassoSvg)); // crop.dom.appendChild(createSvg(cropSvg)); this.append(undo); this.append(redo); this.append(new Element({ class: 'bottom-toolbar-separator' })); this.append(picker); + this.append(lasso); this.append(polygon); this.append(brush); - // this.append(lasso); this.append(new Element({ class: 'bottom-toolbar-separator' })); this.append(sphere); // this.append(crop); @@ -124,6 +124,7 @@ class BottomToolbar extends Container { undo.dom.addEventListener('click', () => events.fire('edit.undo')); redo.dom.addEventListener('click', () => events.fire('edit.redo')); polygon.dom.addEventListener('click', () => events.fire('tool.polygonSelection')); + lasso.dom.addEventListener('click', () => events.fire('tool.lassoSelection')); brush.dom.addEventListener('click', () => events.fire('tool.brushSelection')); picker.dom.addEventListener('click', () => events.fire('tool.rectSelection')); sphere.dom.addEventListener('click', () => events.fire('tool.sphereSelection')); @@ -139,6 +140,7 @@ class BottomToolbar extends Container { picker.class[toolName === 'rectSelection' ? 'add' : 'remove']('active'); brush.class[toolName === 'brushSelection' ? 'add' : 'remove']('active'); polygon.class[toolName === 'polygonSelection' ? 'add' : 'remove']('active'); + lasso.class[toolName === 'lassoSelection' ? 'add' : 'remove']('active'); sphere.class[toolName === 'sphereSelection' ? 'add' : 'remove']('active'); translate.class[toolName === 'move' ? 'add' : 'remove']('active'); rotate.class[toolName === 'rotate' ? 'add' : 'remove']('active'); @@ -155,7 +157,7 @@ class BottomToolbar extends Container { tooltips.register(picker, localize('tooltip.picker')); tooltips.register(brush, localize('tooltip.brush')); tooltips.register(polygon, localize('tooltip.polygon')); - // tooltips.register(lasso, 'Lasso Select'); + tooltips.register(lasso, 'Lasso Select'); tooltips.register(sphere, localize('tooltip.sphere')); // tooltips.register(crop, 'Crop'); tooltips.register(translate, localize('tooltip.translate')); From 8b31a17dca71d076139a9093ddf9c8dbd2e435cc Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Fri, 8 Nov 2024 15:36:32 +0000 Subject: [PATCH 2/3] Update lasso-selection.ts --- src/tools/lasso-selection.ts | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/tools/lasso-selection.ts b/src/tools/lasso-selection.ts index 0581738f..8a8c6e45 100644 --- a/src/tools/lasso-selection.ts +++ b/src/tools/lasso-selection.ts @@ -67,15 +67,6 @@ class LassoSelection { dragId = e.pointerId; parent.setPointerCapture(dragId); - // initialize canvas - if (canvas.width !== parent.clientWidth || canvas.height !== parent.clientHeight) { - canvas.width = parent.clientWidth; - canvas.height = parent.clientHeight; - } - - // clear canvas - context.clearRect(0, 0, canvas.width, canvas.height); - // display it canvas.style.display = 'inline'; @@ -107,12 +98,8 @@ class LassoSelection { commitSelection(e); - events.fire( - 'select.byMask', - e.shiftKey ? 'add' : (e.ctrlKey ? 'remove' : 'set'), - canvas, - context - ); + points = []; + paint(); } }; @@ -146,9 +133,6 @@ class LassoSelection { canvas, context ); - - points = []; - paint(); }; this.activate = () => { From 0a653f46da9f909a6e9066874e6f1cea20b7f836 Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Fri, 8 Nov 2024 15:42:41 +0000 Subject: [PATCH 3/3] Update lasso-selection.ts Remove unnecessary showing mask, use polygon instead of polyline. --- src/tools/lasso-selection.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/tools/lasso-selection.ts b/src/tools/lasso-selection.ts index 8a8c6e45..3a45554f 100644 --- a/src/tools/lasso-selection.ts +++ b/src/tools/lasso-selection.ts @@ -16,19 +16,19 @@ class LassoSelection { svg.id = 'lasso-select-svg'; svg.classList.add('select-svg'); - // create polyline element - const polyline = document.createElementNS(svg.namespaceURI, 'polyline') as SVGPolylineElement; - polyline.setAttribute('fill', 'none'); - polyline.setAttribute('stroke-width', '1'); - polyline.setAttribute('stroke-dasharray', '5, 5'); - polyline.setAttribute('stroke-dashoffset', '0'); + // create polygon element + const polygon = document.createElementNS(svg.namespaceURI, 'polygon') as SVGPolygonElement; + polygon.setAttribute('fill', 'none'); + polygon.setAttribute('stroke-width', '1'); + polygon.setAttribute('stroke-dasharray', '5, 5'); + polygon.setAttribute('stroke-dashoffset', '0'); // create canvas const { canvas, context } = mask; const paint = () => { - polyline.setAttribute('points', [...points, currentPoint].reduce((prev, current) => prev + `${current.x}, ${current.y} `, "")); - polyline.setAttribute('stroke', isClosed() ? '#fa6' : '#f60'); + polygon.setAttribute('points', [...points, currentPoint].reduce((prev, current) => prev + `${current.x}, ${current.y} `, "")); + polygon.setAttribute('stroke', isClosed() ? '#fa6' : '#f60'); }; const dist = (a: Point, b: Point) => { @@ -51,7 +51,6 @@ class LassoSelection { const fasterMediumSpacing = millis > 200 && distance > 10; const firstPoints = points.length === 0; - if (dragId !== undefined && (preventCorners || slowNarrowSpacing || fasterMediumSpacing || firstPoints)) { points.push(currentPoint); lastPointTime = Date.now(); @@ -67,9 +66,6 @@ class LassoSelection { dragId = e.pointerId; parent.setPointerCapture(dragId); - // display it - canvas.style.display = 'inline'; - update(e); } }; @@ -86,7 +82,6 @@ class LassoSelection { const dragEnd = () => { parent.releasePointerCapture(dragId); dragId = undefined; - canvas.style.display = 'none'; }; const pointerup = (e: PointerEvent) => { @@ -155,7 +150,7 @@ class LassoSelection { parent.removeEventListener('pointerup', pointerup); }; - svg.appendChild(polyline); + svg.appendChild(polygon); parent.appendChild(svg); } }