5852d0d410
* fix(arrow): Overlap arrow behavior Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Lint Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat(editor): reduce binding gap (#10739) * feat(editor): reduce binding gap to 7px * feat(editor): reduce binding gap to 5px * feat(editor): reduce binding gap to 3px * go back to 5px * update tests * feat: Simplified update bind points Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Remove non-needed export Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix. Possessed arrows #1 Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Focus point projection stabilization Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Remove arrow stability hack Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Unbound other endpoint Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat(editor): visualize binding midpoints + support for simple arrows (#10611) * feat: Force exact center focus point When the projected point is close to center snap it to the exact center. Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Snap to center around side mid point. Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Trigger CI * fix: Midpoint outline focus point Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Dragging existing arrow reset focus point on outline Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Midpoint indicator Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Rotated mid points Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: No hole Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Cache hits and scene lookups Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove debug Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Consider hit threshold and inside override too Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Increase outline midpoint sticky distance Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Don't show midpoint indicator when no snapping is possible Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Indicate lock-in Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove Map caching Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: incorrect threshold Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: threshold setting Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Hit caching Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Simple arrow mid point selection inconsistency Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: cache override Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Precise know dragging with midpoint refactor Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fear: Frame support Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Crossing arrow won't trigger mid point Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Arrow creation point highlight Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Restore types & tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Restore restore.ts Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: restore.ts Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Elbow arrows reliably highlight center point Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Highlight point ordering Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Bind with focus point across shape Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Lint * fix: Midpoint and binding alignment Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Indicator color Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: More knob tuning Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Radius Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * simplify point indicators --------- Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Snapshots Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Target point selection Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove non-needed change Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Try again removing non-needed modification Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Inside-inside binding arrow endpoint drag trigger focus point editor Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Area based edge case Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Overlapping new arrow jump Signed-off-by: Mark Tolmacs <mark@lazycat.hu> --------- Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com>
559 lines
15 KiB
TypeScript
559 lines
15 KiB
TypeScript
import { pointDistance, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
|
import { invariant } from "@excalidraw/common";
|
|
|
|
import type { AppState, NullableGridSize } from "@excalidraw/excalidraw/types";
|
|
|
|
import {
|
|
bindBindingElement,
|
|
calculateFixedPointForNonElbowArrowBinding,
|
|
FOCUS_POINT_SIZE,
|
|
getBindingGap,
|
|
getGlobalFixedPointForBindableElement,
|
|
isBindingEnabled,
|
|
maxBindingDistance_simple,
|
|
unbindBindingElement,
|
|
updateBoundPoint,
|
|
} from "../binding";
|
|
import {
|
|
isBindableElement,
|
|
isBindingElement,
|
|
isElbowArrow,
|
|
} from "../typeChecks";
|
|
import { LinearElementEditor } from "../linearElementEditor";
|
|
import { getHoveredElementForFocusPoint, hitElementItself } from "../collision";
|
|
import { moveArrowAboveBindable } from "../zindex";
|
|
|
|
import type {
|
|
ElementsMap,
|
|
ExcalidrawArrowElement,
|
|
ExcalidrawBindableElement,
|
|
NonDeletedSceneElementsMap,
|
|
PointsPositionUpdates,
|
|
} from "../types";
|
|
|
|
import type { Scene } from "../Scene";
|
|
|
|
export const isFocusPointVisible = (
|
|
focusPoint: GlobalPoint,
|
|
arrow: ExcalidrawArrowElement,
|
|
bindableElement: ExcalidrawBindableElement,
|
|
elementsMap: ElementsMap,
|
|
appState: {
|
|
isBindingEnabled: AppState["isBindingEnabled"];
|
|
zoom: AppState["zoom"];
|
|
},
|
|
startOrEnd: "start" | "end",
|
|
ignoreOverlap = false,
|
|
): boolean => {
|
|
// No focus point management for elbow arrows, because elbow arrows
|
|
// always have their focus point at the arrow point itself
|
|
if (
|
|
isElbowArrow(arrow) ||
|
|
!isBindingEnabled(appState) ||
|
|
arrow.points.length !== 2
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// Avoid showing the focus point indicator if the focus point is essentially
|
|
// on top of the arrow point it belongs to itself, if not ignoring specifically
|
|
if (!ignoreOverlap) {
|
|
const associatedPointIdx =
|
|
arrow.startBinding?.elementId === bindableElement.id
|
|
? 0
|
|
: arrow.points.length - 1;
|
|
const associatedArrowPoint =
|
|
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
arrow,
|
|
associatedPointIdx,
|
|
elementsMap,
|
|
);
|
|
|
|
if (
|
|
pointDistance(focusPoint, associatedArrowPoint) <
|
|
(FOCUS_POINT_SIZE * 1.5) / appState.zoom.value
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const arrowPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
arrow,
|
|
startOrEnd === "end" ? arrow.points.length - 1 : 0,
|
|
elementsMap,
|
|
);
|
|
|
|
// Check if the focus point is within the element's shape bounds
|
|
// Endpoint dragging takes precedence
|
|
return (
|
|
pointDistance(focusPoint, arrowPoint) >=
|
|
(FOCUS_POINT_SIZE * 1.5) / appState.zoom.value &&
|
|
hitElementItself({
|
|
element: bindableElement,
|
|
elementsMap,
|
|
point: focusPoint,
|
|
threshold: getBindingGap(bindableElement, arrow),
|
|
overrideShouldTestInside: true,
|
|
})
|
|
);
|
|
};
|
|
|
|
// Updates the arrow endpoints in "orbit" configuration
|
|
const focusPointUpdate = (
|
|
arrow: ExcalidrawArrowElement,
|
|
bindableElement: ExcalidrawBindableElement | null,
|
|
isStartBinding: boolean,
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
scene: Scene,
|
|
appState: AppState,
|
|
switchToInsideBinding: boolean,
|
|
) => {
|
|
const pointUpdates = new Map();
|
|
|
|
const bindingField = isStartBinding ? "startBinding" : "endBinding";
|
|
const adjacentBindingField = isStartBinding ? "endBinding" : "startBinding";
|
|
let currentBinding = arrow[bindingField];
|
|
let adjacentBinding = arrow[adjacentBindingField];
|
|
|
|
// Update the dragged focus point related end
|
|
if (currentBinding && bindableElement) {
|
|
// Update the targeted bindings
|
|
const boundToSameElement =
|
|
bindableElement &&
|
|
adjacentBinding &&
|
|
currentBinding.elementId === adjacentBinding.elementId;
|
|
if (switchToInsideBinding || boundToSameElement) {
|
|
currentBinding = {
|
|
...currentBinding,
|
|
mode: "inside",
|
|
};
|
|
} else {
|
|
currentBinding = {
|
|
...currentBinding,
|
|
mode: "orbit",
|
|
};
|
|
}
|
|
|
|
const pointIndex = isStartBinding ? 0 : arrow.points.length - 1;
|
|
const newPoint = updateBoundPoint(
|
|
arrow,
|
|
bindingField as "startBinding" | "endBinding",
|
|
currentBinding,
|
|
bindableElement,
|
|
elementsMap,
|
|
true,
|
|
);
|
|
|
|
if (newPoint) {
|
|
pointUpdates.set(pointIndex, { point: newPoint });
|
|
}
|
|
}
|
|
|
|
// Also update the adjacent end if it has a binding
|
|
if (adjacentBinding && adjacentBinding.mode === "orbit") {
|
|
const adjacentBindableElement = elementsMap.get(
|
|
adjacentBinding.elementId,
|
|
) as ExcalidrawBindableElement;
|
|
|
|
if (
|
|
adjacentBindableElement &&
|
|
isBindableElement(adjacentBindableElement) &&
|
|
isBindingEnabled(appState)
|
|
) {
|
|
// Same shape bound on both ends
|
|
const boundToSameElementAfterUpdate =
|
|
bindableElement && adjacentBinding.elementId === bindableElement.id;
|
|
if (switchToInsideBinding || boundToSameElementAfterUpdate) {
|
|
adjacentBinding = {
|
|
...adjacentBinding,
|
|
mode: "inside",
|
|
};
|
|
} else {
|
|
adjacentBinding = {
|
|
...adjacentBinding,
|
|
mode: "orbit",
|
|
};
|
|
}
|
|
|
|
const adjacentPointIndex = isStartBinding ? arrow.points.length - 1 : 0;
|
|
const adjacentNewPoint = updateBoundPoint(
|
|
arrow,
|
|
adjacentBindingField,
|
|
adjacentBinding,
|
|
adjacentBindableElement,
|
|
elementsMap,
|
|
);
|
|
|
|
if (adjacentNewPoint) {
|
|
pointUpdates.set(adjacentPointIndex, {
|
|
point: adjacentNewPoint,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pointUpdates.size > 0) {
|
|
LinearElementEditor.movePoints(arrow, scene, pointUpdates, {
|
|
[bindingField]: currentBinding,
|
|
[adjacentBindingField]: adjacentBinding,
|
|
});
|
|
}
|
|
};
|
|
|
|
export const handleFocusPointDrag = (
|
|
linearElementEditor: LinearElementEditor,
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
pointerCoords: { x: number; y: number },
|
|
scene: Scene,
|
|
appState: AppState,
|
|
gridSize: NullableGridSize,
|
|
switchToInsideBinding: boolean,
|
|
) => {
|
|
const arrow = LinearElementEditor.getElement(
|
|
linearElementEditor.elementId,
|
|
elementsMap,
|
|
) as any;
|
|
|
|
// Sanity checks
|
|
if (
|
|
!arrow ||
|
|
!isBindingElement(arrow) ||
|
|
isElbowArrow(arrow) ||
|
|
!linearElementEditor.hoveredFocusPointBinding ||
|
|
!linearElementEditor.draggedFocusPointBinding
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const isStartBinding =
|
|
linearElementEditor.draggedFocusPointBinding === "start";
|
|
const binding = isStartBinding ? arrow.startBinding : arrow.endBinding;
|
|
const { x: offsetX, y: offsetY } = linearElementEditor.pointerOffset;
|
|
const point = pointFrom<GlobalPoint>(
|
|
pointerCoords.x - offsetX,
|
|
pointerCoords.y - offsetY,
|
|
);
|
|
const bindingField = isStartBinding ? "startBinding" : "endBinding";
|
|
const hit = getHoveredElementForFocusPoint(
|
|
point,
|
|
arrow,
|
|
scene.getNonDeletedElements(),
|
|
elementsMap,
|
|
maxBindingDistance_simple(appState.zoom),
|
|
);
|
|
|
|
// Hovering a bindable element
|
|
if (hit && isBindingEnabled(appState)) {
|
|
// Break existing binding if bound to another shape or if binding is disabled
|
|
if (arrow[bindingField] && hit.id !== binding?.elementId) {
|
|
unbindBindingElement(
|
|
arrow,
|
|
linearElementEditor.draggedFocusPointBinding,
|
|
scene,
|
|
);
|
|
}
|
|
|
|
// Handle binding mode switch
|
|
const newMode =
|
|
switchToInsideBinding && arrow[bindingField]?.mode === "orbit"
|
|
? "inside"
|
|
: !switchToInsideBinding && arrow[bindingField]?.mode === "inside"
|
|
? "orbit"
|
|
: null;
|
|
|
|
// If no existing binding, create it
|
|
if (!arrow[bindingField] || newMode) {
|
|
// Create a new binding if none exists
|
|
bindBindingElement(
|
|
arrow,
|
|
hit,
|
|
newMode || "orbit",
|
|
linearElementEditor.draggedFocusPointBinding,
|
|
scene,
|
|
point,
|
|
);
|
|
}
|
|
|
|
// Update the binding's fixed point
|
|
scene.mutateElement(arrow, {
|
|
[bindingField]: {
|
|
...arrow[bindingField],
|
|
elementId: hit.id,
|
|
mode: newMode || arrow[bindingField]?.mode || "orbit",
|
|
...calculateFixedPointForNonElbowArrowBinding(
|
|
arrow,
|
|
hit,
|
|
linearElementEditor.draggedFocusPointBinding,
|
|
elementsMap,
|
|
point,
|
|
),
|
|
},
|
|
});
|
|
} else {
|
|
// Not hovering any bindable element, move the arrow endpoint
|
|
const pointUpdates: PointsPositionUpdates = new Map();
|
|
const pointIndex = isStartBinding ? 0 : arrow.points.length - 1;
|
|
pointUpdates.set(pointIndex, {
|
|
point: LinearElementEditor.createPointAt(
|
|
arrow,
|
|
elementsMap,
|
|
point[0],
|
|
point[1],
|
|
gridSize,
|
|
),
|
|
});
|
|
LinearElementEditor.movePoints(arrow, scene, pointUpdates);
|
|
if (arrow[bindingField]) {
|
|
unbindBindingElement(arrow, isStartBinding ? "start" : "end", scene);
|
|
}
|
|
}
|
|
|
|
// Update the arrow endpoints
|
|
focusPointUpdate(
|
|
arrow,
|
|
hit,
|
|
isStartBinding,
|
|
elementsMap,
|
|
scene,
|
|
appState,
|
|
switchToInsideBinding,
|
|
);
|
|
|
|
if (hit && isBindingEnabled(appState)) {
|
|
moveArrowAboveBindable(
|
|
point,
|
|
arrow,
|
|
scene.getElementsIncludingDeleted(),
|
|
elementsMap,
|
|
scene,
|
|
hit,
|
|
);
|
|
}
|
|
};
|
|
|
|
export const handleFocusPointPointerDown = (
|
|
arrow: ExcalidrawArrowElement,
|
|
pointerDownState: { origin: { x: number; y: number } },
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
appState: AppState,
|
|
): {
|
|
hitFocusPoint: "start" | "end" | null;
|
|
pointerOffset: { x: number; y: number };
|
|
} => {
|
|
const pointerPos = pointFrom(
|
|
pointerDownState.origin.x,
|
|
pointerDownState.origin.y,
|
|
);
|
|
const hitThreshold = (FOCUS_POINT_SIZE * 1.5) / appState.zoom.value;
|
|
|
|
// Check start binding focus point
|
|
if (arrow.startBinding?.elementId) {
|
|
const bindableElement = elementsMap.get(arrow.startBinding.elementId);
|
|
if (
|
|
bindableElement &&
|
|
isBindableElement(bindableElement) &&
|
|
!bindableElement.isDeleted
|
|
) {
|
|
const focusPoint = getGlobalFixedPointForBindableElement(
|
|
arrow.startBinding.fixedPoint,
|
|
bindableElement,
|
|
elementsMap,
|
|
);
|
|
if (
|
|
isFocusPointVisible(
|
|
focusPoint,
|
|
arrow,
|
|
bindableElement,
|
|
elementsMap,
|
|
appState,
|
|
"start",
|
|
) &&
|
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
|
) {
|
|
return {
|
|
hitFocusPoint: "start",
|
|
pointerOffset: {
|
|
x: pointerPos[0] - focusPoint[0],
|
|
y: pointerPos[1] - focusPoint[1],
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check end binding focus point (only if start not already hit)
|
|
if (arrow.endBinding?.elementId) {
|
|
const bindableElement = elementsMap.get(arrow.endBinding.elementId);
|
|
if (
|
|
bindableElement &&
|
|
isBindableElement(bindableElement) &&
|
|
!bindableElement.isDeleted
|
|
) {
|
|
const focusPoint = getGlobalFixedPointForBindableElement(
|
|
arrow.endBinding.fixedPoint,
|
|
bindableElement,
|
|
elementsMap,
|
|
);
|
|
if (
|
|
isFocusPointVisible(
|
|
focusPoint,
|
|
arrow,
|
|
bindableElement,
|
|
elementsMap,
|
|
appState,
|
|
"end",
|
|
) &&
|
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
|
) {
|
|
return {
|
|
hitFocusPoint: "end",
|
|
pointerOffset: {
|
|
x: pointerPos[0] - focusPoint[0],
|
|
y: pointerPos[1] - focusPoint[1],
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
hitFocusPoint: null,
|
|
pointerOffset: { x: 0, y: 0 },
|
|
};
|
|
};
|
|
|
|
export const handleFocusPointPointerUp = (
|
|
linearElementEditor: LinearElementEditor,
|
|
scene: Scene,
|
|
) => {
|
|
invariant(
|
|
linearElementEditor.draggedFocusPointBinding,
|
|
"Must have a dragged focus point at pointer release",
|
|
);
|
|
|
|
const arrow = LinearElementEditor.getElement<ExcalidrawArrowElement>(
|
|
linearElementEditor.elementId,
|
|
scene.getNonDeletedElementsMap(),
|
|
);
|
|
invariant(arrow, "Arrow must be in the scene");
|
|
|
|
// Clean up
|
|
const bindingKey =
|
|
linearElementEditor.draggedFocusPointBinding === "start"
|
|
? "startBinding"
|
|
: "endBinding";
|
|
const otherBindingKey =
|
|
linearElementEditor.draggedFocusPointBinding === "start"
|
|
? "endBinding"
|
|
: "startBinding";
|
|
const boundElementId = arrow[bindingKey]?.elementId;
|
|
const otherBoundElementId = arrow[otherBindingKey]?.elementId;
|
|
const oldBoundElement =
|
|
boundElementId &&
|
|
scene
|
|
.getNonDeletedElements()
|
|
.find(
|
|
(element) =>
|
|
element.id !== boundElementId &&
|
|
element.id !== otherBoundElementId &&
|
|
isBindableElement(element) &&
|
|
element.boundElements?.find(({ id }) => id === arrow.id),
|
|
);
|
|
if (oldBoundElement) {
|
|
scene.mutateElement(oldBoundElement, {
|
|
boundElements: oldBoundElement.boundElements?.filter(
|
|
({ id }) => id !== arrow.id,
|
|
),
|
|
});
|
|
}
|
|
|
|
// Record the new bound element
|
|
const boundElement =
|
|
boundElementId && scene.getNonDeletedElementsMap().get(boundElementId);
|
|
if (boundElement) {
|
|
scene.mutateElement(boundElement, {
|
|
boundElements: [
|
|
...(boundElement.boundElements || [])?.filter(
|
|
({ id }) => id !== arrow.id,
|
|
),
|
|
{
|
|
id: arrow.id,
|
|
type: "arrow",
|
|
},
|
|
],
|
|
});
|
|
}
|
|
};
|
|
|
|
export const handleFocusPointHover = (
|
|
arrow: ExcalidrawArrowElement,
|
|
scenePointerX: number,
|
|
scenePointerY: number,
|
|
scene: Scene,
|
|
appState: AppState,
|
|
): "start" | "end" | null => {
|
|
const elementsMap = scene.getNonDeletedElementsMap();
|
|
const pointerPos = pointFrom(scenePointerX, scenePointerY);
|
|
const hitThreshold = (FOCUS_POINT_SIZE * 1.5) / appState.zoom.value;
|
|
|
|
// Check start binding focus point
|
|
if (arrow.startBinding?.elementId) {
|
|
const bindableElement = elementsMap.get(arrow.startBinding.elementId);
|
|
if (
|
|
bindableElement &&
|
|
isBindableElement(bindableElement) &&
|
|
!bindableElement.isDeleted
|
|
) {
|
|
const focusPoint = getGlobalFixedPointForBindableElement(
|
|
arrow.startBinding.fixedPoint,
|
|
bindableElement,
|
|
elementsMap,
|
|
);
|
|
if (
|
|
isFocusPointVisible(
|
|
focusPoint,
|
|
arrow,
|
|
bindableElement,
|
|
elementsMap,
|
|
appState,
|
|
"start",
|
|
) &&
|
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
|
) {
|
|
return "start";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check end binding focus point (only if start not already hovered)
|
|
if (arrow.endBinding?.elementId) {
|
|
const bindableElement = elementsMap.get(arrow.endBinding.elementId);
|
|
if (
|
|
bindableElement &&
|
|
isBindableElement(bindableElement) &&
|
|
!bindableElement.isDeleted
|
|
) {
|
|
const focusPoint = getGlobalFixedPointForBindableElement(
|
|
arrow.endBinding.fixedPoint,
|
|
bindableElement,
|
|
elementsMap,
|
|
);
|
|
if (
|
|
isFocusPointVisible(
|
|
focusPoint,
|
|
arrow,
|
|
bindableElement,
|
|
elementsMap,
|
|
appState,
|
|
"end",
|
|
) &&
|
|
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
|
) {
|
|
return "end";
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|