Binding updates

feat/remove-ga
Mark Tolmacs 6 days ago
parent 7d487f8872
commit 6f9c6fc205
No known key found for this signature in database

@ -13,6 +13,7 @@ import type {
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
FixedPoint, FixedPoint,
SceneElementsMap, SceneElementsMap,
FixedPointBinding,
} from "./types"; } from "./types";
import type { Bounds } from "./bounds"; import type { Bounds } from "./bounds";
@ -64,18 +65,13 @@ import {
line, line,
linesIntersectAt, linesIntersectAt,
pointDistance, pointDistance,
pointOnLineSegment,
pointFromVector, pointFromVector,
vectorScale, vectorScale,
vectorNormalize, vectorNormalize,
vectorRotate, vectorCross,
lineClosestPoint,
vectorDot,
} from "../../math"; } from "../../math";
import { intersectElementWithLineSegment } from "./collision"; import { intersectElementWithLineSegment } from "./collision";
import { distanceToBindableElement } from "./distance"; import { distanceToBindableElement } from "./distance";
import { debugClear, debugDrawPoint } from "../visualdebug";
import { ellipse } from "../../math/ellipse";
export type SuggestedBinding = export type SuggestedBinding =
| NonDeleted<ExcalidrawBindableElement> | NonDeleted<ExcalidrawBindableElement>
@ -121,7 +117,6 @@ export const bindOrUnbindLinearElement = (
endBindingElement: ExcalidrawBindableElement | null | "keep", endBindingElement: ExcalidrawBindableElement | null | "keep",
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
scene: Scene, scene: Scene,
zoom?: AppState["zoom"],
): void => { ): void => {
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();
@ -133,7 +128,6 @@ export const bindOrUnbindLinearElement = (
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap, elementsMap,
zoom,
); );
bindOrUnbindLinearElementEdge( bindOrUnbindLinearElementEdge(
linearElement, linearElement,
@ -143,7 +137,6 @@ export const bindOrUnbindLinearElement = (
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap, elementsMap,
zoom,
); );
const onlyUnbound = Array.from(unboundFromElementIds).filter( const onlyUnbound = Array.from(unboundFromElementIds).filter(
@ -170,7 +163,6 @@ const bindOrUnbindLinearElementEdge = (
// Is mutated // Is mutated
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>, unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"],
): 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") {
@ -207,18 +199,11 @@ const bindOrUnbindLinearElementEdge = (
bindableElement, bindableElement,
startOrEnd, startOrEnd,
elementsMap, elementsMap,
zoom,
); );
boundToElementIds.add(bindableElement.id); boundToElementIds.add(bindableElement.id);
} }
} else { } else {
bindLinearElement( bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap);
linearElement,
bindableElement,
startOrEnd,
elementsMap,
zoom,
);
boundToElementIds.add(bindableElement.id); boundToElementIds.add(bindableElement.id);
} }
}; };
@ -380,14 +365,7 @@ export const bindOrUnbindLinearElements = (
zoom, zoom,
); );
bindOrUnbindLinearElement( bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene);
selectedElement,
start,
end,
elementsMap,
scene,
zoom,
);
}); });
}; };
@ -436,7 +414,6 @@ export const maybeBindLinearElement = (
appState.startBoundElement, appState.startBoundElement,
"start", "start",
elementsMap, elementsMap,
appState.zoom,
); );
} }
@ -457,13 +434,7 @@ export const maybeBindLinearElement = (
"end", "end",
) )
) { ) {
bindLinearElement( bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
linearElement,
hoveredElement,
"end",
elementsMap,
appState.zoom,
);
} }
} }
}; };
@ -493,31 +464,35 @@ export const bindLinearElement = (
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"],
): void => { ): void => {
if (!isArrowElement(linearElement)) { if (!isArrowElement(linearElement)) {
return; return;
} }
const binding: PointBinding = {
const binding: PointBinding | FixedPointBinding = {
elementId: hoveredElement.id, elementId: hoveredElement.id,
...normalizePointBinding(
calculateFocusAndGap(
linearElement,
hoveredElement,
startOrEnd,
elementsMap,
zoom,
),
hoveredElement,
),
...(isElbowArrow(linearElement) ...(isElbowArrow(linearElement)
? calculateFixedPointForElbowArrowBinding( ? {
linearElement, ...calculateFixedPointForElbowArrowBinding(
hoveredElement, linearElement,
startOrEnd, hoveredElement,
elementsMap, startOrEnd,
) elementsMap,
: { fixedPoint: null }), ),
focus: 0,
gap: 0,
}
: {
...normalizePointBinding(
calculateFocusAndGap(
linearElement,
hoveredElement,
startOrEnd,
elementsMap,
),
hoveredElement,
),
}),
}; };
mutateElement(linearElement, { mutateElement(linearElement, {
@ -713,7 +688,6 @@ const calculateFocusAndGap = (
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"],
): { focus: number; gap: number } => { ): { focus: number; gap: number } => {
const direction = startOrEnd === "start" ? -1 : 1; const direction = startOrEnd === "start" ? -1 : 1;
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1; const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
@ -730,28 +704,9 @@ const calculateFocusAndGap = (
elementsMap, elementsMap,
); );
const focus = determineFocusDistance(
hoveredElement,
adjacentPoint,
edgePoint,
);
const focusPointAbsolute = determineFocusPoint(
hoveredElement,
focus,
adjacentPoint,
);
const intersection =
intersectElementWithLineSegment(
hoveredElement,
lineSegment(adjacentPoint, focusPointAbsolute),
).sort(
(g, h) => pointDistanceSq(g, edgePoint) - pointDistanceSq(h, edgePoint),
)[0] ?? edgePoint;
const gap = Math.max(1, pointDistance(intersection, edgePoint));
return { return {
focus, focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
gap, gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
}; };
}; };
@ -1264,27 +1219,16 @@ const updateBoundPoint = (
elementsMap, elementsMap,
); );
const intersection = newEdgePoint =
intersectElementWithLineSegment( intersectElementWithLineSegment(
bindableElement, bindableElement,
lineSegment<GlobalPoint>(adjacentPoint, focusPointAbsolute), lineSegment<GlobalPoint>(adjacentPoint, focusPointAbsolute),
binding.gap,
).sort( ).sort(
(g, h) => (g, h) =>
pointDistanceSq(g!, edgePointAbsolute) - pointDistanceSq(g, edgePointAbsolute) -
pointDistanceSq(h!, edgePointAbsolute), pointDistanceSq(h, edgePointAbsolute),
)[0] ?? edgePointAbsolute; )[0] ?? edgePointAbsolute;
const gapOffsetPoint = intersection
? pointFromVector(
vectorScale(
vectorNormalize(vectorFromPoint(adjacentPoint, intersection)),
binding.gap,
),
intersection,
)
: edgePointAbsolute;
newEdgePoint = gapOffsetPoint;
} }
return LinearElementEditor.pointFromAbsoluteCoords( return LinearElementEditor.pointFromAbsoluteCoords(
@ -1579,12 +1523,10 @@ const determineFocusDistance = (
element.x + element.width / 2, element.x + element.width / 2,
element.y + element.height / 2, element.y + element.height / 2,
); );
const linear = vectorFromPoint(b, a); const intersection = [
const dot1 = Math.abs( linesIntersectAt(
vectorDot( line(a, b),
linear, line(
// One of the diagonals
vectorFromPoint(
pointFrom<GlobalPoint>(element.x, element.y), pointFrom<GlobalPoint>(element.x, element.y),
pointFrom<GlobalPoint>( pointFrom<GlobalPoint>(
element.x + element.width, element.x + element.width,
@ -1592,42 +1534,24 @@ const determineFocusDistance = (
), ),
), ),
), ),
); linesIntersectAt(
const dot2 = Math.abs( line(a, b),
vectorDot( line(
linear,
// The other diagonal
vectorFromPoint(
pointFrom<GlobalPoint>(element.x + element.width, element.y), pointFrom<GlobalPoint>(element.x + element.width, element.y),
pointFrom<GlobalPoint>(element.x, element.y + element.height), pointFrom<GlobalPoint>(element.x, element.y + element.height),
), ),
), ),
); ]
const intersect = .filter((p): p is GlobalPoint => p !== null)
linesIntersectAt( .sort((g, h) => pointDistanceSq(g, b) - pointDistanceSq(h, b))[0];
// The bigger inclination of the diagonal and the linear element is the one
// that determines the intersection point
dot1 > dot2
? line(
pointFrom<GlobalPoint>(element.x + element.width, element.y),
pointFrom<GlobalPoint>(element.x, element.y + element.height),
)
: line(
pointFrom<GlobalPoint>(element.x, element.y),
pointFrom<GlobalPoint>(
element.x + element.width,
element.y + element.height,
),
),
line(a, b),
) || center;
const sign = const sign =
vectorDot(vectorFromPoint(center, a), vectorFromPoint(a, b)) < 0 ? -1 : 1; Math.sign(vectorCross(vectorFromPoint(b, a), vectorFromPoint(b, center))) *
const signedDist = sign * pointDistance(center, intersect); -1;
const sdRatio = const signedDist = sign * pointDistance(center, intersection);
const signedDistanceRatio =
signedDist / (Math.sqrt(element.width ** 2 + element.height ** 2) / 2); signedDist / (Math.sqrt(element.width ** 2 + element.height ** 2) / 2);
return sdRatio; return signedDistanceRatio;
}; };
const determineFocusPoint = ( const determineFocusPoint = (
@ -1645,22 +1569,55 @@ const determineFocusPoint = (
return center; return center;
} }
return pointFromVector( const candidates = [
vectorScale( pointFrom<GlobalPoint>(element.x, element.y),
vectorRotate( pointFrom<GlobalPoint>(element.x + element.width, element.y),
vectorNormalize(vectorFromPoint(adjacentPoint, center)), pointFrom<GlobalPoint>(
(Math.PI / 2) as Radians, element.x + element.width,
), element.y + element.height,
Math.sign(focus) * ),
Math.min( pointFrom<GlobalPoint>(element.x, element.y + element.height),
pointDistance(pointFrom<GlobalPoint>(element.x, element.y), center) * ].map((p) =>
Math.abs(focus), pointFromVector(
element.width / 2, vectorScale(vectorFromPoint(p, center), Math.abs(focus)),
element.height / 2, center,
),
), ),
center,
); );
const selected = [
adjacentPoint[1] < candidates[0][1] && // TOP
(focus > 0
? adjacentPoint[0] < candidates[1][0]
: adjacentPoint[0] > candidates[0][0]),
adjacentPoint[0] > candidates[1][0] && // RIGHT
(focus > 0
? adjacentPoint[1] < candidates[2][1]
: adjacentPoint[1] > candidates[1][1]),
adjacentPoint[1] > candidates[2][1] && // BOTTOM
(focus > 0
? adjacentPoint[0] > candidates[3][0]
: adjacentPoint[0] < candidates[2][0]),
adjacentPoint[0] < candidates[3][0] && // LEFT
(focus > 0
? adjacentPoint[1] < candidates[3][1]
: adjacentPoint[1] > candidates[0][1]),
];
const focusPoint = selected[0]
? focus > 0
? candidates[1]
: candidates[0]
: selected[1]
? focus > 0
? candidates[2]
: candidates[1]
: selected[2]
? focus > 0
? candidates[3]
: candidates[2]
: focus > 0
? candidates[0]
: candidates[3];
return focusPoint;
}; };
export const bindingProperties: Set<BindableProp | BindingProp> = new Set([ export const bindingProperties: Set<BindableProp | BindingProp> = new Set([

@ -156,6 +156,7 @@ export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
export const intersectElementWithLineSegment = ( export const intersectElementWithLineSegment = (
element: ExcalidrawElement, element: ExcalidrawElement,
line: LineSegment<GlobalPoint>, line: LineSegment<GlobalPoint>,
offset: number = 0,
): GlobalPoint[] => { ): GlobalPoint[] => {
switch (element.type) { switch (element.type) {
case "rectangle": case "rectangle":
@ -165,7 +166,7 @@ export const intersectElementWithLineSegment = (
case "embeddable": case "embeddable":
case "frame": case "frame":
case "magicframe": case "magicframe":
return intersectRectanguloidWithLineSegment(element, line); return intersectRectanguloidWithLineSegment(element, line, offset);
case "diamond": case "diamond":
return intersectDiamondWithLineSegment(element, line); return intersectDiamondWithLineSegment(element, line);
case "ellipse": case "ellipse":
@ -178,6 +179,7 @@ export const intersectElementWithLineSegment = (
const intersectRectanguloidWithLineSegment = ( const intersectRectanguloidWithLineSegment = (
element: ExcalidrawRectanguloidElement, element: ExcalidrawRectanguloidElement,
l: LineSegment<GlobalPoint>, l: LineSegment<GlobalPoint>,
offset: number = 0,
): GlobalPoint[] => { ): GlobalPoint[] => {
const center = pointFrom<GlobalPoint>( const center = pointFrom<GlobalPoint>(
element.x + element.width / 2, element.x + element.width / 2,
@ -197,7 +199,10 @@ const intersectRectanguloidWithLineSegment = (
); );
// Get the element's building components we can test against // Get the element's building components we can test against
const [sides, corners] = deconstructRectanguloidElement<GlobalPoint>(element); const [sides, corners] = deconstructRectanguloidElement<GlobalPoint>(
element,
offset,
);
return ( return (
[ [

@ -26,10 +26,14 @@ export function deconstructRectanguloidElement<
Point extends GlobalPoint | LocalPoint, Point extends GlobalPoint | LocalPoint,
>( >(
element: ExcalidrawRectanguloidElement, element: ExcalidrawRectanguloidElement,
offset: number = 0,
): [LineSegment<Point>[], Curve<Point>[]] { ): [LineSegment<Point>[], Curve<Point>[]] {
const r = rectangle( const r = rectangle(
pointFrom(element.x, element.y), pointFrom(element.x - offset, element.y - offset),
pointFrom(element.x + element.width, element.y + element.height), pointFrom(
element.x + element.width + offset,
element.y + element.height + offset,
),
); );
const roundness = getCornerRadius( const roundness = getCornerRadius(
Math.min(element.width, element.height), Math.min(element.width, element.height),

Loading…
Cancel
Save