Fix elbow arrow binding logic

feat/remove-ga
Mark Tolmacs 1 week ago
parent fa8db3cdc0
commit 5d352161fd
No known key found for this signature in database

@ -46,11 +46,9 @@ import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
import { import {
compareHeading, compareHeading,
HEADING_DOWN, HEADING_DOWN,
HEADING_LEFT,
HEADING_RIGHT, HEADING_RIGHT,
HEADING_UP, HEADING_UP,
headingForPointFromElement, headingForPointFromElement,
headingIsHorizontal,
vectorToHeading, vectorToHeading,
type Heading, type Heading,
} from "./heading"; } from "./heading";
@ -72,9 +70,8 @@ import {
vectorNormalize, vectorNormalize,
vectorRotate, vectorRotate,
} from "../../math"; } from "../../math";
import { distanceToBindableElement } from "./distance";
import { intersectElementWithLineSegment } from "./collision"; import { intersectElementWithLineSegment } from "./collision";
import { debugClear, debugDrawLine } from "../visualdebug"; import { distanceToBindableElement } from "./distance";
export type SuggestedBinding = export type SuggestedBinding =
| NonDeleted<ExcalidrawBindableElement> | NonDeleted<ExcalidrawBindableElement>
@ -728,12 +725,29 @@ const calculateFocusAndGap = (
adjacentPointIndex, adjacentPointIndex,
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: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint), focus,
gap: Math.max( gap,
1,
determineGapSize(edgePoint, adjacentPoint, hoveredElement, zoom),
),
}; };
}; };
@ -928,53 +942,44 @@ export const bindPointToSnapToElementOutline = (
const p = isRectanguloidElement(bindableElement) const p = isRectanguloidElement(bindableElement)
? avoidRectangularCorner(bindableElement, globalP) ? avoidRectangularCorner(bindableElement, globalP)
: globalP; : globalP;
const localOtherPoint =
arrow.points[startOrEnd === "start" ? arrow.points.length - 1 : 0];
const otherPoint = pointFrom<GlobalPoint>(
arrow.x + localOtherPoint[0],
arrow.y + localOtherPoint[1],
);
const prev =
arrow.points[startOrEnd === "start" ? 1 : arrow.points.length - 2];
const isHorizontal = headingIsHorizontal(
vectorToHeading(vectorFromPoint(localP, prev)),
);
if (bindableElement && aabb) { if (bindableElement && aabb) {
const heading = headingForPointFromElement(bindableElement, aabb, p);
const center = getCenterForBounds(aabb); const center = getCenterForBounds(aabb);
const intersections = (
intersectElementWithLineSegment( const intersection = intersectElementWithLineSegment(
bindableElement, bindableElement,
lineSegment( lineSegment(
p, center,
pointFrom( pointFromVector(
isHorizontal ? center[0] : p[0], vectorScale(
!isHorizontal ? center[1] : p[1], vectorNormalize(vectorFromPoint(p, center)),
Math.max(bindableElement.width, bindableElement.height),
), ),
center,
), ),
FIXED_BINDING_DISTANCE, ),
) ?? [] )[0];
) const currentDistance = pointDistance(p, center);
.filter((p) => p != null) const fullDistance = pointDistance(intersection, center);
.sort((g, h) => pointDistanceSq(g!, p) - pointDistanceSq(h!, p)); const ratio = currentDistance / fullDistance;
const isVertical =
compareHeading(heading, HEADING_LEFT) || switch (true) {
compareHeading(heading, HEADING_RIGHT); case ratio > 0.9:
const dist = Math.abs(distanceToBindableElement(bindableElement, p)); if (currentDistance - fullDistance > FIXED_BINDING_DISTANCE) {
const isInner = isVertical return p;
? dist < bindableElement.width * -0.1 }
: dist < bindableElement.height * -0.1;
return pointFromVector(
intersections.sort((a, b) => pointDistanceSq(a, p) - pointDistanceSq(b, p)); vectorScale(
vectorNormalize(vectorFromPoint(p, intersection)),
return isInner ratio > 1 ? FIXED_BINDING_DISTANCE : -FIXED_BINDING_DISTANCE,
? headingToMidBindPoint(otherPoint, bindableElement, aabb) ),
: intersections.filter((i) => intersection,
isVertical );
? Math.abs(p[1] - i[1]) < 0.1
: Math.abs(p[0] - i[0]) < 0.1, default:
)[0] ?? p; return headingToMidBindPoint(p, bindableElement, aabb);
}
} }
return p; return p;
@ -1255,25 +1260,27 @@ const updateBoundPoint = (
elementsMap, elementsMap,
); );
const intersections = intersectElementWithLineSegment( const intersection =
bindableElement, intersectElementWithLineSegment(
lineSegment(adjacentPoint, focusPointAbsolute), bindableElement,
binding.gap, lineSegment<GlobalPoint>(adjacentPoint, focusPointAbsolute),
); ).sort(
if (!intersections || intersections.length === 0) {
// This should never happen, since focusPoint should always be
// inside the element, but just in case, bail out
// Note: Might happen with rounded elements due to FP imprecision
newEdgePoint = edgePointAbsolute;
} else {
// Guaranteed to intersect because focusPoint is always inside the shape
intersections.sort(
(g, h) => (g, h) =>
pointDistanceSq(g!, edgePointAbsolute) - pointDistanceSq(g!, edgePointAbsolute) -
pointDistanceSq(h!, edgePointAbsolute), pointDistanceSq(h!, edgePointAbsolute),
); )[0] ?? edgePointAbsolute;
newEdgePoint = intersections[0];
} const gapOffsetPoint = intersection
? pointFromVector(
vectorScale(
vectorNormalize(vectorFromPoint(adjacentPoint, intersection)),
binding.gap,
),
intersection,
)
: edgePointAbsolute;
newEdgePoint = gapOffsetPoint;
} }
return LinearElementEditor.pointFromAbsoluteCoords( return LinearElementEditor.pointFromAbsoluteCoords(
@ -1581,42 +1588,6 @@ const determineFocusDistance = (
); );
}; };
/**
* Determines gap size between element and edgePoint by intersecting the element
* in the direction of adjacentPoint -> edgePoint, then measuring the length of
* the intersection point and edgePoint.
*
* NOTE: This is not always the same as distance from edgePoint to the closest
* point on the element outline!
*/
const determineGapSize = (
edgePoint: GlobalPoint,
adjacentPoint: GlobalPoint,
element: ExcalidrawBindableElement,
zoom?: AppState["zoom"],
): number => {
const s = lineSegment(
edgePoint,
pointFromVector(
// Create a vector from the adjacent point to the edge point
// scale it to cross the maximum gap distance + 1 to ensure intersection
vectorScale(
vectorNormalize(vectorFromPoint(edgePoint, adjacentPoint)),
maxBindingGap(element, element.width, element.height, zoom) + 1,
),
edgePoint,
),
);
debugClear();
debugDrawLine(s, { color: "red", permanent: true });
const distances = intersectElementWithLineSegment(element, s)
.map((p) => pointDistance(edgePoint, p))
.sort((a, b) => b - a);
const distance = distances.pop();
console.log(distance);
return distance ?? 0;
};
const determineFocusPoint = ( const determineFocusPoint = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
// The oriented, relative distance from the center of `element` of the // The oriented, relative distance from the center of `element` of the

@ -156,7 +156,6 @@ 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":
@ -166,11 +165,11 @@ export const intersectElementWithLineSegment = (
case "embeddable": case "embeddable":
case "frame": case "frame":
case "magicframe": case "magicframe":
return intersectRectanguloidWithLineSegment(element, line, offset); return intersectRectanguloidWithLineSegment(element, line);
case "diamond": case "diamond":
return intersectDiamondWithLineSegment(element, line, offset); return intersectDiamondWithLineSegment(element, line);
case "ellipse": case "ellipse":
return intersectEllipseWithLineSegment(element, line, offset); return intersectEllipseWithLineSegment(element, line);
default: default:
throw new Error(`Unimplemented element type '${element.type}'`); throw new Error(`Unimplemented element type '${element.type}'`);
} }
@ -179,7 +178,6 @@ export const intersectElementWithLineSegment = (
const intersectRectanguloidWithLineSegment = ( const intersectRectanguloidWithLineSegment = (
element: ExcalidrawRectanguloidElement, element: ExcalidrawRectanguloidElement,
l: LineSegment<GlobalPoint>, l: LineSegment<GlobalPoint>,
offset: number,
): GlobalPoint[] => { ): GlobalPoint[] => {
const center = pointFrom<GlobalPoint>( const center = pointFrom<GlobalPoint>(
element.x + element.width / 2, element.x + element.width / 2,
@ -199,10 +197,7 @@ 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>( const [sides, corners] = deconstructRectanguloidElement<GlobalPoint>(element);
element,
offset,
);
return ( return (
[ [
@ -244,7 +239,6 @@ const intersectRectanguloidWithLineSegment = (
const intersectDiamondWithLineSegment = ( const intersectDiamondWithLineSegment = (
element: ExcalidrawDiamondElement, element: ExcalidrawDiamondElement,
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,
@ -256,7 +250,7 @@ const intersectDiamondWithLineSegment = (
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians); const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians); const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
const [sides, curves] = deconstructDiamondElement(element, offset); const [sides, curves] = deconstructDiamondElement(element);
return ( return (
[ [
@ -295,7 +289,6 @@ const intersectDiamondWithLineSegment = (
const intersectEllipseWithLineSegment = ( const intersectEllipseWithLineSegment = (
element: ExcalidrawEllipseElement, element: ExcalidrawEllipseElement,
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,
@ -306,7 +299,7 @@ const intersectEllipseWithLineSegment = (
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians); const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
return ellipseLineIntersectionPoints( return ellipseLineIntersectionPoints(
ellipse(center, element.width / 2 + offset, element.height / 2 + offset), ellipse(center, element.width / 2, element.height / 2),
line(rotatedA, rotatedB), line(rotatedA, rotatedB),
).map((p) => pointRotateRads(p, center, element.angle)); ).map((p) => pointRotateRads(p, center, element.angle));
}; };

@ -45,7 +45,7 @@ export const distanceToBindableElement = (
* @param p The point to consider * @param p The point to consider
* @returns The eucledian distance to the outline of the rectanguloid element * @returns The eucledian distance to the outline of the rectanguloid element
*/ */
export const distanceToRectanguloidElement = ( const distanceToRectanguloidElement = (
element: ExcalidrawRectanguloidElement, element: ExcalidrawRectanguloidElement,
p: GlobalPoint, p: GlobalPoint,
) => { ) => {
@ -76,7 +76,7 @@ export const distanceToRectanguloidElement = (
* @param p The point to consider * @param p The point to consider
* @returns The eucledian distance to the outline of the diamond * @returns The eucledian distance to the outline of the diamond
*/ */
export const distanceToDiamondElement = ( const distanceToDiamondElement = (
element: ExcalidrawDiamondElement, element: ExcalidrawDiamondElement,
p: GlobalPoint, p: GlobalPoint,
): number => { ): number => {
@ -107,7 +107,7 @@ export const distanceToDiamondElement = (
* @param p The point to consider * @param p The point to consider
* @returns The eucledian distance to the outline of the ellipse * @returns The eucledian distance to the outline of the ellipse
*/ */
export const distanceToEllipseElement = ( const distanceToEllipseElement = (
element: ExcalidrawEllipseElement, element: ExcalidrawEllipseElement,
p: GlobalPoint, p: GlobalPoint,
): number => { ): number => {

@ -26,14 +26,10 @@ 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 - offset, element.y - offset), pointFrom(element.x, element.y),
pointFrom( pointFrom(element.x + element.width, element.y + element.height),
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