Move linear element handling out of App.tsx

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs 2025-04-08 13:44:54 +02:00
parent c2b78346c1
commit 3068787ac4
No known key found for this signature in database
4 changed files with 532 additions and 420 deletions

View file

@ -1,14 +1,7 @@
import {
average,
type GlobalPoint,
type LocalPoint,
pointTranslate,
vector,
} from "@excalidraw/math";
import { average } from "@excalidraw/math";
import type {
ExcalidrawBindableElement,
ExcalidrawElement,
FontFamilyValues,
FontString,
} from "@excalidraw/element/types";
@ -1208,6 +1201,3 @@ export const escapeDoubleQuotes = (str: string) => {
export const castArray = <T>(value: T | T[]): T[] =>
Array.isArray(value) ? value : [value];
export const toLocalPoint = (p: GlobalPoint, element: ExcalidrawElement) =>
pointTranslate<GlobalPoint, LocalPoint>(p, vector(-element.x, -element.y));

View file

@ -6,7 +6,6 @@ import {
invariant,
isDevEnv,
isTestEnv,
toLocalPoint,
} from "@excalidraw/common";
import {
@ -527,14 +526,18 @@ export const bindLinearElement = (
const points = Array.from(linearElement.points);
if (isArrowElement(linearElement)) {
points[edgePointIndex] = toLocalPoint(
bindPointToSnapToElementOutline(
const [x, y] = bindPointToSnapToElementOutline(
linearElement,
hoveredElement,
startOrEnd,
elementsMap,
),
);
points[edgePointIndex] = LinearElementEditor.createPointAt(
linearElement,
elementsMap,
x,
y,
null,
);
}

View file

@ -100,7 +100,6 @@ import {
arrayToMap,
type EXPORT_IMAGE_TYPES,
randomInteger,
toLocalPoint,
} from "@excalidraw/common";
import {
@ -114,7 +113,6 @@ import {
fixBindingsAfterDeletion,
getHoveredElementForBinding,
isBindingEnabled,
isLinearElementSimpleAndAlreadyBound,
maybeBindLinearElement,
shouldEnableBindingForPointerEvent,
updateBoundElements,
@ -172,7 +170,6 @@ import {
} from "@excalidraw/element/typeChecks";
import {
getLockedLinearCursorAlignSize,
getNormalizedDimensions,
isElementCompletelyInViewport,
isElementInViewport,
@ -307,7 +304,6 @@ import { isNonDeletedElement } from "@excalidraw/element";
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
import type {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawFreeDrawElement,
ExcalidrawGenericElement,
@ -466,6 +462,14 @@ import { isMaybeMermaidDefinition } from "../mermaid";
import { LassoTrail } from "../lasso";
import {
handleCanvasPointerMoveForLinearElement,
handleDoubleClickForLinearElement,
maybeSuggestBindingsForLinearElementAtCoords,
onPointerMoveFromPointerDownOnLinearElement,
onPointerUpFromPointerDownOnLinearElementHandler,
} from "../linear";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import BraveMeasureTextError from "./BraveMeasureTextError";
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
@ -5447,77 +5451,18 @@ class App extends React.Component<AppProps, AppState> {
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
if (
event[KEYS.CTRL_OR_CMD] &&
(!this.state.editingLinearElement ||
this.state.editingLinearElement.elementId !==
selectedElements[0].id) &&
!isElbowArrow(selectedElements[0])
) {
this.store.shouldCaptureIncrement();
this.setState({
editingLinearElement: new LinearElementEditor(selectedElements[0]),
});
return;
} else if (
this.state.selectedLinearElement &&
isElbowArrow(selectedElements[0])
) {
const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords(
this.state.selectedLinearElement,
{ x: sceneX, y: sceneY },
this.state,
this.scene.getNonDeletedElementsMap(),
);
const midPoint = hitCoords
? LinearElementEditor.getSegmentMidPointIndex(
this.state.selectedLinearElement,
this.state,
hitCoords,
this.scene.getNonDeletedElementsMap(),
handleDoubleClickForLinearElement(
this,
this.store,
selectedElements[0],
event,
sceneX,
sceneY,
)
: -1;
if (midPoint && midPoint > -1) {
this.store.shouldCaptureIncrement();
LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
{
...this.state.selectedLinearElement,
segmentMidPointHoveredCoords: null,
},
{ x: sceneX, y: sceneY },
this.state,
this.scene.getNonDeletedElementsMap(),
);
const nextIndex = nextCoords
? LinearElementEditor.getSegmentMidPointIndex(
this.state.selectedLinearElement,
this.state,
nextCoords,
this.scene.getNonDeletedElementsMap(),
)
: null;
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
pointerDownState: {
...this.state.selectedLinearElement.pointerDownState,
segmentMidpoint: {
index: nextIndex,
value: hitCoords,
added: false,
},
},
segmentMidPointHoveredCoords: nextCoords,
},
});
) {
return;
}
}
}
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
this.startImageCropping(selectedElements[0]);
@ -5896,9 +5841,10 @@ class App extends React.Component<AppProps, AppState> {
// and point
const { newElement } = this.state;
if (isBindingElement(newElement, false)) {
this.maybeSuggestBindingsForLinearElementAtCoords(
maybeSuggestBindingsForLinearElementAtCoords(
newElement,
[scenePointer],
this,
this.state.startBoundElement,
);
} else {
@ -5908,122 +5854,15 @@ class App extends React.Component<AppProps, AppState> {
if (this.state.multiElement) {
const { multiElement } = this.state;
const { x: rx, y: ry } = multiElement;
const { points, lastCommittedPoint } = multiElement;
const lastPoint = points[points.length - 1];
setCursorForShape(this.interactiveCanvas, this.state);
if (lastPoint === lastCommittedPoint) {
// if we haven't yet created a temp point and we're beyond commit-zone
// threshold, add a point
if (
pointDistance(
pointFrom(scenePointerX - rx, scenePointerY - ry),
lastPoint,
) >= LINE_CONFIRM_THRESHOLD
) {
mutateElement(
handleCanvasPointerMoveForLinearElement(
multiElement,
{
points: [
...points,
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
],
},
false,
);
} else {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
// in this branch, we're inside the commit zone, and no uncommitted
// point exists. Thus do nothing (don't add/remove points).
}
} else if (
points.length > 2 &&
lastCommittedPoint &&
pointDistance(
pointFrom(scenePointerX - rx, scenePointerY - ry),
lastCommittedPoint,
) < LINE_CONFIRM_THRESHOLD
) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
mutateElement(
multiElement,
{
points: points.slice(0, -1),
},
false,
);
} else {
const [gridX, gridY] = getGridPoint(
this,
scenePointerX,
scenePointerY,
event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement)
? null
: this.getEffectiveGridSize(),
event,
this.triggerRender,
);
const [lastCommittedX, lastCommittedY] =
multiElement?.lastCommittedPoint ?? [0, 0];
let dxFromLastCommitted = gridX - rx - lastCommittedX;
let dyFromLastCommitted = gridY - ry - lastCommittedY;
if (shouldRotateWithDiscreteAngle(event)) {
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
getLockedLinearCursorAlignSize(
// actual coordinate of the last committed point
lastCommittedX + rx,
lastCommittedY + ry,
// cursor-grid coordinate
gridX,
gridY,
));
}
if (isPathALoop(points, this.state.zoom.value)) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
}
// update last uncommitted point
mutateElement(
multiElement,
{
points: [
...points.slice(0, -1),
isArrowElement(multiElement)
? toLocalPoint(
getOutlineAvoidingPoint(
multiElement,
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
multiElement.points.length - 1,
this.scene,
this.state.zoom,
pointFrom<GlobalPoint>(
multiElement.x + lastCommittedX + dxFromLastCommitted,
multiElement.y + lastCommittedY + dyFromLastCommitted,
),
),
multiElement,
)
: pointFrom<LocalPoint>(
lastCommittedX + dxFromLastCommitted,
lastCommittedY + dyFromLastCommitted,
),
],
},
false,
{
isDragging: true,
},
);
// in this path, we're mutating multiElement to reflect
// how it will be after adding pointer position as the next point
// trigger update here so that new element canvas renders again to reflect this
this.triggerRender(false);
}
return;
}
@ -8301,9 +8140,10 @@ class App extends React.Component<AppProps, AppState> {
pointerCoords.x,
pointerCoords.y,
(element, pointsSceneCoords) => {
this.maybeSuggestBindingsForLinearElementAtCoords(
maybeSuggestBindingsForLinearElementAtCoords(
element,
pointsSceneCoords,
this,
);
},
linearElementEditor,
@ -8691,120 +8531,14 @@ class App extends React.Component<AppProps, AppState> {
});
}
} else if (isLinearElement(newElement)) {
pointerDownState.drag.hasOccurred = true;
const points = newElement.points;
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
onPointerMoveFromPointerDownOnLinearElement(
newElement,
this,
pointerDownState,
pointerCoords,
event,
elementsMap,
);
let dx = gridX - newElement.x;
let dy = gridY - newElement.y;
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
newElement.x,
newElement.y,
pointerCoords.x,
pointerCoords.y,
));
}
if (points.length === 1) {
mutateElement(
newElement,
{
points: [
...points,
isArrowElement(newElement)
? toLocalPoint(
getOutlineAvoidingPoint(
newElement,
pointFrom<GlobalPoint>(
pointerCoords.x,
pointerCoords.y,
),
newElement.points.length - 1,
this.scene,
this.state.zoom,
pointFrom<GlobalPoint>(
newElement.x + dx,
newElement.y + dy,
),
),
newElement,
)
: pointFrom<LocalPoint>(dx, dy),
],
},
false,
);
} else if (
points.length === 2 ||
(points.length > 1 && isElbowArrow(newElement))
) {
mutateElement(
newElement,
{
points: [
...points.slice(0, -1),
isArrowElement(newElement)
? toLocalPoint(
getOutlineAvoidingPoint(
newElement,
pointFrom<GlobalPoint>(
pointerCoords.x,
pointerCoords.y,
),
newElement.points.length - 1,
this.scene,
this.state.zoom,
pointFrom<GlobalPoint>(
newElement.x + dx,
newElement.y + dy,
),
),
newElement,
)
: pointFrom<LocalPoint>(dx, dy),
],
},
false,
{ isDragging: true },
);
LinearElementEditor.movePoints(newElement, [
{
index: 0,
isDragging: false,
point: toLocalPoint(
getOutlineAvoidingPoint(
newElement,
pointFrom<GlobalPoint>(
pointerDownState.origin.x,
pointerDownState.origin.y,
),
0,
this.scene,
this.state.zoom,
),
newElement,
),
},
]);
}
this.setState({
newElement,
});
if (isBindingElement(newElement, false)) {
// When creating a linear element by dragging
this.maybeSuggestBindingsForLinearElementAtCoords(
newElement,
[pointerCoords],
this.state.startBoundElement,
);
}
} else {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
@ -9170,65 +8904,15 @@ class App extends React.Component<AppProps, AppState> {
}
if (isLinearElement(newElement)) {
if (newElement!.points.length > 1) {
this.store.shouldCaptureIncrement();
}
const pointerCoords = viewportCoordsToSceneCoords(
onPointerUpFromPointerDownOnLinearElementHandler(
newElement,
multiElement,
this,
this.store,
pointerDownState,
childEvent,
this.state,
activeTool,
);
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
mutateElement(newElement, {
points: [
...newElement.points,
pointFrom<LocalPoint>(
pointerCoords.x - newElement.x,
pointerCoords.y - newElement.y,
),
],
});
this.setState({
multiElement: newElement,
newElement,
});
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
if (
isBindingEnabled(this.state) &&
isBindingElement(newElement, false)
) {
maybeBindLinearElement(
newElement,
this.state,
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
);
}
this.setState({ suggestedBindings: [], startBoundElement: null });
if (!activeTool.locked) {
resetCursor(this.interactiveCanvas);
this.setState((prevState) => ({
newElement: null,
activeTool: updateActiveTool(this.state, {
type: "selection",
}),
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[newElement.id]: true,
},
prevState,
),
selectedLinearElement: new LinearElementEditor(newElement),
}));
} else {
this.setState((prevState) => ({
newElement: null,
}));
}
// so that the scene gets rendered again to display the newly drawn linear as well
this.scene.triggerUpdate();
}
return;
}
@ -10294,49 +9978,6 @@ class App extends React.Component<AppProps, AppState> {
});
};
private maybeSuggestBindingsForLinearElementAtCoords = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
/** scene coords */
pointerCoords: {
x: number;
y: number;
}[],
// During line creation the start binding hasn't been written yet
// into `linearElement`
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
): void => {
if (!pointerCoords.length) {
return;
}
const suggestedBindings = pointerCoords.reduce(
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
const hoveredBindableElement = getHoveredElementForBinding(
coords,
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
isElbowArrow(linearElement),
isElbowArrow(linearElement),
);
if (
hoveredBindableElement != null &&
!isLinearElementSimpleAndAlreadyBound(
linearElement,
oppositeBindingBoundElement?.id,
hoveredBindableElement,
)
) {
acc.push(hoveredBindableElement);
}
return acc;
},
[],
);
this.setState({ suggestedBindings });
};
private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),

View file

@ -0,0 +1,478 @@
import {
CURSOR_TYPE,
getGridPoint,
KEYS,
LINE_CONFIRM_THRESHOLD,
shouldRotateWithDiscreteAngle,
updateActiveTool,
viewportCoordsToSceneCoords,
} from "@excalidraw/common";
import { getLockedLinearCursorAlignSize } from "@excalidraw/element/sizeHelpers";
import {
isArrowElement,
isBindingElement,
isElbowArrow,
} from "@excalidraw/element/typeChecks";
import {
getHoveredElementForBinding,
getOutlineAvoidingPoint,
isBindingEnabled,
isLinearElementSimpleAndAlreadyBound,
maybeBindLinearElement,
} from "@excalidraw/element/binding";
import { pointDistance, pointFrom } from "@excalidraw/math";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import { isPathALoop } from "@excalidraw/element/shapes";
import { makeNextSelectedElementIds } from "@excalidraw/element/selection";
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
import type {
ExcalidrawBindableElement,
ExcalidrawLinearElement,
NonDeleted,
NonDeletedSceneElementsMap,
} from "@excalidraw/element/types";
import { resetCursor, setCursor, setCursorForShape } from "./cursor";
import type App from "./components/App";
import type { ActiveTool, PointerDownState } from "./types";
/**
* This function is called when the user drags the pointer to create a new linear element.
*/
export function onPointerMoveFromPointerDownOnLinearElement(
newElement: ExcalidrawLinearElement,
app: App,
pointerDownState: PointerDownState,
pointerCoords: { x: number; y: number },
event: PointerEvent,
elementsMap: NonDeletedSceneElementsMap,
) {
pointerDownState.drag.hasOccurred = true;
const points = newElement.points;
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
);
let dx = gridX - newElement.x;
let dy = gridY - newElement.y;
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
newElement.x,
newElement.y,
pointerCoords.x,
pointerCoords.y,
));
}
if (points.length === 1) {
let x = newElement.x + dx;
let y = newElement.y + dy;
if (isArrowElement(newElement)) {
[x, y] = getOutlineAvoidingPoint(
newElement,
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
newElement.points.length - 1,
app.scene,
app.state.zoom,
pointFrom<GlobalPoint>(newElement.x + dx, newElement.y + dy),
);
}
mutateElement(
newElement,
{
points: [
...points,
LinearElementEditor.createPointAt(
newElement,
elementsMap,
x,
y,
app.getEffectiveGridSize(),
),
],
},
false,
);
} else if (
points.length === 2 ||
(points.length > 1 && isElbowArrow(newElement))
) {
const targets = [
{
index: points.length - 1,
isDragging: true,
point: pointFrom<LocalPoint>(dx, dy),
},
];
if (isArrowElement(newElement)) {
const [x, y] = getOutlineAvoidingPoint(
newElement,
pointFrom<GlobalPoint>(
pointerDownState.origin.x,
pointerDownState.origin.y,
),
0,
app.scene,
app.state.zoom,
);
targets.unshift({
index: 0,
isDragging: false,
point: LinearElementEditor.createPointAt(
newElement,
elementsMap,
x,
y,
app.getEffectiveGridSize(),
),
});
}
LinearElementEditor.movePoints(newElement, targets);
}
app.setState({
newElement,
});
if (isBindingElement(newElement, false)) {
// When creating a linear element by dragging
maybeSuggestBindingsForLinearElementAtCoords(
newElement,
[pointerCoords],
app,
app.state.startBoundElement,
);
}
}
/**
*
*/
export function handleCanvasPointerMoveForLinearElement(
multiElement: NonDeleted<ExcalidrawLinearElement>,
app: App,
scenePointerX: number,
scenePointerY: number,
event: React.PointerEvent<HTMLCanvasElement>,
triggerRender: (forceUpdate?: boolean) => void,
) {
const { x: rx, y: ry } = multiElement;
const { points, lastCommittedPoint } = multiElement;
const lastPoint = points[points.length - 1];
setCursorForShape(app.interactiveCanvas, app.state);
if (lastPoint === lastCommittedPoint) {
// if we haven't yet created a temp point and we're beyond commit-zone
// threshold, add a point
if (
pointDistance(
pointFrom(scenePointerX - rx, scenePointerY - ry),
lastPoint,
) >= LINE_CONFIRM_THRESHOLD
) {
mutateElement(
multiElement,
{
points: [
...points,
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
],
},
false,
);
} else {
setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER);
// in this branch, we're inside the commit zone, and no uncommitted
// point exists. Thus do nothing (don't add/remove points).
}
} else if (
points.length > 2 &&
lastCommittedPoint &&
pointDistance(
pointFrom(scenePointerX - rx, scenePointerY - ry),
lastCommittedPoint,
) < LINE_CONFIRM_THRESHOLD
) {
setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER);
mutateElement(
multiElement,
{
points: points.slice(0, -1),
},
false,
);
} else {
const [gridX, gridY] = getGridPoint(
scenePointerX,
scenePointerY,
event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement)
? null
: app.getEffectiveGridSize(),
);
console.log(points);
const [lastCommittedX, lastCommittedY] =
multiElement?.lastCommittedPoint ?? [0, 0];
let dxFromLastCommitted = gridX - rx - lastCommittedX;
let dyFromLastCommitted = gridY - ry - lastCommittedY;
if (shouldRotateWithDiscreteAngle(event)) {
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
getLockedLinearCursorAlignSize(
// actual coordinate of the last committed point
lastCommittedX + rx,
lastCommittedY + ry,
// cursor-grid coordinate
gridX,
gridY,
));
}
if (isPathALoop(points, app.state.zoom.value)) {
setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER);
}
let x = multiElement.x + lastCommittedX + dxFromLastCommitted;
let y = multiElement.y + lastCommittedY + dyFromLastCommitted;
if (isArrowElement(multiElement)) {
[x, y] = getOutlineAvoidingPoint(
multiElement,
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
multiElement.points.length - 1,
app.scene,
app.state.zoom,
pointFrom<GlobalPoint>(x, y),
);
}
// update last uncommitted point
LinearElementEditor.movePoints(multiElement, [
{
index: points.length - 1,
point: LinearElementEditor.createPointAt(
multiElement,
app.scene.getNonDeletedElementsMap(),
x,
y,
app.getEffectiveGridSize(),
),
isDragging: true,
},
]);
// in this path, we're mutating multiElement to reflect
// how it will be after adding pointer position as the next point
// trigger update here so that new element canvas renders again to reflect this
triggerRender(false);
}
}
export function onPointerUpFromPointerDownOnLinearElementHandler(
newElement: ExcalidrawLinearElement,
multiElement: NonDeleted<ExcalidrawLinearElement> | null,
app: App,
store: App["store"],
pointerDownState: PointerDownState,
childEvent: PointerEvent,
activeTool: {
lastActiveTool: ActiveTool | null;
locked: boolean;
fromSelection: boolean;
} & ActiveTool,
) {
if (newElement!.points.length > 1) {
store.shouldCaptureIncrement();
}
const pointerCoords = viewportCoordsToSceneCoords(childEvent, app.state);
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
mutateElement(newElement, {
points: [
...newElement.points,
pointFrom<LocalPoint>(
pointerCoords.x - newElement.x,
pointerCoords.y - newElement.y,
),
],
});
app.setState({
multiElement: newElement,
newElement,
});
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
if (isBindingEnabled(app.state) && isBindingElement(newElement, false)) {
maybeBindLinearElement(
newElement,
app.state,
app.scene.getNonDeletedElementsMap(),
app.scene.getNonDeletedElements(),
);
}
app.setState({ suggestedBindings: [], startBoundElement: null });
if (!activeTool.locked) {
resetCursor(app.interactiveCanvas);
app.setState((prevState) => ({
newElement: null,
activeTool: updateActiveTool(app.state, {
type: "selection",
}),
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[newElement.id]: true,
},
prevState,
),
selectedLinearElement: new LinearElementEditor(newElement),
}));
} else {
app.setState((prevState) => ({
newElement: null,
}));
}
// so that the scene gets rendered again to display the newly drawn linear as well
app.scene.triggerUpdate();
}
}
/**
* Handles double click on a linear element to edit it or delete a segment
*/
export function handleDoubleClickForLinearElement(
app: App,
store: App["store"],
selectedElement: NonDeleted<ExcalidrawLinearElement>,
event: React.MouseEvent<HTMLCanvasElement>,
sceneX: number,
sceneY: number,
) {
if (
event[KEYS.CTRL_OR_CMD] &&
(!app.state.editingLinearElement ||
app.state.editingLinearElement.elementId !== selectedElement.id) &&
!isElbowArrow(selectedElement)
) {
store.shouldCaptureIncrement();
app.setState({
editingLinearElement: new LinearElementEditor(selectedElement),
});
} else if (app.state.selectedLinearElement && isElbowArrow(selectedElement)) {
const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords(
app.state.selectedLinearElement,
{ x: sceneX, y: sceneY },
app.state,
app.scene.getNonDeletedElementsMap(),
);
const midPoint = hitCoords
? LinearElementEditor.getSegmentMidPointIndex(
app.state.selectedLinearElement,
app.state,
hitCoords,
app.scene.getNonDeletedElementsMap(),
)
: -1;
if (midPoint && midPoint > -1) {
store.shouldCaptureIncrement();
LinearElementEditor.deleteFixedSegment(selectedElement, midPoint);
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
{
...app.state.selectedLinearElement,
segmentMidPointHoveredCoords: null,
},
{ x: sceneX, y: sceneY },
app.state,
app.scene.getNonDeletedElementsMap(),
);
const nextIndex = nextCoords
? LinearElementEditor.getSegmentMidPointIndex(
app.state.selectedLinearElement,
app.state,
nextCoords,
app.scene.getNonDeletedElementsMap(),
)
: null;
app.setState({
selectedLinearElement: {
...app.state.selectedLinearElement,
pointerDownState: {
...app.state.selectedLinearElement.pointerDownState,
segmentMidpoint: {
index: nextIndex,
value: hitCoords,
added: false,
},
},
segmentMidPointHoveredCoords: nextCoords,
},
});
return true;
}
}
}
export function maybeSuggestBindingsForLinearElementAtCoords(
linearElement: NonDeleted<ExcalidrawLinearElement>,
/** scene coords */
pointerCoords: {
x: number;
y: number;
}[],
app: App,
// During line creation the start binding hasn't been written yet
// into `linearElement`
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
) {
if (!pointerCoords.length) {
return;
}
const suggestedBindings = pointerCoords.reduce(
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
const hoveredBindableElement = getHoveredElementForBinding(
coords,
app.scene.getNonDeletedElements(),
app.scene.getNonDeletedElementsMap(),
app.state.zoom,
isElbowArrow(linearElement),
isElbowArrow(linearElement),
);
if (
hoveredBindableElement != null &&
!isLinearElementSimpleAndAlreadyBound(
linearElement,
oppositeBindingBoundElement?.id,
hoveredBindableElement,
)
) {
acc.push(hoveredBindableElement);
}
return acc;
},
[],
);
app.setState({ suggestedBindings });
}