mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: Elbow arrow z-index binding (#9067)
This commit is contained in:
parent
86c67bd37f
commit
302664e500
8 changed files with 165 additions and 31 deletions
|
@ -1605,6 +1605,8 @@ export const actionChangeArrowType = register({
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
const endHoveredElement =
|
const endHoveredElement =
|
||||||
!newElement.endBinding &&
|
!newElement.endBinding &&
|
||||||
|
@ -1613,6 +1615,8 @@ export const actionChangeArrowType = register({
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
const startElement = startHoveredElement
|
const startElement = startHoveredElement
|
||||||
? startHoveredElement
|
? startHoveredElement
|
||||||
|
|
|
@ -5770,7 +5770,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (editingLinearElement?.lastUncommittedPoint != null) {
|
if (editingLinearElement?.lastUncommittedPoint != null) {
|
||||||
this.maybeSuggestBindingAtCursor(scenePointer);
|
this.maybeSuggestBindingAtCursor(
|
||||||
|
scenePointer,
|
||||||
|
editingLinearElement.elbowed,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// causes stack overflow if not sync
|
// causes stack overflow if not sync
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
|
@ -5790,7 +5793,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.state.startBoundElement,
|
this.state.startBoundElement,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.maybeSuggestBindingAtCursor(scenePointer);
|
this.maybeSuggestBindingAtCursor(scenePointer, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7728,6 +7731,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
isElbowArrow(element),
|
isElbowArrow(element),
|
||||||
|
isElbowArrow(element),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.scene.insertElement(element);
|
this.scene.insertElement(element);
|
||||||
|
@ -10005,15 +10009,20 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private maybeSuggestBindingAtCursor = (pointerCoords: {
|
private maybeSuggestBindingAtCursor = (
|
||||||
x: number;
|
pointerCoords: {
|
||||||
y: number;
|
x: number;
|
||||||
}): void => {
|
y: number;
|
||||||
|
},
|
||||||
|
considerAll: boolean,
|
||||||
|
): void => {
|
||||||
const hoveredBindableElement = getHoveredElementForBinding(
|
const hoveredBindableElement = getHoveredElementForBinding(
|
||||||
pointerCoords,
|
pointerCoords,
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
|
false,
|
||||||
|
considerAll,
|
||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
suggestedBindings:
|
suggestedBindings:
|
||||||
|
@ -10043,7 +10052,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
isArrowElement(linearElement) && isElbowArrow(linearElement),
|
isElbowArrow(linearElement),
|
||||||
|
isElbowArrow(linearElement),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
hoveredBindableElement != null &&
|
hoveredBindableElement != null &&
|
||||||
|
|
|
@ -49,7 +49,11 @@ import type { ElementUpdate } from "./mutateElement";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import type Scene from "../scene/Scene";
|
import type Scene from "../scene/Scene";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { arrayToMap, tupleToCoors } from "../utils";
|
import {
|
||||||
|
arrayToMap,
|
||||||
|
isBindingFallthroughEnabled,
|
||||||
|
tupleToCoors,
|
||||||
|
} from "../utils";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
|
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
|
||||||
|
@ -75,6 +79,7 @@ import {
|
||||||
clamp,
|
clamp,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { segmentIntersectRectangleElement } from "../../utils/geometry/shape";
|
import { segmentIntersectRectangleElement } from "../../utils/geometry/shape";
|
||||||
|
import { getElementsAtPosition } from "../scene/comparisons";
|
||||||
|
|
||||||
export type SuggestedBinding =
|
export type SuggestedBinding =
|
||||||
| NonDeleted<ExcalidrawBindableElement>
|
| NonDeleted<ExcalidrawBindableElement>
|
||||||
|
@ -425,7 +430,8 @@ export const maybeBindLinearElement = (
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
isElbowArrow(linearElement) && isElbowArrow(linearElement),
|
isElbowArrow(linearElement),
|
||||||
|
isElbowArrow(linearElement),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hoveredElement !== null) {
|
if (hoveredElement !== null) {
|
||||||
|
@ -558,7 +564,64 @@ export const getHoveredElementForBinding = (
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
fullShape?: boolean,
|
fullShape?: boolean,
|
||||||
|
considerAllElements?: boolean,
|
||||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||||
|
if (considerAllElements) {
|
||||||
|
let cullRest = false;
|
||||||
|
const candidateElements = getElementsAtPosition(
|
||||||
|
elements,
|
||||||
|
(element) =>
|
||||||
|
isBindableElement(element, false) &&
|
||||||
|
bindingBorderTest(
|
||||||
|
element,
|
||||||
|
pointerCoords,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
(fullShape ||
|
||||||
|
!isBindingFallthroughEnabled(
|
||||||
|
element as ExcalidrawBindableElement,
|
||||||
|
)) &&
|
||||||
|
// disable fullshape snapping for frame elements so we
|
||||||
|
// can bind to frame children
|
||||||
|
!isFrameLikeElement(element),
|
||||||
|
),
|
||||||
|
).filter((element) => {
|
||||||
|
if (cullRest) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isBindingFallthroughEnabled(element as ExcalidrawBindableElement)) {
|
||||||
|
cullRest = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}) as NonDeleted<ExcalidrawBindableElement>[] | null;
|
||||||
|
|
||||||
|
// Return early if there are no candidates or just one candidate
|
||||||
|
if (!candidateElements || candidateElements.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateElements.length === 1) {
|
||||||
|
return candidateElements[0] as NonDeleted<ExcalidrawBindableElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the shape with the border being tested (if any)
|
||||||
|
const borderTestElements = candidateElements.filter((element) =>
|
||||||
|
bindingBorderTest(element, pointerCoords, elementsMap, zoom, false),
|
||||||
|
);
|
||||||
|
if (borderTestElements.length === 1) {
|
||||||
|
return borderTestElements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer smaller shapes
|
||||||
|
return candidateElements
|
||||||
|
.sort(
|
||||||
|
(a, b) => b.width ** 2 + b.height ** 2 - (a.width ** 2 + a.height ** 2),
|
||||||
|
)
|
||||||
|
.pop() as NonDeleted<ExcalidrawBindableElement>;
|
||||||
|
}
|
||||||
|
|
||||||
const hoveredElement = getElementAtPosition(
|
const hoveredElement = getElementAtPosition(
|
||||||
elements,
|
elements,
|
||||||
(element) =>
|
(element) =>
|
||||||
|
@ -570,7 +633,8 @@ export const getHoveredElementForBinding = (
|
||||||
zoom,
|
zoom,
|
||||||
// disable fullshape snapping for frame elements so we
|
// disable fullshape snapping for frame elements so we
|
||||||
// can bind to frame children
|
// can bind to frame children
|
||||||
fullShape && !isFrameLikeElement(element),
|
(fullShape || !isBindingFallthroughEnabled(element)) &&
|
||||||
|
!isFrameLikeElement(element),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1220,6 +1284,8 @@ const getElligibleElementForBindingElement = (
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
zoom,
|
zoom,
|
||||||
|
isElbowArrow(linearElement),
|
||||||
|
isElbowArrow(linearElement),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,11 +20,11 @@ import {
|
||||||
bindPointToSnapToElementOutline,
|
bindPointToSnapToElementOutline,
|
||||||
distanceToBindableElement,
|
distanceToBindableElement,
|
||||||
avoidRectangularCorner,
|
avoidRectangularCorner,
|
||||||
getHoveredElementForBinding,
|
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
getHeadingForElbowArrowSnap,
|
getHeadingForElbowArrowSnap,
|
||||||
getGlobalFixedPointForBindableElement,
|
getGlobalFixedPointForBindableElement,
|
||||||
snapToMid,
|
snapToMid,
|
||||||
|
getHoveredElementForBinding,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { Heading } from "./heading";
|
import type { Heading } from "./heading";
|
||||||
|
@ -872,6 +872,8 @@ export const updateElbowArrowPoints = (
|
||||||
updates: {
|
updates: {
|
||||||
points?: readonly LocalPoint[];
|
points?: readonly LocalPoint[];
|
||||||
fixedSegments?: FixedSegment[] | null;
|
fixedSegments?: FixedSegment[] | null;
|
||||||
|
startBinding?: FixedPointBinding | null;
|
||||||
|
endBinding?: FixedPointBinding | null;
|
||||||
},
|
},
|
||||||
options?: {
|
options?: {
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
|
@ -953,17 +955,48 @@ export const updateElbowArrowPoints = (
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
...rest
|
...rest
|
||||||
} = getElbowArrowData(arrow, elementsMap, updatedPoints, options);
|
} = getElbowArrowData(
|
||||||
|
{
|
||||||
|
x: arrow.x,
|
||||||
|
y: arrow.y,
|
||||||
|
startBinding:
|
||||||
|
typeof updates.startBinding !== "undefined"
|
||||||
|
? updates.startBinding
|
||||||
|
: arrow.startBinding,
|
||||||
|
endBinding:
|
||||||
|
typeof updates.endBinding !== "undefined"
|
||||||
|
? updates.endBinding
|
||||||
|
: arrow.endBinding,
|
||||||
|
startArrowhead: arrow.startArrowhead,
|
||||||
|
endArrowhead: arrow.endArrowhead,
|
||||||
|
},
|
||||||
|
elementsMap,
|
||||||
|
updatedPoints,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
||||||
|
|
||||||
////
|
////
|
||||||
// 1. Renormalize the arrow
|
// 1. Renormalize the arrow
|
||||||
////
|
////
|
||||||
if (!updates.points && !updates.fixedSegments) {
|
if (
|
||||||
|
!updates.points &&
|
||||||
|
!updates.fixedSegments &&
|
||||||
|
!updates.startBinding &&
|
||||||
|
!updates.endBinding
|
||||||
|
) {
|
||||||
return handleSegmentRenormalization(arrow, elementsMap);
|
return handleSegmentRenormalization(arrow, elementsMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Short circuit on no-op to avoid huge performance hit
|
||||||
|
if (
|
||||||
|
updates.startBinding === arrow.startBinding &&
|
||||||
|
updates.endBinding === arrow.endBinding
|
||||||
|
) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
////
|
////
|
||||||
// 2. Just normal elbow arrow things
|
// 2. Just normal elbow arrow things
|
||||||
////
|
////
|
||||||
|
@ -1010,6 +1043,7 @@ export const updateElbowArrowPoints = (
|
||||||
|
|
||||||
////
|
////
|
||||||
// 5. Handle resize
|
// 5. Handle resize
|
||||||
|
////
|
||||||
if (updates.points && updates.fixedSegments) {
|
if (updates.points && updates.fixedSegments) {
|
||||||
return updates;
|
return updates;
|
||||||
}
|
}
|
||||||
|
@ -2120,6 +2154,7 @@ const getHoveredElements = (
|
||||||
nonDeletedSceneElementsMap,
|
nonDeletedSceneElementsMap,
|
||||||
zoom,
|
zoom,
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
getHoveredElementForBinding(
|
getHoveredElementForBinding(
|
||||||
tupleToCoors(origEndGlobalPoint),
|
tupleToCoors(origEndGlobalPoint),
|
||||||
|
@ -2127,6 +2162,7 @@ const getHoveredElements = (
|
||||||
nonDeletedSceneElementsMap,
|
nonDeletedSceneElementsMap,
|
||||||
zoom,
|
zoom,
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
|
@ -444,6 +444,8 @@ export class LinearElementEditor {
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
|
isElbowArrow(element),
|
||||||
|
isElbowArrow(element),
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
@ -796,6 +798,7 @@ export class LinearElementEditor {
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
app.state.zoom,
|
app.state.zoom,
|
||||||
|
linearElementEditor.elbowed,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -33,13 +33,16 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
|
|
||||||
// casting to any because can't use `in` operator
|
// casting to any because can't use `in` operator
|
||||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||||
const { points, fixedSegments, fileId } = updates as any;
|
const { points, fixedSegments, fileId, startBinding, endBinding } =
|
||||||
|
updates as any;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isElbowArrow(element) &&
|
isElbowArrow(element) &&
|
||||||
(Object.keys(updates).length === 0 || // normalization case
|
(Object.keys(updates).length === 0 || // normalization case
|
||||||
typeof points !== "undefined" || // repositioning
|
typeof points !== "undefined" || // repositioning
|
||||||
typeof fixedSegments !== "undefined") // segment fixing
|
typeof fixedSegments !== "undefined" || // segment fixing
|
||||||
|
typeof startBinding !== "undefined" ||
|
||||||
|
typeof endBinding !== "undefined") // manual binding to element
|
||||||
) {
|
) {
|
||||||
const elementsMap = toBrandedType<SceneElementsMap>(
|
const elementsMap = toBrandedType<SceneElementsMap>(
|
||||||
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
|
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
|
||||||
|
@ -58,6 +61,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
{
|
{
|
||||||
fixedSegments,
|
fixedSegments,
|
||||||
points,
|
points,
|
||||||
|
startBinding,
|
||||||
|
endBinding,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isDragging: options?.isDragging,
|
isDragging: options?.isDragging,
|
||||||
|
|
|
@ -75,20 +75,23 @@ export const getElementsAtPosition = (
|
||||||
isAtPositionFn: (element: NonDeletedExcalidrawElement) => boolean,
|
isAtPositionFn: (element: NonDeletedExcalidrawElement) => boolean,
|
||||||
) => {
|
) => {
|
||||||
const iframeLikes: ExcalidrawIframeElement[] = [];
|
const iframeLikes: ExcalidrawIframeElement[] = [];
|
||||||
// The parameter elements comes ordered from lower z-index to higher.
|
const elementsAtPosition: NonDeletedExcalidrawElement[] = [];
|
||||||
// We want to preserve that order on the returned array.
|
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||||
// Exception being embeddables which should be on top of everything else in
|
// because array is ordered from lower z-index to highest and we want element z-index
|
||||||
// terms of hit testing.
|
// with higher z-index
|
||||||
const elsAtPos = elements.filter((element) => {
|
for (let index = elements.length - 1; index >= 0; --index) {
|
||||||
const hit = !element.isDeleted && isAtPositionFn(element);
|
const element = elements[index];
|
||||||
if (hit) {
|
if (element.isDeleted) {
|
||||||
if (isIframeElement(element)) {
|
continue;
|
||||||
iframeLikes.push(element);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
if (isIframeElement(element)) {
|
||||||
});
|
iframeLikes.push(element);
|
||||||
return elsAtPos.concat(iframeLikes);
|
continue;
|
||||||
|
}
|
||||||
|
if (isAtPositionFn(element)) {
|
||||||
|
elementsAtPosition.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elementsAtPosition.concat(iframeLikes);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,11 @@ import {
|
||||||
isDarwin,
|
isDarwin,
|
||||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import type { FontFamilyValues, FontString } from "./element/types";
|
import type {
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
FontFamilyValues,
|
||||||
|
FontString,
|
||||||
|
} from "./element/types";
|
||||||
import type {
|
import type {
|
||||||
ActiveTool,
|
ActiveTool,
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -543,6 +547,9 @@ export const isTransparent = (color: string) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isBindingFallthroughEnabled = (el: ExcalidrawBindableElement) =>
|
||||||
|
el.fillStyle !== "solid" || isTransparent(el.backgroundColor);
|
||||||
|
|
||||||
export type ResolvablePromise<T> = Promise<T> & {
|
export type ResolvablePromise<T> = Promise<T> & {
|
||||||
resolve: [T] extends [undefined]
|
resolve: [T] extends [undefined]
|
||||||
? (value?: MaybePromise<Awaited<T>>) => void
|
? (value?: MaybePromise<Awaited<T>>) => void
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue