mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-25 00:44:38 +02:00
Refactor delayed bind mode change
This commit is contained in:
@@ -9,7 +9,6 @@ import {
|
||||
vectorFromPoint,
|
||||
curveLength,
|
||||
curvePointAtLength,
|
||||
lineSegment,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||
@@ -24,10 +23,7 @@ import {
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
bindingBorderTest,
|
||||
CaptureUpdateAction,
|
||||
deconstructLinearOrFreeDrawElement,
|
||||
getHoveredElementForBinding,
|
||||
isPathALoop,
|
||||
moveArrowAboveBindable,
|
||||
type Store,
|
||||
@@ -45,11 +41,9 @@ import type {
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import {
|
||||
getGlobalFixedPointForBindableElement,
|
||||
getOutlineAvoidingPoint,
|
||||
isBindingEnabled,
|
||||
getBindingStrategyForDraggingBindingElementEndpoints,
|
||||
getStartGlobalEndLocalPointsForSimpleArrowBinding,
|
||||
maybeSuggestBindingsForBindingElementAtCoords,
|
||||
snapToCenter,
|
||||
} from "./binding";
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
@@ -59,17 +53,8 @@ import {
|
||||
|
||||
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
handleBindTextResize,
|
||||
} from "./textElement";
|
||||
import {
|
||||
isBindingElement,
|
||||
isElbowArrow,
|
||||
isSimpleArrow,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import { isArrowElement, isBindingElement, isElbowArrow } from "./typeChecks";
|
||||
|
||||
import { ShapeCache, toggleLinePolygonState } from "./shape";
|
||||
|
||||
@@ -84,7 +69,6 @@ import type {
|
||||
NonDeleted,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
@@ -357,31 +341,24 @@ export class LinearElementEditor {
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
customLineAngle,
|
||||
);
|
||||
const [x, y] = LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
pointFrom<LocalPoint>(
|
||||
width + referencePoint[0],
|
||||
height + referencePoint[1],
|
||||
),
|
||||
elementsMap,
|
||||
const reference = pointFrom<LocalPoint>(
|
||||
width + referencePoint[0],
|
||||
height + referencePoint[1],
|
||||
);
|
||||
const deltaX = reference[0] - draggingPoint[0];
|
||||
const deltaY = reference[1] - draggingPoint[1];
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
app.scene,
|
||||
pointDraggingUpdates(
|
||||
selectedPointsIndices,
|
||||
0,
|
||||
0,
|
||||
deltaX,
|
||||
deltaY,
|
||||
elementsMap,
|
||||
lastClickedPoint,
|
||||
element,
|
||||
x,
|
||||
y,
|
||||
linearElementEditor,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
elements,
|
||||
app,
|
||||
true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -403,12 +380,7 @@ export class LinearElementEditor {
|
||||
deltaX,
|
||||
deltaY,
|
||||
elementsMap,
|
||||
lastClickedPoint,
|
||||
element,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
linearElementEditor,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
elements,
|
||||
app,
|
||||
),
|
||||
@@ -1948,211 +1920,127 @@ const normalizeSelectedPoints = (
|
||||
return nextPoints.length ? nextPoints : null;
|
||||
};
|
||||
|
||||
const pointDraggingUpdates = (
|
||||
export const pointDraggingUpdates = (
|
||||
selectedPointsIndices: readonly number[],
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
lastClickedPoint: number,
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
gridSize: NullableGridSize,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
app: AppClassProperties,
|
||||
angleLocked?: boolean,
|
||||
): PointsPositionUpdates => {
|
||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap, true);
|
||||
const hasMidPoints =
|
||||
selectedPointsIndices.filter(
|
||||
(_, idx) => idx > 0 && idx < element.points.length - 1,
|
||||
).length > 0;
|
||||
|
||||
const updates = new Map(
|
||||
const naiveDraggingPoints = new Map(
|
||||
selectedPointsIndices.map((pointIndex) => {
|
||||
let newPointPosition: LocalPoint =
|
||||
pointIndex === lastClickedPoint
|
||||
? LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
gridSize,
|
||||
)
|
||||
: pointFrom(
|
||||
element.points[pointIndex][0] + deltaX,
|
||||
element.points[pointIndex][1] + deltaY,
|
||||
);
|
||||
|
||||
if (
|
||||
isSimpleArrow(element) &&
|
||||
!hasMidPoints &&
|
||||
(pointIndex === 0 || pointIndex === element.points.length - 1)
|
||||
) {
|
||||
let newGlobalPointPosition = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + newPointPosition[0],
|
||||
element.y + newPointPosition[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
newGlobalPointPosition,
|
||||
elements,
|
||||
elementsMap,
|
||||
);
|
||||
const otherGlobalPoint =
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
element,
|
||||
pointIndex === 0 ? -1 : 0,
|
||||
elementsMap,
|
||||
);
|
||||
const otherPointInsideElement =
|
||||
!!hoveredElement &&
|
||||
!!bindingBorderTest(hoveredElement, otherGlobalPoint, elementsMap);
|
||||
|
||||
if (
|
||||
isBindingEnabled(app.state) &&
|
||||
isBindingElement(element, false) &&
|
||||
hoveredElement &&
|
||||
app.state.bindMode === "orbit" &&
|
||||
!otherPointInsideElement
|
||||
) {
|
||||
let customIntersector;
|
||||
if (angleLocked) {
|
||||
const adjacentPointIndex =
|
||||
pointIndex === 0 ? 1 : element.points.length - 2;
|
||||
const globalAdjacentPoint =
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
element,
|
||||
adjacentPointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
customIntersector = lineSegment<GlobalPoint>(
|
||||
globalAdjacentPoint,
|
||||
newGlobalPointPosition,
|
||||
);
|
||||
}
|
||||
|
||||
newGlobalPointPosition = getOutlineAvoidingPoint(
|
||||
element,
|
||||
hoveredElement,
|
||||
element.startBinding
|
||||
? snapToCenter(
|
||||
hoveredElement,
|
||||
elementsMap,
|
||||
newGlobalPointPosition,
|
||||
)
|
||||
: newGlobalPointPosition,
|
||||
pointIndex,
|
||||
elementsMap,
|
||||
customIntersector,
|
||||
);
|
||||
}
|
||||
|
||||
newPointPosition = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
newGlobalPointPosition[0] - linearElementEditor.pointerOffset.x,
|
||||
newGlobalPointPosition[1] - linearElementEditor.pointerOffset.y,
|
||||
null,
|
||||
);
|
||||
|
||||
// Update z-index of the arrow
|
||||
if (
|
||||
isBindingEnabled(app.state) &&
|
||||
isBindingElement(element) &&
|
||||
hoveredElement
|
||||
) {
|
||||
const boundTextElement = getBoundTextElement(
|
||||
hoveredElement,
|
||||
elementsMap,
|
||||
);
|
||||
const containerElement = isTextElement(hoveredElement)
|
||||
? getContainerElement(hoveredElement, elementsMap)
|
||||
: null;
|
||||
const newElements = moveArrowAboveBindable(
|
||||
element,
|
||||
[
|
||||
hoveredElement.id,
|
||||
boundTextElement?.id,
|
||||
containerElement?.id,
|
||||
].filter((id): id is NonDeletedExcalidrawElement["id"] => !!id),
|
||||
app.scene,
|
||||
);
|
||||
|
||||
app.syncActionResult({
|
||||
elements: newElements,
|
||||
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
pointIndex,
|
||||
{
|
||||
point: newPointPosition,
|
||||
isDragging: pointIndex === lastClickedPoint,
|
||||
point: pointFrom<LocalPoint>(
|
||||
element.points[pointIndex][0] + deltaX,
|
||||
element.points[pointIndex][1] + deltaY,
|
||||
),
|
||||
isDragging: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
if (isSimpleArrow(element)) {
|
||||
const adjacentPointIndices =
|
||||
element.points.length === 2
|
||||
? [0, 1]
|
||||
: element.points.length === 3
|
||||
? [1]
|
||||
: [1, element.points.length - 2];
|
||||
|
||||
adjacentPointIndices
|
||||
.filter((adjacentPointIndex) =>
|
||||
selectedPointsIndices.includes(adjacentPointIndex),
|
||||
)
|
||||
.flatMap((adjacentPointIndex) =>
|
||||
element.points.length === 3
|
||||
? [0, 2]
|
||||
: adjacentPointIndex === 1
|
||||
? 0
|
||||
: element.points.length - 1,
|
||||
)
|
||||
.forEach((pointIndex) => {
|
||||
const binding =
|
||||
element[pointIndex === 0 ? "startBinding" : "endBinding"];
|
||||
const bindingIsOrbiting = binding?.mode === "orbit";
|
||||
if (bindingIsOrbiting) {
|
||||
const hoveredElement = elementsMap.get(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
const focusGlobalPoint = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
hoveredElement,
|
||||
elementsMap,
|
||||
);
|
||||
const newGlobalPointPosition = getOutlineAvoidingPoint(
|
||||
element,
|
||||
hoveredElement,
|
||||
snapToCenter(hoveredElement, elementsMap, focusGlobalPoint),
|
||||
pointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
const newPointPosition = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
elementsMap,
|
||||
newGlobalPointPosition[0] - linearElementEditor.pointerOffset.x,
|
||||
newGlobalPointPosition[1] - linearElementEditor.pointerOffset.y,
|
||||
null,
|
||||
);
|
||||
updates.set(pointIndex, {
|
||||
point: newPointPosition,
|
||||
isDragging: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
// Linear elements have no special logic
|
||||
if (!isArrowElement(element) || isElbowArrow(element)) {
|
||||
return naiveDraggingPoints;
|
||||
}
|
||||
|
||||
return updates;
|
||||
const startIsDragged = selectedPointsIndices.includes(0);
|
||||
const endIsDragged = selectedPointsIndices.includes(
|
||||
element.points.length - 1,
|
||||
);
|
||||
|
||||
if (startIsDragged === endIsDragged) {
|
||||
return naiveDraggingPoints;
|
||||
}
|
||||
|
||||
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
|
||||
element,
|
||||
naiveDraggingPoints,
|
||||
elementsMap,
|
||||
elements,
|
||||
app.state,
|
||||
);
|
||||
|
||||
const originalStartGlobalPoint =
|
||||
LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
element.points[0],
|
||||
elementsMap,
|
||||
);
|
||||
const originalEndGlobalPoint = LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
element.points[element.points.length - 1],
|
||||
elementsMap,
|
||||
);
|
||||
const offsetStartGlobalPoint = startIsDragged
|
||||
? pointFrom<GlobalPoint>(
|
||||
originalStartGlobalPoint[0] + deltaX,
|
||||
originalStartGlobalPoint[1] + deltaY,
|
||||
)
|
||||
: originalStartGlobalPoint;
|
||||
const offsetEndGlobalPoint = pointFrom<GlobalPoint>(
|
||||
originalEndGlobalPoint[0] + deltaX,
|
||||
originalEndGlobalPoint[1] + deltaY,
|
||||
);
|
||||
const offsetEndLocalPoint = pointFrom<LocalPoint>(
|
||||
offsetEndGlobalPoint[0] - offsetStartGlobalPoint[0],
|
||||
offsetEndGlobalPoint[1] - offsetStartGlobalPoint[1],
|
||||
);
|
||||
|
||||
const [startGlobalPoint, endLocalPoint] =
|
||||
getStartGlobalEndLocalPointsForSimpleArrowBinding(
|
||||
element,
|
||||
start,
|
||||
end,
|
||||
offsetStartGlobalPoint,
|
||||
offsetEndLocalPoint,
|
||||
elementsMap,
|
||||
);
|
||||
const startLocalPoint = LinearElementEditor.pointFromAbsoluteCoords(
|
||||
element,
|
||||
startGlobalPoint,
|
||||
elementsMap,
|
||||
);
|
||||
const finalEndLocalPoint = pointFrom<LocalPoint>(
|
||||
endLocalPoint[0] + (startGlobalPoint[0] - element.x),
|
||||
endLocalPoint[1] + (startGlobalPoint[1] - element.y),
|
||||
);
|
||||
|
||||
if (startIsDragged !== endIsDragged) {
|
||||
moveArrowAboveBindable(
|
||||
startIsDragged
|
||||
? startGlobalPoint
|
||||
: LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
finalEndLocalPoint,
|
||||
elementsMap,
|
||||
),
|
||||
element,
|
||||
elements,
|
||||
elementsMap,
|
||||
app.scene,
|
||||
);
|
||||
}
|
||||
|
||||
const indices = Array.from(
|
||||
new Set([0, element.points.length - 1, ...selectedPointsIndices]),
|
||||
);
|
||||
|
||||
return new Map(
|
||||
indices.map((idx) => {
|
||||
return [
|
||||
idx,
|
||||
idx === 0
|
||||
? { point: startLocalPoint, isDragging: true }
|
||||
: idx === element.points.length - 1
|
||||
? { point: finalEndLocalPoint, isDragging: true }
|
||||
: naiveDraggingPoints.get(idx)!,
|
||||
];
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
import type { GlobalPoint } from "@excalidraw/math";
|
||||
|
||||
import { isFrameLikeElement } from "./typeChecks";
|
||||
|
||||
import { isFrameLikeElement, isTextElement } from "./typeChecks";
|
||||
import { getElementsInGroup } from "./groups";
|
||||
|
||||
import { syncMovedIndices } from "./fractionalIndex";
|
||||
|
||||
import { getSelectedElements } from "./selection";
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
import { getHoveredElementForBinding } from "./collision";
|
||||
|
||||
import type { Scene } from "./Scene";
|
||||
|
||||
import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
Ordered,
|
||||
OrderedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
@@ -144,12 +146,37 @@ const getContiguousFrameRangeElements = (
|
||||
return allElements.slice(rangeStart, rangeEnd + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves the arrow element above any bindable elements it intersects with or
|
||||
* hovers over.
|
||||
*/
|
||||
export const moveArrowAboveBindable = (
|
||||
point: GlobalPoint,
|
||||
arrow: ExcalidrawArrowElement,
|
||||
bindableIds: string[],
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
): readonly OrderedExcalidrawElement[] => {
|
||||
const elements = scene.getElementsIncludingDeleted();
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
point,
|
||||
elements,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
if (!hoveredElement) {
|
||||
return elements;
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(hoveredElement, elementsMap);
|
||||
const containerElement = isTextElement(hoveredElement)
|
||||
? getContainerElement(hoveredElement, elementsMap)
|
||||
: null;
|
||||
|
||||
const bindableIds = [
|
||||
hoveredElement.id,
|
||||
boundTextElement?.id,
|
||||
containerElement?.id,
|
||||
].filter((id): id is NonDeletedExcalidrawElement["id"] => !!id);
|
||||
const bindableIdx = elements.findIndex((el) => bindableIds.includes(el.id));
|
||||
const arrowIdx = elements.findIndex((el) => el.id === arrow.id);
|
||||
|
||||
@@ -157,9 +184,8 @@ export const moveArrowAboveBindable = (
|
||||
const updatedElements = Array.from(elements);
|
||||
const arrow = updatedElements.splice(arrowIdx, 1)[0];
|
||||
updatedElements.splice(bindableIdx, 0, arrow);
|
||||
syncMovedIndices(elements, arrayToMap([arrow]));
|
||||
|
||||
return updatedElements;
|
||||
scene.replaceAllElements(updatedElements);
|
||||
}
|
||||
|
||||
return elements;
|
||||
|
||||
@@ -243,6 +243,7 @@ import {
|
||||
getBindingStrategyForDraggingBindingElementEndpoints,
|
||||
getStartGlobalEndLocalPointsForSimpleArrowBinding,
|
||||
mutateElement,
|
||||
pointDraggingUpdates,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||
@@ -934,19 +935,24 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
if (hoveredElement) {
|
||||
invariant(
|
||||
this.state.selectedLinearElement?.elementId === arrow.id,
|
||||
"The selectedLinearElement is expected to not change while a bind mode timeout is ticking",
|
||||
);
|
||||
|
||||
// Once the start is set to inside binding, it remains so
|
||||
const arrowStartIsInside =
|
||||
this.state.selectedLinearElement.pointerDownState
|
||||
.arrowStartIsInside ||
|
||||
arrow.startBinding?.elementId === hoveredElement.id;
|
||||
|
||||
// Change the global binding mode
|
||||
flushSync(() => {
|
||||
invariant(
|
||||
this.state.selectedLinearElement?.elementId === arrow.id,
|
||||
"The selectedLinearElement is expected to not change while a bind mode timeout is ticking",
|
||||
this.state.selectedLinearElement,
|
||||
"this.state.selectedLinearElement must exist",
|
||||
);
|
||||
|
||||
// Once the start is set to inside binding, it remains so
|
||||
const arrowStartIsInside =
|
||||
this.state.selectedLinearElement.pointerDownState
|
||||
.arrowStartIsInside ||
|
||||
arrow.startBinding?.elementId === hoveredElement.id;
|
||||
|
||||
// Change the global binding mode
|
||||
this.setState({
|
||||
bindMode: "inside",
|
||||
selectedLinearElement: {
|
||||
@@ -957,21 +963,36 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Make the arrow endpoint "jump" to the cursor
|
||||
const point = LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
x,
|
||||
y,
|
||||
isBindingEnabled(this.state) ? this.getEffectiveGridSize() : null,
|
||||
);
|
||||
this.scene.mutateElement(arrow, {
|
||||
points: startDragged
|
||||
? [point, ...arrow.points.slice(1)]
|
||||
: [...arrow.points.slice(0, -1), point],
|
||||
});
|
||||
});
|
||||
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
const elements = this.scene.getNonDeletedElements();
|
||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
elementsMap,
|
||||
x - this.state.selectedLinearElement.pointerOffset.x,
|
||||
y - this.state.selectedLinearElement.pointerOffset.y,
|
||||
this.getEffectiveGridSize(),
|
||||
);
|
||||
const draggingPoint =
|
||||
arrow.points[
|
||||
this.state.selectedLinearElement.pointerDownState.lastClickedPoint
|
||||
];
|
||||
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
|
||||
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
|
||||
LinearElementEditor.movePoints(
|
||||
arrow,
|
||||
this.scene,
|
||||
pointDraggingUpdates(
|
||||
startDragged ? [0] : endDragged ? [arrow.points.length - 1] : [],
|
||||
deltaX,
|
||||
deltaY,
|
||||
elementsMap,
|
||||
arrow,
|
||||
elements,
|
||||
this,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user