mirror of
https://github.com/excalidraw/excalidraw
synced 2025-07-25 13:58:22 +08:00
Include point updates after binding update
Fix point updates when endpoint dragged and opposite endpoint orbits centered focus point only for new arrows
This commit is contained in:
@ -11,11 +11,14 @@ import { type AppState } from "@excalidraw/excalidraw/types";
|
||||
import { arrayToMap, invariant, throttleRAF } from "@excalidraw/common";
|
||||
import { useCallback, useImperativeHandle, useRef } from "react";
|
||||
|
||||
import { isArrowElement, isBindableElement } from "@excalidraw/element";
|
||||
import {
|
||||
getGlobalFixedPointForBindableElement,
|
||||
isArrowElement,
|
||||
isBindableElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
isLineSegment,
|
||||
pointFrom,
|
||||
type GlobalPoint,
|
||||
type LineSegment,
|
||||
} from "@excalidraw/math";
|
||||
@ -94,9 +97,10 @@ const _renderBinding = (
|
||||
const bindable = elementsMap.get(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
const [x, y] = pointFrom<GlobalPoint>(
|
||||
bindable.x + bindable.width * binding.fixedPoint[0],
|
||||
bindable.y + bindable.height * binding.fixedPoint[1],
|
||||
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
bindable,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
context.save();
|
||||
@ -128,9 +132,10 @@ const _renderBindableBinding = (
|
||||
const bindable = elementsMap.get(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
const [x, y] = pointFrom<GlobalPoint>(
|
||||
bindable.x + bindable.width * binding.fixedPoint[0],
|
||||
bindable.y + bindable.height * binding.fixedPoint[1],
|
||||
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
bindable,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
context.save();
|
||||
|
@ -125,6 +125,9 @@ export const bindOrUnbindBindingElement = (
|
||||
draggingPoints: PointsPositionUpdates,
|
||||
scene: Scene,
|
||||
appState: AppState,
|
||||
opts?: {
|
||||
newArrow: boolean;
|
||||
},
|
||||
) => {
|
||||
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
|
||||
arrow,
|
||||
@ -132,9 +135,43 @@ export const bindOrUnbindBindingElement = (
|
||||
scene.getNonDeletedElementsMap(),
|
||||
scene.getNonDeletedElements(),
|
||||
appState,
|
||||
opts,
|
||||
);
|
||||
bindOrUnbindBindingElementEdge(arrow, start, "start", scene);
|
||||
bindOrUnbindBindingElementEdge(arrow, end, "end", scene);
|
||||
if (start.focusPoint || end.focusPoint) {
|
||||
// If the strategy dictates a focus point override, then
|
||||
// update the arrow points to point to the focus point.
|
||||
const updates: PointsPositionUpdates = new Map();
|
||||
|
||||
if (start.focusPoint) {
|
||||
updates.set(0, {
|
||||
point:
|
||||
updateBoundPoint(
|
||||
arrow,
|
||||
"startBinding",
|
||||
arrow.startBinding,
|
||||
start.element,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
) || arrow.points[0],
|
||||
});
|
||||
}
|
||||
|
||||
if (end.focusPoint) {
|
||||
updates.set(arrow.points.length - 1, {
|
||||
point:
|
||||
updateBoundPoint(
|
||||
arrow,
|
||||
"endBinding",
|
||||
arrow.endBinding,
|
||||
end.element,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
) || arrow.points[arrow.points.length - 1],
|
||||
});
|
||||
}
|
||||
|
||||
LinearElementEditor.movePoints(arrow, scene, updates);
|
||||
}
|
||||
};
|
||||
|
||||
const bindOrUnbindBindingElementEdge = (
|
||||
@ -193,6 +230,9 @@ const bindingStrategyForEndpointDragging = (
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
zoom: AppState["zoom"],
|
||||
globalBindMode?: AppState["bindMode"],
|
||||
opts?: {
|
||||
newArrow: boolean;
|
||||
},
|
||||
): { current: BindingStrategy; other: BindingStrategy } => {
|
||||
let current: BindingStrategy = { mode: undefined };
|
||||
let other: BindingStrategy = { mode: undefined };
|
||||
@ -233,10 +273,12 @@ const bindingStrategyForEndpointDragging = (
|
||||
current = {
|
||||
element: hovered,
|
||||
mode: "orbit",
|
||||
focusPoint: pointFrom<GlobalPoint>(
|
||||
hovered.x + hovered.width / 2,
|
||||
hovered.y + hovered.height / 2,
|
||||
),
|
||||
focusPoint: opts?.newArrow
|
||||
? pointFrom<GlobalPoint>(
|
||||
hovered.x + hovered.width / 2,
|
||||
hovered.y + hovered.height / 2,
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return { current, other };
|
||||
@ -273,10 +315,12 @@ const bindingStrategyForEndpointDragging = (
|
||||
current = {
|
||||
element: hovered,
|
||||
mode: "orbit",
|
||||
focusPoint: pointFrom<GlobalPoint>(
|
||||
hovered.x + hovered.width / 2,
|
||||
hovered.y + hovered.height / 2,
|
||||
),
|
||||
focusPoint: opts?.newArrow
|
||||
? pointFrom<GlobalPoint>(
|
||||
hovered.x + hovered.width / 2,
|
||||
hovered.y + hovered.height / 2,
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return { current, other };
|
||||
@ -302,6 +346,9 @@ const getBindingStrategyForDraggingBindingElementEndpoints = (
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
appState: AppState,
|
||||
opts?: {
|
||||
newArrow: boolean;
|
||||
},
|
||||
): { start: BindingStrategy; end: BindingStrategy } => {
|
||||
const globalBindMode = appState.bindMode || "focus";
|
||||
const startIdx = 0;
|
||||
@ -330,7 +377,18 @@ const getBindingStrategyForDraggingBindingElementEndpoints = (
|
||||
|
||||
return {
|
||||
start: hovered
|
||||
? { element: hovered, mode: hit ? "inside" : "outside" }
|
||||
? hit
|
||||
? { element: hovered, mode: "inside" }
|
||||
: opts?.newArrow
|
||||
? {
|
||||
element: hovered,
|
||||
mode: "orbit",
|
||||
focusPoint: pointFrom<GlobalPoint>(
|
||||
hovered.x + hovered.width / 2,
|
||||
hovered.y + hovered.height / 2,
|
||||
),
|
||||
}
|
||||
: { element: hovered, mode: "inside" }
|
||||
: { mode: undefined },
|
||||
end: { mode: undefined },
|
||||
};
|
||||
@ -372,6 +430,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints = (
|
||||
elements,
|
||||
appState.zoom,
|
||||
globalBindMode,
|
||||
opts,
|
||||
);
|
||||
|
||||
return { start: current, end: other };
|
||||
@ -393,6 +452,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints = (
|
||||
elements,
|
||||
appState.zoom,
|
||||
globalBindMode,
|
||||
opts,
|
||||
);
|
||||
|
||||
return { start: other, end: current };
|
||||
@ -560,21 +620,6 @@ export const bindBindingElement = (
|
||||
}
|
||||
};
|
||||
|
||||
export const isBindingElementSimpleAndAlreadyBound = (
|
||||
linearElement: NonDeleted<ExcalidrawArrowElement>,
|
||||
alreadyBoundToId: ExcalidrawBindableElement["id"] | undefined,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
): boolean => {
|
||||
return (
|
||||
alreadyBoundToId === bindableElement.id &&
|
||||
isBindingElementSimple(linearElement)
|
||||
);
|
||||
};
|
||||
|
||||
const isBindingElementSimple = (
|
||||
linearElement: NonDeleted<ExcalidrawArrowElement>,
|
||||
): boolean => linearElement.points.length < 3 && !isElbowArrow(linearElement);
|
||||
|
||||
export const unbindBindingElement = (
|
||||
arrow: NonDeleted<ExcalidrawArrowElement>,
|
||||
startOrEnd: "start" | "end",
|
||||
@ -1115,7 +1160,7 @@ export const snapToMid = (
|
||||
};
|
||||
|
||||
export const updateBoundPoint = (
|
||||
linearElement: NonDeleted<ExcalidrawArrowElement>,
|
||||
arrow: NonDeleted<ExcalidrawArrowElement>,
|
||||
startOrEnd: "startBinding" | "endBinding",
|
||||
binding: FixedPointBinding | null | undefined,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
@ -1124,36 +1169,37 @@ export const updateBoundPoint = (
|
||||
if (
|
||||
binding == null ||
|
||||
// We only need to update the other end if this is a 2 point line element
|
||||
(binding.elementId !== bindableElement.id &&
|
||||
linearElement.points.length > 2)
|
||||
(binding.elementId !== bindableElement.id && arrow.points.length > 2)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fixedPoint = normalizeFixedPoint(binding.fixedPoint);
|
||||
const globalMidPoint = elementCenterPoint(bindableElement, elementsMap);
|
||||
const global = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||
);
|
||||
const rotatedGlobal = pointRotateRads(
|
||||
global,
|
||||
globalMidPoint,
|
||||
bindableElement.angle,
|
||||
const global = getGlobalFixedPointForBindableElement(
|
||||
fixedPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
const element =
|
||||
arrow.points.length === 1
|
||||
? {
|
||||
...arrow,
|
||||
points: [arrow.points[0], arrow.points[0]],
|
||||
}
|
||||
: arrow;
|
||||
const maybeOutlineGlobal =
|
||||
binding.mode === "orbit"
|
||||
? getOutlineAvoidingPoint(
|
||||
linearElement,
|
||||
element,
|
||||
bindableElement,
|
||||
rotatedGlobal,
|
||||
startOrEnd === "startBinding" ? 0 : linearElement.points.length - 1,
|
||||
global,
|
||||
startOrEnd === "startBinding" ? 0 : arrow.points.length - 1,
|
||||
elementsMap,
|
||||
)
|
||||
: rotatedGlobal;
|
||||
: global;
|
||||
|
||||
return LinearElementEditor.pointFromAbsoluteCoords(
|
||||
linearElement,
|
||||
arrow,
|
||||
maybeOutlineGlobal,
|
||||
elementsMap,
|
||||
);
|
||||
|
@ -23,9 +23,9 @@ import {
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
bindingBorderTest,
|
||||
deconstructLinearOrFreeDrawElement,
|
||||
getHoveredElementForBinding,
|
||||
hitElementItself,
|
||||
isPathALoop,
|
||||
type Store,
|
||||
} from "@excalidraw/element";
|
||||
@ -42,6 +42,7 @@ import type {
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import {
|
||||
getGlobalFixedPointForBindableElement,
|
||||
getOutlineAvoidingPoint,
|
||||
isBindingEnabled,
|
||||
maybeSuggestBindingsForBindingElementAtCoords,
|
||||
@ -55,7 +56,12 @@ import {
|
||||
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import { isArrowElement, isBindingElement, isElbowArrow } from "./typeChecks";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBindingElement,
|
||||
isElbowArrow,
|
||||
isSimpleArrow,
|
||||
} from "./typeChecks";
|
||||
|
||||
import { ShapeCache, toggleLinePolygonState } from "./shape";
|
||||
|
||||
@ -1936,12 +1942,13 @@ const pointDraggingUpdates = (
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
appState: AppState,
|
||||
): PointsPositionUpdates => {
|
||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap, true);
|
||||
const hasMidPoints =
|
||||
selectedPointsIndices.filter(
|
||||
(_, idx) => idx > 0 && idx < element.points.length - 1,
|
||||
).length > 0;
|
||||
|
||||
return new Map(
|
||||
const updates = new Map(
|
||||
selectedPointsIndices.map((pointIndex) => {
|
||||
let newPointPosition: LocalPoint =
|
||||
pointIndex === lastClickedPoint
|
||||
@ -1958,14 +1965,10 @@ const pointDraggingUpdates = (
|
||||
);
|
||||
|
||||
if (
|
||||
isSimpleArrow(element) &&
|
||||
!hasMidPoints &&
|
||||
(pointIndex === 0 || pointIndex === element.points.length - 1)
|
||||
) {
|
||||
const [, , , , cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
elementsMap,
|
||||
true,
|
||||
);
|
||||
let newGlobalPointPosition = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + newPointPosition[0],
|
||||
@ -1988,12 +1991,12 @@ const pointDraggingUpdates = (
|
||||
);
|
||||
const otherPointInsideElement =
|
||||
!!hoveredElement &&
|
||||
hitElementItself({
|
||||
element: hoveredElement,
|
||||
point: otherGlobalPoint,
|
||||
!!bindingBorderTest(
|
||||
hoveredElement,
|
||||
otherGlobalPoint,
|
||||
elementsMap,
|
||||
threshold: 0,
|
||||
});
|
||||
appState.zoom,
|
||||
);
|
||||
|
||||
if (
|
||||
isBindingEnabled(appState) &&
|
||||
@ -2029,4 +2032,60 @@ const pointDraggingUpdates = (
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
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,
|
||||
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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return updates;
|
||||
};
|
||||
|
@ -276,6 +276,7 @@ export const actionFinalize = register({
|
||||
]),
|
||||
scene,
|
||||
appState,
|
||||
{ newArrow: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -239,6 +239,7 @@ import {
|
||||
calculateFixedPointForNonElbowArrowBinding,
|
||||
normalizeFixedPoint,
|
||||
bindOrUnbindBindingElement,
|
||||
updateBoundPoint,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||
@ -6236,40 +6237,31 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const startElement = this.scene.getElement(
|
||||
multiElement.startBinding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
const avoidancePoint = getOutlineAvoidingPoint(
|
||||
const localPoint = updateBoundPoint(
|
||||
multiElement,
|
||||
"startBinding",
|
||||
multiElement.startBinding,
|
||||
startElement,
|
||||
startPoint,
|
||||
0,
|
||||
elementsMap,
|
||||
);
|
||||
if (!pointsEqual(startPoint, avoidancePoint)) {
|
||||
const avoidancePoint = localPoint
|
||||
? LinearElementEditor.getPointGlobalCoordinates(
|
||||
multiElement,
|
||||
localPoint,
|
||||
elementsMap,
|
||||
)
|
||||
: null;
|
||||
if (avoidancePoint && !pointsEqual(startPoint, avoidancePoint)) {
|
||||
const point = LinearElementEditor.pointFromAbsoluteCoords(
|
||||
multiElement,
|
||||
avoidancePoint,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
multiElement,
|
||||
this.scene,
|
||||
new Map([
|
||||
[
|
||||
0,
|
||||
{
|
||||
point: LinearElementEditor.pointFromAbsoluteCoords(
|
||||
multiElement,
|
||||
avoidancePoint,
|
||||
elementsMap,
|
||||
),
|
||||
},
|
||||
],
|
||||
]),
|
||||
{
|
||||
startBinding: {
|
||||
...multiElement.startBinding,
|
||||
...calculateFixedPointForNonElbowArrowBinding(
|
||||
multiElement,
|
||||
startElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
),
|
||||
},
|
||||
},
|
||||
new Map([[0, { point }]]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -8168,6 +8160,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
]),
|
||||
this.scene,
|
||||
this.state,
|
||||
{ newArrow: true },
|
||||
);
|
||||
}
|
||||
this.setState((prevState) => {
|
||||
|
Reference in New Issue
Block a user