Skip to content

Commit

Permalink
lasso-selection
Browse files Browse the repository at this point in the history
  • Loading branch information
simonbethke committed Nov 6, 2024
1 parent 9407aa5 commit 61ba815
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 8 deletions.
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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));
Expand Down
179 changes: 179 additions & 0 deletions src/tools/lasso-selection.ts
Original file line number Diff line number Diff line change
@@ -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 };
18 changes: 10 additions & 8 deletions src/ui/bottom-toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);
Expand All @@ -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'));
Expand All @@ -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');
Expand All @@ -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'));
Expand Down

0 comments on commit 61ba815

Please sign in to comment.