mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: lasso selection (#9169)
* lasso without 'real' shape detection * select a single linear el * improve ux * feed segments to worker * simplify path threshold adaptive to zoom * add a tiny threshold for checks * refactor code * lasso tests * fix: ts * do not capture lasso tool * try worker-loader in next config * update config * refactor * lint * feat: show active tool when using "more tools" * keep lasso if selected from toolbar * fix incorrect checks for resetting to selection * shift for additive selection * bound text related fixes * lint * keep alt toggled lasso selection if shift pressed * fix regression * fix 'dead' lassos * lint * use workerpool and polyfill * fix worker bundled with window related code * refactor * add file extension for worker constructor error * another attempt at constructor error * attempt at build issue * attempt with dynamic import * test not importing from math * narrow down imports * Reusing existing workers infrastructure (fallback to the main thread, type-safety) * Points on curve inside the shared chunk * Give up on experimental code splitting * Remove potentially unnecessary optimisation * Removing workers as the complexit is much worse, while perf. does not seem to be much better * fix selecting text containers and containing frames together * render fill directly from animated trail * do not re-render static when setting selected element ids in lasso * remove unnecessary property * tweak trail animation * slice points to remove notch * always start alt-lasso from initial point * revert build & worker changes (unused) * remove `lasso` from `hasStrokeColor` * label change * remove unused props * remove unsafe optimization * snaps --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> Co-authored-by: Marcel Mraz <marcel@excalidraw.com>
This commit is contained in:
parent
6e47fadb59
commit
ce267aa0d3
33 changed files with 2709 additions and 146 deletions
|
@ -461,6 +461,8 @@ import { isOverScrollBars } from "../scene/scrollbars";
|
|||
|
||||
import { isMaybeMermaidDefinition } from "../mermaid";
|
||||
|
||||
import { LassoTrail } from "../lasso";
|
||||
|
||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
||||
|
@ -692,6 +694,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
? "rgba(0, 0, 0, 0.2)"
|
||||
: "rgba(255, 255, 255, 0.2)",
|
||||
});
|
||||
lassoTrail = new LassoTrail(this.animationFrameHandler, this);
|
||||
|
||||
onChangeEmitter = new Emitter<
|
||||
[
|
||||
|
@ -1670,7 +1673,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
<div className="excalidraw-contextMenuContainer" />
|
||||
<div className="excalidraw-eye-dropper-container" />
|
||||
<SVGLayer
|
||||
trails={[this.laserTrails, this.eraserTrail]}
|
||||
trails={[
|
||||
this.laserTrails,
|
||||
this.eraserTrail,
|
||||
this.lassoTrail,
|
||||
]}
|
||||
/>
|
||||
{selectedElements.length === 1 &&
|
||||
this.state.openDialog?.name !==
|
||||
|
@ -4630,7 +4637,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state.openDialog?.name === "elementLinkSelector"
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||
} else if (this.state.activeTool.type === "selection") {
|
||||
} else if (
|
||||
this.state.activeTool.type === "selection" ||
|
||||
this.state.activeTool.type === "lasso"
|
||||
) {
|
||||
resetCursor(this.interactiveCanvas);
|
||||
} else {
|
||||
setCursorForShape(this.interactiveCanvas, this.state);
|
||||
|
@ -4738,7 +4748,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
)
|
||||
| { type: "custom"; customType: string }
|
||||
) & { locked?: boolean },
|
||||
) & { locked?: boolean; fromSelection?: boolean },
|
||||
keepSelection = false,
|
||||
) => {
|
||||
if (!this.isToolSupported(tool.type)) {
|
||||
console.warn(
|
||||
|
@ -4780,7 +4791,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.store.shouldCaptureIncrement();
|
||||
}
|
||||
|
||||
if (nextActiveTool.type !== "selection") {
|
||||
if (nextActiveTool.type === "lasso") {
|
||||
return {
|
||||
...prevState,
|
||||
activeTool: nextActiveTool,
|
||||
...(keepSelection
|
||||
? {}
|
||||
: {
|
||||
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
||||
selectedGroupIds: makeNextSelectedElementIds({}, prevState),
|
||||
editingGroupId: null,
|
||||
multiElement: null,
|
||||
}),
|
||||
...commonResets,
|
||||
};
|
||||
} else if (nextActiveTool.type !== "selection") {
|
||||
return {
|
||||
...prevState,
|
||||
activeTool: nextActiveTool,
|
||||
|
@ -6603,6 +6628,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
!this.state.penMode ||
|
||||
event.pointerType !== "touch" ||
|
||||
this.state.activeTool.type === "selection" ||
|
||||
this.state.activeTool.type === "lasso" ||
|
||||
this.state.activeTool.type === "text" ||
|
||||
this.state.activeTool.type === "image";
|
||||
|
||||
|
@ -6610,7 +6636,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.state.activeTool.type === "text") {
|
||||
if (this.state.activeTool.type === "lasso") {
|
||||
this.lassoTrail.startPath(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
event.shiftKey,
|
||||
);
|
||||
} else if (this.state.activeTool.type === "text") {
|
||||
this.handleTextOnPointerDown(event, pointerDownState);
|
||||
} else if (
|
||||
this.state.activeTool.type === "arrow" ||
|
||||
|
@ -7067,7 +7099,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
private clearSelectionIfNotUsingSelection = (): void => {
|
||||
if (this.state.activeTool.type !== "selection") {
|
||||
if (
|
||||
this.state.activeTool.type !== "selection" &&
|
||||
this.state.activeTool.type !== "lasso"
|
||||
) {
|
||||
this.setState({
|
||||
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||
selectedGroupIds: {},
|
||||
|
@ -8267,7 +8302,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (
|
||||
(hasHitASelectedElement ||
|
||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
||||
!isSelectingPointsInLineEditor
|
||||
!isSelectingPointsInLineEditor &&
|
||||
this.state.activeTool.type !== "lasso"
|
||||
) {
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
|
||||
|
@ -8539,7 +8575,37 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (this.state.selectionElement) {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||
if (event.altKey) {
|
||||
this.setActiveTool(
|
||||
{ type: "lasso", fromSelection: true },
|
||||
event.shiftKey,
|
||||
);
|
||||
this.lassoTrail.startPath(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
event.shiftKey,
|
||||
);
|
||||
this.setAppState({
|
||||
selectionElement: null,
|
||||
});
|
||||
} else {
|
||||
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||
}
|
||||
} else if (this.state.activeTool.type === "lasso") {
|
||||
if (!event.altKey && this.state.activeTool.fromSelection) {
|
||||
this.setActiveTool({ type: "selection" });
|
||||
this.createGenericElementOnPointerDown("selection", pointerDownState);
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||
this.maybeDragNewGenericElement(pointerDownState, event);
|
||||
this.lassoTrail.endPath();
|
||||
} else {
|
||||
this.lassoTrail.addPointToPath(
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
event.shiftKey,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// It is very important to read this.state within each move event,
|
||||
// otherwise we would read a stale one!
|
||||
|
@ -8794,6 +8860,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
originSnapOffset: null,
|
||||
}));
|
||||
|
||||
// just in case, tool changes mid drag, always clean up
|
||||
this.lassoTrail.endPath();
|
||||
this.lastPointerMoveCoords = null;
|
||||
|
||||
SnapCache.setReferenceSnapPoints(null);
|
||||
|
@ -9510,6 +9578,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
if (
|
||||
// do not clear selection if lasso is active
|
||||
this.state.activeTool.type !== "lasso" &&
|
||||
// not elbow midpoint dragged
|
||||
!(hitElement && isElbowArrow(hitElement)) &&
|
||||
// not dragged
|
||||
|
@ -9608,7 +9678,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
||||
if (
|
||||
!activeTool.locked &&
|
||||
activeTool.type !== "freedraw" &&
|
||||
(activeTool.type !== "lasso" ||
|
||||
// if lasso is turned on but from selection => reset to selection
|
||||
(activeTool.type === "lasso" && activeTool.fromSelection))
|
||||
) {
|
||||
resetCursor(this.interactiveCanvas);
|
||||
this.setState({
|
||||
newElement: null,
|
||||
|
@ -10463,7 +10539,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
width: distance(pointerDownState.origin.x, pointerCoords.x),
|
||||
height: distance(pointerDownState.origin.y, pointerCoords.y),
|
||||
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
|
||||
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
||||
shouldResizeFromCenter: false,
|
||||
zoom: this.state.zoom.value,
|
||||
informMutation,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue