mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
Move linear element handling out of App.tsx
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
parent
c2b78346c1
commit
3068787ac4
4 changed files with 532 additions and 420 deletions
|
@ -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));
|
||||
|
|
|
@ -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(
|
||||
linearElement,
|
||||
hoveredElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
),
|
||||
const [x, y] = bindPointToSnapToElementOutline(
|
||||
linearElement,
|
||||
hoveredElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
);
|
||||
points[edgePointIndex] = LinearElementEditor.createPointAt(
|
||||
linearElement,
|
||||
elementsMap,
|
||||
x,
|
||||
y,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,75 +5451,16 @@ 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])
|
||||
handleDoubleClickForLinearElement(
|
||||
this,
|
||||
this.store,
|
||||
selectedElements[0],
|
||||
event,
|
||||
sceneX,
|
||||
sceneY,
|
||||
)
|
||||
) {
|
||||
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(),
|
||||
)
|
||||
: -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,121 +5854,14 @@ 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(
|
||||
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(
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement)
|
||||
? null
|
||||
: this.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
handleCanvasPointerMoveForLinearElement(
|
||||
multiElement,
|
||||
this,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
event,
|
||||
this.triggerRender,
|
||||
);
|
||||
|
||||
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(),
|
||||
);
|
||||
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({
|
||||
onPointerMoveFromPointerDownOnLinearElement(
|
||||
newElement,
|
||||
});
|
||||
|
||||
if (isBindingElement(newElement, false)) {
|
||||
// When creating a linear element by dragging
|
||||
this.maybeSuggestBindingsForLinearElementAtCoords(
|
||||
newElement,
|
||||
[pointerCoords],
|
||||
this.state.startBoundElement,
|
||||
);
|
||||
}
|
||||
this,
|
||||
pointerDownState,
|
||||
pointerCoords,
|
||||
event,
|
||||
elementsMap,
|
||||
);
|
||||
} 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),
|
||||
|
|
478
packages/excalidraw/linear.ts
Normal file
478
packages/excalidraw/linear.ts
Normal 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 });
|
||||
}
|
Loading…
Add table
Reference in a new issue