|
@@ -27,8 +27,6 @@ import {
|
|
PRECISION,
|
|
PRECISION,
|
|
} from "@excalidraw/math";
|
|
} from "@excalidraw/math";
|
|
|
|
|
|
-import { isPointOnShape } from "@excalidraw/utils/collision";
|
|
|
|
-
|
|
|
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
|
|
|
|
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
|
@@ -41,7 +39,7 @@ import {
|
|
doBoundsIntersect,
|
|
doBoundsIntersect,
|
|
} from "./bounds";
|
|
} from "./bounds";
|
|
import { intersectElementWithLineSegment } from "./collision";
|
|
import { intersectElementWithLineSegment } from "./collision";
|
|
-import { distanceToBindableElement } from "./distance";
|
|
|
|
|
|
+import { distanceToElement } from "./distance";
|
|
import {
|
|
import {
|
|
headingForPointFromElement,
|
|
headingForPointFromElement,
|
|
headingIsHorizontal,
|
|
headingIsHorizontal,
|
|
@@ -63,7 +61,7 @@ import {
|
|
isTextElement,
|
|
isTextElement,
|
|
} from "./typeChecks";
|
|
} from "./typeChecks";
|
|
|
|
|
|
-import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
|
|
|
|
|
+import { aabbForElement } from "./shapes";
|
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
|
|
|
|
|
import type { Scene } from "./Scene";
|
|
import type { Scene } from "./Scene";
|
|
@@ -109,7 +107,6 @@ export const isBindingEnabled = (appState: AppState): boolean => {
|
|
|
|
|
|
export const FIXED_BINDING_DISTANCE = 5;
|
|
export const FIXED_BINDING_DISTANCE = 5;
|
|
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
|
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
|
-export const BINDING_HIGHLIGHT_OFFSET = 4;
|
|
|
|
|
|
|
|
const getNonDeletedElements = (
|
|
const getNonDeletedElements = (
|
|
scene: Scene,
|
|
scene: Scene,
|
|
@@ -131,6 +128,7 @@ export const bindOrUnbindLinearElement = (
|
|
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
|
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
|
scene: Scene,
|
|
scene: Scene,
|
|
): void => {
|
|
): void => {
|
|
|
|
+ const elementsMap = scene.getNonDeletedElementsMap();
|
|
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
|
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
|
const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
|
const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
|
bindOrUnbindLinearElementEdge(
|
|
bindOrUnbindLinearElementEdge(
|
|
@@ -141,6 +139,7 @@ export const bindOrUnbindLinearElement = (
|
|
boundToElementIds,
|
|
boundToElementIds,
|
|
unboundFromElementIds,
|
|
unboundFromElementIds,
|
|
scene,
|
|
scene,
|
|
|
|
+ elementsMap,
|
|
);
|
|
);
|
|
bindOrUnbindLinearElementEdge(
|
|
bindOrUnbindLinearElementEdge(
|
|
linearElement,
|
|
linearElement,
|
|
@@ -150,6 +149,7 @@ export const bindOrUnbindLinearElement = (
|
|
boundToElementIds,
|
|
boundToElementIds,
|
|
unboundFromElementIds,
|
|
unboundFromElementIds,
|
|
scene,
|
|
scene,
|
|
|
|
+ elementsMap,
|
|
);
|
|
);
|
|
|
|
|
|
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
|
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
|
@@ -176,6 +176,7 @@ const bindOrUnbindLinearElementEdge = (
|
|
// Is mutated
|
|
// Is mutated
|
|
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
|
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
|
scene: Scene,
|
|
scene: Scene,
|
|
|
|
+ elementsMap: ElementsMap,
|
|
): void => {
|
|
): void => {
|
|
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
|
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
|
if (bindableElement === "keep") {
|
|
if (bindableElement === "keep") {
|
|
@@ -216,43 +217,29 @@ const bindOrUnbindLinearElementEdge = (
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
-const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|
|
|
- linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
- edge: "start" | "end",
|
|
|
|
- elementsMap: NonDeletedSceneElementsMap,
|
|
|
|
- zoom?: AppState["zoom"],
|
|
|
|
-): NonDeleted<ExcalidrawElement> | null => {
|
|
|
|
- const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
|
|
|
- const elementId =
|
|
|
|
- edge === "start"
|
|
|
|
- ? linearElement.startBinding?.elementId
|
|
|
|
- : linearElement.endBinding?.elementId;
|
|
|
|
- if (elementId) {
|
|
|
|
- const element = elementsMap.get(elementId);
|
|
|
|
- if (
|
|
|
|
- isBindableElement(element) &&
|
|
|
|
- bindingBorderTest(element, coors, elementsMap, zoom)
|
|
|
|
- ) {
|
|
|
|
- return element;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return null;
|
|
|
|
-};
|
|
|
|
-
|
|
|
|
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
|
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
zoom?: AppState["zoom"],
|
|
zoom?: AppState["zoom"],
|
|
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
|
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
|
- ["start", "end"].map((edge) =>
|
|
|
|
- getOriginalBindingIfStillCloseOfLinearElementEdge(
|
|
|
|
- linearElement,
|
|
|
|
- edge as "start" | "end",
|
|
|
|
- elementsMap,
|
|
|
|
- zoom,
|
|
|
|
- ),
|
|
|
|
- );
|
|
|
|
|
|
+ (["start", "end"] as const).map((edge) => {
|
|
|
|
+ const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
|
|
|
+ const elementId =
|
|
|
|
+ edge === "start"
|
|
|
|
+ ? linearElement.startBinding?.elementId
|
|
|
|
+ : linearElement.endBinding?.elementId;
|
|
|
|
+ if (elementId) {
|
|
|
|
+ const element = elementsMap.get(elementId);
|
|
|
|
+ if (
|
|
|
|
+ isBindableElement(element) &&
|
|
|
|
+ bindingBorderTest(element, coors, elementsMap, zoom)
|
|
|
|
+ ) {
|
|
|
|
+ return element;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return null;
|
|
|
|
+ });
|
|
|
|
|
|
const getBindingStrategyForDraggingArrowEndpoints = (
|
|
const getBindingStrategyForDraggingArrowEndpoints = (
|
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
|
@@ -268,7 +255,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
|
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
|
const start = startDragged
|
|
const start = startDragged
|
|
? isBindingEnabled
|
|
? isBindingEnabled
|
|
- ? getElligibleElementForBindingElement(
|
|
|
|
|
|
+ ? getEligibleElementForBindingElement(
|
|
selectedElement,
|
|
selectedElement,
|
|
"start",
|
|
"start",
|
|
elementsMap,
|
|
elementsMap,
|
|
@@ -279,7 +266,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|
: "keep";
|
|
: "keep";
|
|
const end = endDragged
|
|
const end = endDragged
|
|
? isBindingEnabled
|
|
? isBindingEnabled
|
|
- ? getElligibleElementForBindingElement(
|
|
|
|
|
|
+ ? getEligibleElementForBindingElement(
|
|
selectedElement,
|
|
selectedElement,
|
|
"end",
|
|
"end",
|
|
elementsMap,
|
|
elementsMap,
|
|
@@ -311,7 +298,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|
);
|
|
);
|
|
const start = startIsClose
|
|
const start = startIsClose
|
|
? isBindingEnabled
|
|
? isBindingEnabled
|
|
- ? getElligibleElementForBindingElement(
|
|
|
|
|
|
+ ? getEligibleElementForBindingElement(
|
|
selectedElement,
|
|
selectedElement,
|
|
"start",
|
|
"start",
|
|
elementsMap,
|
|
elementsMap,
|
|
@@ -322,7 +309,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|
: null;
|
|
: null;
|
|
const end = endIsClose
|
|
const end = endIsClose
|
|
? isBindingEnabled
|
|
? isBindingEnabled
|
|
- ? getElligibleElementForBindingElement(
|
|
|
|
|
|
+ ? getEligibleElementForBindingElement(
|
|
selectedElement,
|
|
selectedElement,
|
|
"end",
|
|
"end",
|
|
elementsMap,
|
|
elementsMap,
|
|
@@ -441,22 +428,13 @@ export const maybeBindLinearElement = (
|
|
const normalizePointBinding = (
|
|
const normalizePointBinding = (
|
|
binding: { focus: number; gap: number },
|
|
binding: { focus: number; gap: number },
|
|
hoveredElement: ExcalidrawBindableElement,
|
|
hoveredElement: ExcalidrawBindableElement,
|
|
-) => {
|
|
|
|
- let gap = binding.gap;
|
|
|
|
- const maxGap = maxBindingGap(
|
|
|
|
- hoveredElement,
|
|
|
|
- hoveredElement.width,
|
|
|
|
- hoveredElement.height,
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- if (gap > maxGap) {
|
|
|
|
- gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET;
|
|
|
|
- }
|
|
|
|
- return {
|
|
|
|
- ...binding,
|
|
|
|
- gap,
|
|
|
|
- };
|
|
|
|
-};
|
|
|
|
|
|
+) => ({
|
|
|
|
+ ...binding,
|
|
|
|
+ gap: Math.min(
|
|
|
|
+ binding.gap,
|
|
|
|
+ maxBindingGap(hoveredElement, hoveredElement.width, hoveredElement.height),
|
|
|
|
+ ),
|
|
|
|
+});
|
|
|
|
|
|
export const bindLinearElement = (
|
|
export const bindLinearElement = (
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
@@ -488,6 +466,7 @@ export const bindLinearElement = (
|
|
linearElement,
|
|
linearElement,
|
|
hoveredElement,
|
|
hoveredElement,
|
|
startOrEnd,
|
|
startOrEnd,
|
|
|
|
+ scene.getNonDeletedElementsMap(),
|
|
),
|
|
),
|
|
};
|
|
};
|
|
}
|
|
}
|
|
@@ -703,8 +682,13 @@ const calculateFocusAndGap = (
|
|
);
|
|
);
|
|
|
|
|
|
return {
|
|
return {
|
|
- focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
|
|
|
|
- gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
|
|
|
|
|
|
+ focus: determineFocusDistance(
|
|
|
|
+ hoveredElement,
|
|
|
|
+ elementsMap,
|
|
|
|
+ adjacentPoint,
|
|
|
|
+ edgePoint,
|
|
|
|
+ ),
|
|
|
|
+ gap: Math.max(1, distanceToElement(hoveredElement, elementsMap, edgePoint)),
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
|
|
@@ -874,6 +858,7 @@ export const getHeadingForElbowArrowSnap = (
|
|
bindableElement: ExcalidrawBindableElement | undefined | null,
|
|
bindableElement: ExcalidrawBindableElement | undefined | null,
|
|
aabb: Bounds | undefined | null,
|
|
aabb: Bounds | undefined | null,
|
|
origPoint: GlobalPoint,
|
|
origPoint: GlobalPoint,
|
|
|
|
+ elementsMap: ElementsMap,
|
|
zoom?: AppState["zoom"],
|
|
zoom?: AppState["zoom"],
|
|
): Heading => {
|
|
): Heading => {
|
|
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
|
|
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
|
|
@@ -882,11 +867,16 @@ export const getHeadingForElbowArrowSnap = (
|
|
return otherPointHeading;
|
|
return otherPointHeading;
|
|
}
|
|
}
|
|
|
|
|
|
- const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
|
|
|
|
|
|
+ const distance = getDistanceForBinding(
|
|
|
|
+ origPoint,
|
|
|
|
+ bindableElement,
|
|
|
|
+ elementsMap,
|
|
|
|
+ zoom,
|
|
|
|
+ );
|
|
|
|
|
|
if (!distance) {
|
|
if (!distance) {
|
|
return vectorToHeading(
|
|
return vectorToHeading(
|
|
- vectorFromPoint(p, elementCenterPoint(bindableElement)),
|
|
|
|
|
|
+ vectorFromPoint(p, elementCenterPoint(bindableElement, elementsMap)),
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
@@ -896,9 +886,10 @@ export const getHeadingForElbowArrowSnap = (
|
|
const getDistanceForBinding = (
|
|
const getDistanceForBinding = (
|
|
point: Readonly<GlobalPoint>,
|
|
point: Readonly<GlobalPoint>,
|
|
bindableElement: ExcalidrawBindableElement,
|
|
bindableElement: ExcalidrawBindableElement,
|
|
|
|
+ elementsMap: ElementsMap,
|
|
zoom?: AppState["zoom"],
|
|
zoom?: AppState["zoom"],
|
|
) => {
|
|
) => {
|
|
- const distance = distanceToBindableElement(bindableElement, point);
|
|
|
|
|
|
+ const distance = distanceToElement(bindableElement, elementsMap, point);
|
|
const bindDistance = maxBindingGap(
|
|
const bindDistance = maxBindingGap(
|
|
bindableElement,
|
|
bindableElement,
|
|
bindableElement.width,
|
|
bindableElement.width,
|
|
@@ -913,12 +904,13 @@ export const bindPointToSnapToElementOutline = (
|
|
arrow: ExcalidrawElbowArrowElement,
|
|
arrow: ExcalidrawElbowArrowElement,
|
|
bindableElement: ExcalidrawBindableElement,
|
|
bindableElement: ExcalidrawBindableElement,
|
|
startOrEnd: "start" | "end",
|
|
startOrEnd: "start" | "end",
|
|
|
|
+ elementsMap: ElementsMap,
|
|
): GlobalPoint => {
|
|
): GlobalPoint => {
|
|
if (isDevEnv() || isTestEnv()) {
|
|
if (isDevEnv() || isTestEnv()) {
|
|
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
|
|
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
|
|
}
|
|
}
|
|
|
|
|
|
- const aabb = aabbForElement(bindableElement);
|
|
|
|
|
|
+ const aabb = aabbForElement(bindableElement, elementsMap);
|
|
const localP =
|
|
const localP =
|
|
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
|
|
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
|
|
const globalP = pointFrom<GlobalPoint>(
|
|
const globalP = pointFrom<GlobalPoint>(
|
|
@@ -926,7 +918,7 @@ export const bindPointToSnapToElementOutline = (
|
|
arrow.y + localP[1],
|
|
arrow.y + localP[1],
|
|
);
|
|
);
|
|
const edgePoint = isRectanguloidElement(bindableElement)
|
|
const edgePoint = isRectanguloidElement(bindableElement)
|
|
- ? avoidRectangularCorner(bindableElement, globalP)
|
|
|
|
|
|
+ ? avoidRectangularCorner(bindableElement, elementsMap, globalP)
|
|
: globalP;
|
|
: globalP;
|
|
const elbowed = isElbowArrow(arrow);
|
|
const elbowed = isElbowArrow(arrow);
|
|
const center = getCenterForBounds(aabb);
|
|
const center = getCenterForBounds(aabb);
|
|
@@ -945,26 +937,31 @@ export const bindPointToSnapToElementOutline = (
|
|
const isHorizontal = headingIsHorizontal(
|
|
const isHorizontal = headingIsHorizontal(
|
|
headingForPointFromElement(bindableElement, aabb, globalP),
|
|
headingForPointFromElement(bindableElement, aabb, globalP),
|
|
);
|
|
);
|
|
|
|
+ const snapPoint = snapToMid(bindableElement, elementsMap, edgePoint);
|
|
const otherPoint = pointFrom<GlobalPoint>(
|
|
const otherPoint = pointFrom<GlobalPoint>(
|
|
- isHorizontal ? center[0] : edgePoint[0],
|
|
|
|
- !isHorizontal ? center[1] : edgePoint[1],
|
|
|
|
|
|
+ isHorizontal ? center[0] : snapPoint[0],
|
|
|
|
+ !isHorizontal ? center[1] : snapPoint[1],
|
|
);
|
|
);
|
|
- intersection = intersectElementWithLineSegment(
|
|
|
|
- bindableElement,
|
|
|
|
- lineSegment(
|
|
|
|
- otherPoint,
|
|
|
|
- pointFromVector(
|
|
|
|
- vectorScale(
|
|
|
|
- vectorNormalize(vectorFromPoint(edgePoint, otherPoint)),
|
|
|
|
- Math.max(bindableElement.width, bindableElement.height) * 2,
|
|
|
|
- ),
|
|
|
|
- otherPoint,
|
|
|
|
|
|
+ const intersector = lineSegment(
|
|
|
|
+ otherPoint,
|
|
|
|
+ pointFromVector(
|
|
|
|
+ vectorScale(
|
|
|
|
+ vectorNormalize(vectorFromPoint(snapPoint, otherPoint)),
|
|
|
|
+ Math.max(bindableElement.width, bindableElement.height) * 2,
|
|
),
|
|
),
|
|
|
|
+ otherPoint,
|
|
),
|
|
),
|
|
- )[0];
|
|
|
|
|
|
+ );
|
|
|
|
+ intersection = intersectElementWithLineSegment(
|
|
|
|
+ bindableElement,
|
|
|
|
+ elementsMap,
|
|
|
|
+ intersector,
|
|
|
|
+ FIXED_BINDING_DISTANCE,
|
|
|
|
+ ).sort(pointDistanceSq)[0];
|
|
} else {
|
|
} else {
|
|
intersection = intersectElementWithLineSegment(
|
|
intersection = intersectElementWithLineSegment(
|
|
bindableElement,
|
|
bindableElement,
|
|
|
|
+ elementsMap,
|
|
lineSegment(
|
|
lineSegment(
|
|
adjacentPoint,
|
|
adjacentPoint,
|
|
pointFromVector(
|
|
pointFromVector(
|
|
@@ -991,31 +988,15 @@ export const bindPointToSnapToElementOutline = (
|
|
return edgePoint;
|
|
return edgePoint;
|
|
}
|
|
}
|
|
|
|
|
|
- if (elbowed) {
|
|
|
|
- const scalar =
|
|
|
|
- pointDistanceSq(edgePoint, center) -
|
|
|
|
- pointDistanceSq(intersection, center) >
|
|
|
|
- 0
|
|
|
|
- ? FIXED_BINDING_DISTANCE
|
|
|
|
- : -FIXED_BINDING_DISTANCE;
|
|
|
|
-
|
|
|
|
- return pointFromVector(
|
|
|
|
- vectorScale(
|
|
|
|
- vectorNormalize(vectorFromPoint(edgePoint, intersection)),
|
|
|
|
- scalar,
|
|
|
|
- ),
|
|
|
|
- intersection,
|
|
|
|
- );
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return edgePoint;
|
|
|
|
|
|
+ return elbowed ? intersection : edgePoint;
|
|
};
|
|
};
|
|
|
|
|
|
export const avoidRectangularCorner = (
|
|
export const avoidRectangularCorner = (
|
|
element: ExcalidrawBindableElement,
|
|
element: ExcalidrawBindableElement,
|
|
|
|
+ elementsMap: ElementsMap,
|
|
p: GlobalPoint,
|
|
p: GlobalPoint,
|
|
): GlobalPoint => {
|
|
): GlobalPoint => {
|
|
- const center = elementCenterPoint(element);
|
|
|
|
|
|
+ const center = elementCenterPoint(element, elementsMap);
|
|
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
|
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
|
|
|
|
|
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
|
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
|
@@ -1108,35 +1089,34 @@ export const avoidRectangularCorner = (
|
|
|
|
|
|
export const snapToMid = (
|
|
export const snapToMid = (
|
|
element: ExcalidrawBindableElement,
|
|
element: ExcalidrawBindableElement,
|
|
|
|
+ elementsMap: ElementsMap,
|
|
p: GlobalPoint,
|
|
p: GlobalPoint,
|
|
tolerance: number = 0.05,
|
|
tolerance: number = 0.05,
|
|
): GlobalPoint => {
|
|
): GlobalPoint => {
|
|
const { x, y, width, height, angle } = element;
|
|
const { x, y, width, height, angle } = element;
|
|
-
|
|
|
|
- const center = elementCenterPoint(element, -0.1, -0.1);
|
|
|
|
-
|
|
|
|
|
|
+ const center = elementCenterPoint(element, elementsMap, -0.1, -0.1);
|
|
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
|
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
|
|
|
|
|
// snap-to-center point is adaptive to element size, but we don't want to go
|
|
// snap-to-center point is adaptive to element size, but we don't want to go
|
|
// above and below certain px distance
|
|
// above and below certain px distance
|
|
- const verticalThrehsold = clamp(tolerance * height, 5, 80);
|
|
|
|
- const horizontalThrehsold = clamp(tolerance * width, 5, 80);
|
|
|
|
|
|
+ const verticalThreshold = clamp(tolerance * height, 5, 80);
|
|
|
|
+ const horizontalThreshold = clamp(tolerance * width, 5, 80);
|
|
|
|
|
|
if (
|
|
if (
|
|
nonRotated[0] <= x + width / 2 &&
|
|
nonRotated[0] <= x + width / 2 &&
|
|
- nonRotated[1] > center[1] - verticalThrehsold &&
|
|
|
|
- nonRotated[1] < center[1] + verticalThrehsold
|
|
|
|
|
|
+ nonRotated[1] > center[1] - verticalThreshold &&
|
|
|
|
+ nonRotated[1] < center[1] + verticalThreshold
|
|
) {
|
|
) {
|
|
// LEFT
|
|
// LEFT
|
|
- return pointRotateRads(
|
|
|
|
|
|
+ return pointRotateRads<GlobalPoint>(
|
|
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
|
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
|
center,
|
|
center,
|
|
angle,
|
|
angle,
|
|
);
|
|
);
|
|
} else if (
|
|
} else if (
|
|
nonRotated[1] <= y + height / 2 &&
|
|
nonRotated[1] <= y + height / 2 &&
|
|
- nonRotated[0] > center[0] - horizontalThrehsold &&
|
|
|
|
- nonRotated[0] < center[0] + horizontalThrehsold
|
|
|
|
|
|
+ nonRotated[0] > center[0] - horizontalThreshold &&
|
|
|
|
+ nonRotated[0] < center[0] + horizontalThreshold
|
|
) {
|
|
) {
|
|
// TOP
|
|
// TOP
|
|
return pointRotateRads(
|
|
return pointRotateRads(
|
|
@@ -1146,8 +1126,8 @@ export const snapToMid = (
|
|
);
|
|
);
|
|
} else if (
|
|
} else if (
|
|
nonRotated[0] >= x + width / 2 &&
|
|
nonRotated[0] >= x + width / 2 &&
|
|
- nonRotated[1] > center[1] - verticalThrehsold &&
|
|
|
|
- nonRotated[1] < center[1] + verticalThrehsold
|
|
|
|
|
|
+ nonRotated[1] > center[1] - verticalThreshold &&
|
|
|
|
+ nonRotated[1] < center[1] + verticalThreshold
|
|
) {
|
|
) {
|
|
// RIGHT
|
|
// RIGHT
|
|
return pointRotateRads(
|
|
return pointRotateRads(
|
|
@@ -1157,8 +1137,8 @@ export const snapToMid = (
|
|
);
|
|
);
|
|
} else if (
|
|
} else if (
|
|
nonRotated[1] >= y + height / 2 &&
|
|
nonRotated[1] >= y + height / 2 &&
|
|
- nonRotated[0] > center[0] - horizontalThrehsold &&
|
|
|
|
- nonRotated[0] < center[0] + horizontalThrehsold
|
|
|
|
|
|
+ nonRotated[0] > center[0] - horizontalThreshold &&
|
|
|
|
+ nonRotated[0] < center[0] + horizontalThreshold
|
|
) {
|
|
) {
|
|
// DOWN
|
|
// DOWN
|
|
return pointRotateRads(
|
|
return pointRotateRads(
|
|
@@ -1167,7 +1147,7 @@ export const snapToMid = (
|
|
angle,
|
|
angle,
|
|
);
|
|
);
|
|
} else if (element.type === "diamond") {
|
|
} else if (element.type === "diamond") {
|
|
- const distance = FIXED_BINDING_DISTANCE - 1;
|
|
|
|
|
|
+ const distance = FIXED_BINDING_DISTANCE;
|
|
const topLeft = pointFrom<GlobalPoint>(
|
|
const topLeft = pointFrom<GlobalPoint>(
|
|
x + width / 4 - distance,
|
|
x + width / 4 - distance,
|
|
y + height / 4 - distance,
|
|
y + height / 4 - distance,
|
|
@@ -1184,27 +1164,28 @@ export const snapToMid = (
|
|
x + (3 * width) / 4 + distance,
|
|
x + (3 * width) / 4 + distance,
|
|
y + (3 * height) / 4 + distance,
|
|
y + (3 * height) / 4 + distance,
|
|
);
|
|
);
|
|
|
|
+
|
|
if (
|
|
if (
|
|
pointDistance(topLeft, nonRotated) <
|
|
pointDistance(topLeft, nonRotated) <
|
|
- Math.max(horizontalThrehsold, verticalThrehsold)
|
|
|
|
|
|
+ Math.max(horizontalThreshold, verticalThreshold)
|
|
) {
|
|
) {
|
|
return pointRotateRads(topLeft, center, angle);
|
|
return pointRotateRads(topLeft, center, angle);
|
|
}
|
|
}
|
|
if (
|
|
if (
|
|
pointDistance(topRight, nonRotated) <
|
|
pointDistance(topRight, nonRotated) <
|
|
- Math.max(horizontalThrehsold, verticalThrehsold)
|
|
|
|
|
|
+ Math.max(horizontalThreshold, verticalThreshold)
|
|
) {
|
|
) {
|
|
return pointRotateRads(topRight, center, angle);
|
|
return pointRotateRads(topRight, center, angle);
|
|
}
|
|
}
|
|
if (
|
|
if (
|
|
pointDistance(bottomLeft, nonRotated) <
|
|
pointDistance(bottomLeft, nonRotated) <
|
|
- Math.max(horizontalThrehsold, verticalThrehsold)
|
|
|
|
|
|
+ Math.max(horizontalThreshold, verticalThreshold)
|
|
) {
|
|
) {
|
|
return pointRotateRads(bottomLeft, center, angle);
|
|
return pointRotateRads(bottomLeft, center, angle);
|
|
}
|
|
}
|
|
if (
|
|
if (
|
|
pointDistance(bottomRight, nonRotated) <
|
|
pointDistance(bottomRight, nonRotated) <
|
|
- Math.max(horizontalThrehsold, verticalThrehsold)
|
|
|
|
|
|
+ Math.max(horizontalThreshold, verticalThreshold)
|
|
) {
|
|
) {
|
|
return pointRotateRads(bottomRight, center, angle);
|
|
return pointRotateRads(bottomRight, center, angle);
|
|
}
|
|
}
|
|
@@ -1239,8 +1220,9 @@ const updateBoundPoint = (
|
|
linearElement,
|
|
linearElement,
|
|
bindableElement,
|
|
bindableElement,
|
|
startOrEnd === "startBinding" ? "start" : "end",
|
|
startOrEnd === "startBinding" ? "start" : "end",
|
|
|
|
+ elementsMap,
|
|
).fixedPoint;
|
|
).fixedPoint;
|
|
- const globalMidPoint = elementCenterPoint(bindableElement);
|
|
|
|
|
|
+ const globalMidPoint = elementCenterPoint(bindableElement, elementsMap);
|
|
const global = pointFrom<GlobalPoint>(
|
|
const global = pointFrom<GlobalPoint>(
|
|
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
|
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
|
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
|
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
|
@@ -1266,6 +1248,7 @@ const updateBoundPoint = (
|
|
);
|
|
);
|
|
const focusPointAbsolute = determineFocusPoint(
|
|
const focusPointAbsolute = determineFocusPoint(
|
|
bindableElement,
|
|
bindableElement,
|
|
|
|
+ elementsMap,
|
|
binding.focus,
|
|
binding.focus,
|
|
adjacentPoint,
|
|
adjacentPoint,
|
|
);
|
|
);
|
|
@@ -1284,7 +1267,7 @@ const updateBoundPoint = (
|
|
elementsMap,
|
|
elementsMap,
|
|
);
|
|
);
|
|
|
|
|
|
- const center = elementCenterPoint(bindableElement);
|
|
|
|
|
|
+ const center = elementCenterPoint(bindableElement, elementsMap);
|
|
const interceptorLength =
|
|
const interceptorLength =
|
|
pointDistance(adjacentPoint, edgePointAbsolute) +
|
|
pointDistance(adjacentPoint, edgePointAbsolute) +
|
|
pointDistance(adjacentPoint, center) +
|
|
pointDistance(adjacentPoint, center) +
|
|
@@ -1292,6 +1275,7 @@ const updateBoundPoint = (
|
|
const intersections = [
|
|
const intersections = [
|
|
...intersectElementWithLineSegment(
|
|
...intersectElementWithLineSegment(
|
|
bindableElement,
|
|
bindableElement,
|
|
|
|
+ elementsMap,
|
|
lineSegment<GlobalPoint>(
|
|
lineSegment<GlobalPoint>(
|
|
adjacentPoint,
|
|
adjacentPoint,
|
|
pointFromVector(
|
|
pointFromVector(
|
|
@@ -1342,6 +1326,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
|
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
|
hoveredElement: ExcalidrawBindableElement,
|
|
hoveredElement: ExcalidrawBindableElement,
|
|
startOrEnd: "start" | "end",
|
|
startOrEnd: "start" | "end",
|
|
|
|
+ elementsMap: ElementsMap,
|
|
): { fixedPoint: FixedPoint } => {
|
|
): { fixedPoint: FixedPoint } => {
|
|
const bounds = [
|
|
const bounds = [
|
|
hoveredElement.x,
|
|
hoveredElement.x,
|
|
@@ -1353,6 +1338,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|
linearElement,
|
|
linearElement,
|
|
hoveredElement,
|
|
hoveredElement,
|
|
startOrEnd,
|
|
startOrEnd,
|
|
|
|
+ elementsMap,
|
|
);
|
|
);
|
|
const globalMidPoint = pointFrom(
|
|
const globalMidPoint = pointFrom(
|
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
|
@@ -1396,7 +1382,7 @@ const maybeCalculateNewGapWhenScaling = (
|
|
return { ...currentBinding, gap: newGap };
|
|
return { ...currentBinding, gap: newGap };
|
|
};
|
|
};
|
|
|
|
|
|
-const getElligibleElementForBindingElement = (
|
|
|
|
|
|
+const getEligibleElementForBindingElement = (
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
startOrEnd: "start" | "end",
|
|
startOrEnd: "start" | "end",
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
elementsMap: NonDeletedSceneElementsMap,
|
|
@@ -1548,14 +1534,38 @@ export const bindingBorderTest = (
|
|
zoom?: AppState["zoom"],
|
|
zoom?: AppState["zoom"],
|
|
fullShape?: boolean,
|
|
fullShape?: boolean,
|
|
): boolean => {
|
|
): boolean => {
|
|
|
|
+ const p = pointFrom<GlobalPoint>(x, y);
|
|
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
|
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
|
|
|
+ const shouldTestInside =
|
|
|
|
+ // disable fullshape snapping for frame elements so we
|
|
|
|
+ // can bind to frame children
|
|
|
|
+ (fullShape || !isBindingFallthroughEnabled(element)) &&
|
|
|
|
+ !isFrameLikeElement(element);
|
|
|
|
+
|
|
|
|
+ // PERF: Run a cheap test to see if the binding element
|
|
|
|
+ // is even close to the element
|
|
|
|
+ const bounds = [
|
|
|
|
+ x - threshold,
|
|
|
|
+ y - threshold,
|
|
|
|
+ x + threshold,
|
|
|
|
+ y + threshold,
|
|
|
|
+ ] as Bounds;
|
|
|
|
+ const elementBounds = getElementBounds(element, elementsMap);
|
|
|
|
+ if (!doBoundsIntersect(bounds, elementBounds)) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
|
|
- const shape = getElementShape(element, elementsMap);
|
|
|
|
- return (
|
|
|
|
- isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
|
|
|
- (fullShape === true &&
|
|
|
|
- pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
|
|
|
|
|
+ // Do the intersection test against the element since it's close enough
|
|
|
|
+ const intersections = intersectElementWithLineSegment(
|
|
|
|
+ element,
|
|
|
|
+ elementsMap,
|
|
|
|
+ lineSegment(elementCenterPoint(element, elementsMap), p),
|
|
);
|
|
);
|
|
|
|
+ const distance = distanceToElement(element, elementsMap, p);
|
|
|
|
+
|
|
|
|
+ return shouldTestInside
|
|
|
|
+ ? intersections.length === 0 || distance <= threshold
|
|
|
|
+ : intersections.length > 0 && distance <= threshold;
|
|
};
|
|
};
|
|
|
|
|
|
export const maxBindingGap = (
|
|
export const maxBindingGap = (
|
|
@@ -1575,7 +1585,7 @@ export const maxBindingGap = (
|
|
// bigger bindable boundary for bigger elements
|
|
// bigger bindable boundary for bigger elements
|
|
Math.min(0.25 * smallerDimension, 32),
|
|
Math.min(0.25 * smallerDimension, 32),
|
|
// keep in sync with the zoomed highlight
|
|
// keep in sync with the zoomed highlight
|
|
- BINDING_HIGHLIGHT_THICKNESS / zoomValue + BINDING_HIGHLIGHT_OFFSET,
|
|
|
|
|
|
+ BINDING_HIGHLIGHT_THICKNESS / zoomValue + FIXED_BINDING_DISTANCE,
|
|
);
|
|
);
|
|
};
|
|
};
|
|
|
|
|
|
@@ -1586,12 +1596,13 @@ export const maxBindingGap = (
|
|
// of the element.
|
|
// of the element.
|
|
const determineFocusDistance = (
|
|
const determineFocusDistance = (
|
|
element: ExcalidrawBindableElement,
|
|
element: ExcalidrawBindableElement,
|
|
|
|
+ elementsMap: ElementsMap,
|
|
// Point on the line, in absolute coordinates
|
|
// Point on the line, in absolute coordinates
|
|
a: GlobalPoint,
|
|
a: GlobalPoint,
|
|
// Another point on the line, in absolute coordinates (closer to element)
|
|
// Another point on the line, in absolute coordinates (closer to element)
|
|
b: GlobalPoint,
|
|
b: GlobalPoint,
|
|
): number => {
|
|
): number => {
|
|
- const center = elementCenterPoint(element);
|
|
|
|
|
|
+ const center = elementCenterPoint(element, elementsMap);
|
|
|
|
|
|
if (pointsEqual(a, b)) {
|
|
if (pointsEqual(a, b)) {
|
|
return 0;
|
|
return 0;
|
|
@@ -1716,12 +1727,13 @@ const determineFocusDistance = (
|
|
|
|
|
|
const determineFocusPoint = (
|
|
const determineFocusPoint = (
|
|
element: ExcalidrawBindableElement,
|
|
element: ExcalidrawBindableElement,
|
|
|
|
+ elementsMap: ElementsMap,
|
|
// The oriented, relative distance from the center of `element` of the
|
|
// The oriented, relative distance from the center of `element` of the
|
|
// returned focusPoint
|
|
// returned focusPoint
|
|
focus: number,
|
|
focus: number,
|
|
adjacentPoint: GlobalPoint,
|
|
adjacentPoint: GlobalPoint,
|
|
): GlobalPoint => {
|
|
): GlobalPoint => {
|
|
- const center = elementCenterPoint(element);
|
|
|
|
|
|
+ const center = elementCenterPoint(element, elementsMap);
|
|
|
|
|
|
if (focus === 0) {
|
|
if (focus === 0) {
|
|
return center;
|
|
return center;
|
|
@@ -2144,6 +2156,7 @@ export class BindableElement {
|
|
export const getGlobalFixedPointForBindableElement = (
|
|
export const getGlobalFixedPointForBindableElement = (
|
|
fixedPointRatio: [number, number],
|
|
fixedPointRatio: [number, number],
|
|
element: ExcalidrawBindableElement,
|
|
element: ExcalidrawBindableElement,
|
|
|
|
+ elementsMap: ElementsMap,
|
|
): GlobalPoint => {
|
|
): GlobalPoint => {
|
|
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
|
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
|
|
|
|
|
@@ -2152,7 +2165,7 @@ export const getGlobalFixedPointForBindableElement = (
|
|
element.x + element.width * fixedX,
|
|
element.x + element.width * fixedX,
|
|
element.y + element.height * fixedY,
|
|
element.y + element.height * fixedY,
|
|
),
|
|
),
|
|
- elementCenterPoint(element),
|
|
|
|
|
|
+ elementCenterPoint(element, elementsMap),
|
|
element.angle,
|
|
element.angle,
|
|
);
|
|
);
|
|
};
|
|
};
|
|
@@ -2176,6 +2189,7 @@ export const getGlobalFixedPoints = (
|
|
? getGlobalFixedPointForBindableElement(
|
|
? getGlobalFixedPointForBindableElement(
|
|
arrow.startBinding.fixedPoint,
|
|
arrow.startBinding.fixedPoint,
|
|
startElement as ExcalidrawBindableElement,
|
|
startElement as ExcalidrawBindableElement,
|
|
|
|
+ elementsMap,
|
|
)
|
|
)
|
|
: pointFrom<GlobalPoint>(
|
|
: pointFrom<GlobalPoint>(
|
|
arrow.x + arrow.points[0][0],
|
|
arrow.x + arrow.points[0][0],
|
|
@@ -2186,6 +2200,7 @@ export const getGlobalFixedPoints = (
|
|
? getGlobalFixedPointForBindableElement(
|
|
? getGlobalFixedPointForBindableElement(
|
|
arrow.endBinding.fixedPoint,
|
|
arrow.endBinding.fixedPoint,
|
|
endElement as ExcalidrawBindableElement,
|
|
endElement as ExcalidrawBindableElement,
|
|
|
|
+ elementsMap,
|
|
)
|
|
)
|
|
: pointFrom<GlobalPoint>(
|
|
: pointFrom<GlobalPoint>(
|
|
arrow.x + arrow.points[arrow.points.length - 1][0],
|
|
arrow.x + arrow.points[arrow.points.length - 1][0],
|