Revert to master

This commit is contained in:
Mark Tolmacs 2025-03-24 14:39:48 +01:00
parent 1a87aa8e55
commit 4d1e2c2bbb
25 changed files with 410 additions and 577 deletions

View file

@ -13,8 +13,6 @@ import { useCallback, useImperativeHandle, useRef } from "react";
import { import {
isLineSegment, isLineSegment,
isCurve,
type Curve,
type GlobalPoint, type GlobalPoint,
type LineSegment, type LineSegment,
} from "@excalidraw/math"; } from "@excalidraw/math";

View file

@ -463,10 +463,23 @@ export const maybeBindLinearElement = (
} }
}; };
const normalizePointBinding = (binding: { focus: number; gap: number }) => { const normalizePointBinding = (
binding: { focus: number; gap: number },
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 { return {
...binding, ...binding,
gap: FIXED_BINDING_DISTANCE, gap,
}; };
}; };
@ -716,7 +729,7 @@ const calculateFocusAndGap = (
return { return {
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint), focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
gap: FIXED_BINDING_DISTANCE, gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
}; };
}; };
@ -734,7 +747,7 @@ export const updateBoundElements = (
changedElements?: Map<string, OrderedExcalidrawElement>; changedElements?: Map<string, OrderedExcalidrawElement>;
}, },
) => { ) => {
const { simultaneouslyUpdated } = options ?? {}; const { newSize, simultaneouslyUpdated } = options ?? {};
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated, simultaneouslyUpdated,
); );
@ -767,13 +780,23 @@ export const updateBoundElements = (
startBounds = getElementBounds(startBindingElement, elementsMap); startBounds = getElementBounds(startBindingElement, elementsMap);
endBounds = getElementBounds(endBindingElement, elementsMap); endBounds = getElementBounds(endBindingElement, elementsMap);
} }
const bindings = {
startBinding: maybeCalculateNewGapWhenScaling(
changedElement,
element.startBinding,
newSize,
),
endBinding: maybeCalculateNewGapWhenScaling(
changedElement,
element.endBinding,
newSize,
),
};
// `linearElement` is being moved/scaled already, just update the binding // `linearElement` is being moved/scaled already, just update the binding
if (simultaneouslyUpdatedElementIds.has(element.id)) { if (simultaneouslyUpdatedElementIds.has(element.id)) {
mutateElement( mutateElement(element, bindings, true);
element,
{ startBinding: element.startBinding, endBinding: element.endBinding },
true,
);
return; return;
} }
@ -795,9 +818,7 @@ export const updateBoundElements = (
const point = updateBoundPoint( const point = updateBoundPoint(
element, element,
bindingProp, bindingProp,
bindingProp === "startBinding" bindings[bindingProp],
? element.startBinding
: element.endBinding,
bindableElement, bindableElement,
elementsMap, elementsMap,
); );
@ -917,7 +938,6 @@ 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");
@ -925,12 +945,10 @@ export const bindPointToSnapToElementOutline = (
const aabb = aabbForElement(bindableElement); const aabb = aabbForElement(bindableElement);
const localP = const localP =
linearElement.points[ arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
startOrEnd === "start" ? 0 : linearElement.points.length - 1
];
const globalP = pointFrom<GlobalPoint>( const globalP = pointFrom<GlobalPoint>(
linearElement.x + localP[0], arrow.x + localP[0],
linearElement.y + localP[1], arrow.y + localP[1],
); );
const edgePoint = isRectanguloidElement(bindableElement) const edgePoint = isRectanguloidElement(bindableElement)
? avoidRectangularCorner(bindableElement, globalP) ? avoidRectangularCorner(bindableElement, globalP)
@ -967,11 +985,7 @@ export const bindPointToSnapToElementOutline = (
), ),
otherPoint, otherPoint,
), ),
adjacentPoint,
), ),
).sort(
(g, h) =>
pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint),
)[0]; )[0];
} else { } else {
intersection = intersectElementWithLineSegment( intersection = intersectElementWithLineSegment(
@ -1019,20 +1033,6 @@ export const bindPointToSnapToElementOutline = (
); );
} }
const currentDistance = pointDistance(edgePoint, center);
const fullDistance = Math.max(
pointDistance(intersection ?? edgePoint, center),
1e-5, // Avoid division by zero
);
if (!isInside) {
return intersection;
}
if (elbowed) {
return headingToMidBindPoint(edgePoint, bindableElement, aabb);
}
return edgePoint; return edgePoint;
}; };
@ -1040,7 +1040,10 @@ export const avoidRectangularCorner = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
p: GlobalPoint, p: GlobalPoint,
): GlobalPoint => { ): GlobalPoint => {
const center = elementCenterPoint(element); const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
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) {
@ -1310,24 +1313,6 @@ const updateBoundPoint = (
), ),
]; ];
// debugClear();
// intersections.forEach((intersection) => {
// debugDrawPoint(intersection, { permanent: true, color: "red" });
// });
// debugDrawLine(
// lineSegment<GlobalPoint>(
// adjacentPoint,
// pointFromVector(
// vectorScale(
// vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)),
// interceptorLength,
// ),
// adjacentPoint,
// ),
// ),
// { permanent: true, color: "green" },
// );
if (intersections.length > 1) { if (intersections.length > 1) {
// The adjacent point is outside the shape (+ gap) // The adjacent point is outside the shape (+ gap)
newEdgePoint = intersections[0]; newEdgePoint = intersections[0];
@ -1363,7 +1348,6 @@ 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,
@ -1385,6 +1369,28 @@ export const calculateFixedPointForElbowArrowBinding = (
}; };
}; };
const maybeCalculateNewGapWhenScaling = (
changedElement: ExcalidrawBindableElement,
currentBinding: PointBinding | null | undefined,
newSize: { width: number; height: number } | undefined,
): PointBinding | null | undefined => {
if (currentBinding == null || newSize == null) {
return currentBinding;
}
const { width: newWidth, height: newHeight } = newSize;
const { width, height } = changedElement;
const newGap = Math.max(
1,
Math.min(
maxBindingGap(changedElement, newWidth, newHeight),
currentBinding.gap *
(newWidth < newHeight ? newWidth / width : newHeight / height),
),
);
return { ...currentBinding, gap: newGap };
};
const getElligibleElementForBindingElement = ( const getElligibleElementForBindingElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
@ -1765,7 +1771,10 @@ const determineFocusDistance = (
// 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 = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
if (pointsEqual(a, b)) { if (pointsEqual(a, b)) {
return 0; return 0;
@ -1895,7 +1904,10 @@ const determineFocusPoint = (
focus: number, focus: number,
adjacentPoint: GlobalPoint, adjacentPoint: GlobalPoint,
): GlobalPoint => { ): GlobalPoint => {
const center = elementCenterPoint(element); const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
if (focus === 0) { if (focus === 0) {
return center; return center;
@ -2326,7 +2338,10 @@ 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), pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
),
element.angle, element.angle,
); );
}; };

View file

@ -41,7 +41,6 @@ import {
import { import {
deconstructDiamondElement, deconstructDiamondElement,
deconstructRectanguloidElement, deconstructRectanguloidElement,
elementCenterPoint,
} from "./utils"; } from "./utils";
import type { import type {
@ -192,7 +191,10 @@ const intersectRectanguloidWithLineSegment = (
l: LineSegment<GlobalPoint>, l: LineSegment<GlobalPoint>,
offset: number = 0, offset: number = 0,
): GlobalPoint[] => { ): GlobalPoint[] => {
const center = elementCenterPoint(element); const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
// To emulate a rotated rectangle we rotate the point in the inverse angle // To emulate a rotated rectangle we rotate the point in the inverse angle
// instead. It's all the same distance-wise. // instead. It's all the same distance-wise.
const rotatedA = pointRotateRads<GlobalPoint>( const rotatedA = pointRotateRads<GlobalPoint>(
@ -210,32 +212,31 @@ const intersectRectanguloidWithLineSegment = (
const [sides, corners] = deconstructRectanguloidElement(element, offset); const [sides, corners] = deconstructRectanguloidElement(element, offset);
return ( return (
// Test intersection against the sides, keep only the valid [
// intersection points and rotate them back to scene space // Test intersection against the sides, keep only the valid
sides // intersection points and rotate them back to scene space
.map((s) => ...sides
lineSegmentIntersectionPoints( .map((s) =>
lineSegment<GlobalPoint>(rotatedA, rotatedB), lineSegmentIntersectionPoints(
s, lineSegment<GlobalPoint>(rotatedA, rotatedB),
), s,
) ),
.filter((x) => x != null) )
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle)) .filter((x) => x != null)
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle)),
// Test intersection against the corners which are cubic bezier curves, // Test intersection against the corners which are cubic bezier curves,
// keep only the valid intersection points and rotate them back to scene // keep only the valid intersection points and rotate them back to scene
// space // space
.concat( ...corners
corners .flatMap((t) =>
.flatMap((t) => curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)), )
) .filter((i) => i != null)
.filter((i) => i != null) .map((j) => pointRotateRads(j, center, element.angle)),
.map((j) => pointRotateRads(j, center, element.angle)), ]
)
// Remove duplicates // Remove duplicates
.filter( .filter(
(p, idx, points) => (p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
points.findIndex((d) => pointsEqual(p, d, 1e-3)) === idx,
) )
); );
}; };
@ -252,7 +253,10 @@ const intersectDiamondWithLineSegment = (
l: LineSegment<GlobalPoint>, l: LineSegment<GlobalPoint>,
offset: number = 0, offset: number = 0,
): GlobalPoint[] => { ): GlobalPoint[] => {
const center = elementCenterPoint(element); const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
// Rotate the point to the inverse direction to simulate the rotated diamond // Rotate the point to the inverse direction to simulate the rotated diamond
// points. It's all the same distance-wise. // points. It's all the same distance-wise.
@ -262,29 +266,28 @@ const intersectDiamondWithLineSegment = (
const [sides, curves] = deconstructDiamondElement(element, offset); const [sides, curves] = deconstructDiamondElement(element, offset);
return ( return (
sides [
.map((s) => ...sides
lineSegmentIntersectionPoints( .map((s) =>
lineSegment<GlobalPoint>(rotatedA, rotatedB), lineSegmentIntersectionPoints(
s, lineSegment<GlobalPoint>(rotatedA, rotatedB),
), s,
) ),
.filter((p): p is GlobalPoint => p != null) )
// Rotate back intersection points .filter((p): p is GlobalPoint => p != null)
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle)) // Rotate back intersection points
.concat( .map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle)),
curves ...curves
.flatMap((p) => .flatMap((p) =>
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)), curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
) )
.filter((p) => p != null) .filter((p) => p != null)
// Rotate back intersection points // Rotate back intersection points
.map((p) => pointRotateRads(p, center, element.angle)), .map((p) => pointRotateRads(p, center, element.angle)),
) ]
// Remove duplicates // Remove duplicates
.filter( .filter(
(p, idx, points) => (p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
points.findIndex((d) => pointsEqual(p, d, 1e-3)) === idx,
) )
); );
}; };
@ -301,7 +304,10 @@ const intersectEllipseWithLineSegment = (
l: LineSegment<GlobalPoint>, l: LineSegment<GlobalPoint>,
offset: number = 0, offset: number = 0,
): GlobalPoint[] => { ): GlobalPoint[] => {
const center = elementCenterPoint(element); const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
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);

View file

@ -61,7 +61,7 @@ export const cropElement = (
const rotatedPointer = pointRotateRads( const rotatedPointer = pointRotateRads(
pointFrom(pointerX, pointerY), pointFrom(pointerX, pointerY),
elementCenterPoint(element), pointFrom(element.x + element.width / 2, element.y + element.height / 2),
-element.angle as Radians, -element.angle as Radians,
); );

View file

@ -1,6 +1,7 @@
import { import {
curvePointDistance, curvePointDistance,
distanceToLineSegment, distanceToLineSegment,
pointFrom,
pointRotateRads, pointRotateRads,
} from "@excalidraw/math"; } from "@excalidraw/math";
@ -52,7 +53,10 @@ const distanceToRectanguloidElement = (
element: ExcalidrawRectanguloidElement, element: ExcalidrawRectanguloidElement,
p: GlobalPoint, p: GlobalPoint,
) => { ) => {
const center = elementCenterPoint(element); const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
// To emulate a rotated rectangle we rotate the point in the inverse angle // To emulate a rotated rectangle we rotate the point in the inverse angle
// instead. It's all the same distance-wise. // instead. It's all the same distance-wise.
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians); const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
@ -80,7 +84,10 @@ const distanceToDiamondElement = (
element: ExcalidrawDiamondElement, element: ExcalidrawDiamondElement,
p: GlobalPoint, p: GlobalPoint,
): number => { ): number => {
const center = elementCenterPoint(element); const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
// Rotate the point to the inverse direction to simulate the rotated diamond // Rotate the point to the inverse direction to simulate the rotated diamond
// points. It's all the same distance-wise. // points. It's all the same distance-wise.
@ -108,7 +115,10 @@ const distanceToEllipseElement = (
element: ExcalidrawEllipseElement, element: ExcalidrawEllipseElement,
p: GlobalPoint, p: GlobalPoint,
): number => { ): number => {
const center = elementCenterPoint(element); const center = pointFrom(
element.x + element.width / 2,
element.y + element.height / 2,
);
return ellipseDistanceFromPoint( return ellipseDistanceFromPoint(
// Instead of rotating the ellipse, rotate the point to the inverse angle // Instead of rotating the ellipse, rotate the point to the inverse angle
pointRotateRads(p, center, -element.angle as Radians), pointRotateRads(p, center, -element.angle as Radians),

View file

@ -1249,7 +1249,6 @@ const getElbowArrowData = (
...arrow, ...arrow,
type: "arrow", type: "arrow",
elbowed: true, elbowed: true,
type: "arrow",
points: nextPoints, points: nextPoints,
} as ExcalidrawElbowArrowElement, } as ExcalidrawElbowArrowElement,
"start", "start",
@ -1263,7 +1262,6 @@ const getElbowArrowData = (
...arrow, ...arrow,
type: "arrow", type: "arrow",
elbowed: true, elbowed: true,
type: "arrow",
points: nextPoints, points: nextPoints,
} as ExcalidrawElbowArrowElement, } as ExcalidrawElbowArrowElement,
"end", "end",
@ -2223,7 +2221,6 @@ const getGlobalPoint = (
arrow, arrow,
element, element,
startOrEnd, startOrEnd,
elementsMap,
); );
return snapToMid(element, snapPoint); return snapToMid(element, snapPoint);

View file

@ -239,43 +239,6 @@ export class LinearElementEditor {
}); });
} }
static getOutlineAvoidingPoint(
element: NonDeleted<ExcalidrawLinearElement>,
coords: GlobalPoint,
pointIndex: number,
app: AppClassProperties,
fallback?: GlobalPoint,
): GlobalPoint {
const hoveredElement = getHoveredElementForBinding(
{ x: coords[0], y: coords[1] },
app.scene.getNonDeletedElements(),
app.scene.getNonDeletedElementsMap(),
app.state.zoom,
true,
isElbowArrow(element),
);
if (hoveredElement) {
const newPoints = Array.from(element.points);
newPoints[pointIndex] = pointFrom<LocalPoint>(
coords[0] - element.x,
coords[1] - element.y,
);
return bindPointToSnapToElementOutline(
{
...element,
points: newPoints,
},
hoveredElement,
pointIndex === 0 ? "start" : "end",
app.scene.getNonDeletedElementsMap(),
);
}
return fallback ?? coords;
}
/** /**
* @returns whether point was dragged * @returns whether point was dragged
*/ */
@ -295,16 +258,14 @@ export class LinearElementEditor {
return null; return null;
} }
const { elementId } = linearElementEditor; const { elementId } = linearElementEditor;
const elementsMap = app.scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return null; return null;
} }
const elbowed = isElbowArrow(element);
if ( if (
elbowed && isElbowArrow(element) &&
!linearElementEditor.pointerDownState.lastClickedIsEndPoint && !linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
linearElementEditor.pointerDownState.lastClickedPoint !== 0 linearElementEditor.pointerDownState.lastClickedPoint !== 0
) { ) {
@ -321,7 +282,7 @@ export class LinearElementEditor {
: undefined, : undefined,
].filter((idx): idx is number => idx !== undefined) ].filter((idx): idx is number => idx !== undefined)
: linearElementEditor.selectedPointsIndices; : linearElementEditor.selectedPointsIndices;
const lastClickedPoint = elbowed const lastClickedPoint = isElbowArrow(element)
? linearElementEditor.pointerDownState.lastClickedPoint > 0 ? linearElementEditor.pointerDownState.lastClickedPoint > 0
? element.points.length - 1 ? element.points.length - 1
: 0 : 0
@ -373,43 +334,19 @@ export class LinearElementEditor {
LinearElementEditor.movePoints( LinearElementEditor.movePoints(
element, element,
selectedPointsIndices.map((pointIndex) => { selectedPointsIndices.map((pointIndex) => {
let newPointPosition = pointFrom<LocalPoint>( const newPointPosition: LocalPoint =
element.points[pointIndex][0] + deltaX, pointIndex === lastClickedPoint
element.points[pointIndex][1] + deltaY, ? LinearElementEditor.createPointAt(
);
// Check if point dragging is happening
if (pointIndex === lastClickedPoint) {
let globalNewPointPosition = pointFrom<GlobalPoint>(
scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y,
);
if (
pointIndex === 0 ||
pointIndex === element.points.length - 1
) {
globalNewPointPosition =
LinearElementEditor.getOutlineAvoidingPoint(
element, element,
pointFrom<GlobalPoint>( elementsMap,
element.x + element.points[pointIndex][0] + deltaX, scenePointerX - linearElementEditor.pointerOffset.x,
element.y + element.points[pointIndex][1] + deltaY, scenePointerY - linearElementEditor.pointerOffset.y,
), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
pointIndex, )
app, : pointFrom(
element.points[pointIndex][0] + deltaX,
element.points[pointIndex][1] + deltaY,
); );
}
newPointPosition = LinearElementEditor.createPointAt(
element,
elementsMap,
globalNewPointPosition[0],
globalNewPointPosition[1],
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
);
}
return { return {
index: pointIndex, index: pointIndex,
point: newPointPosition, point: newPointPosition,

View file

@ -18,7 +18,6 @@ import { getDiamondPoints } from "./bounds";
import type { import type {
ExcalidrawDiamondElement, ExcalidrawDiamondElement,
ExcalidrawElement,
ExcalidrawRectanguloidElement, ExcalidrawRectanguloidElement,
} from "./types"; } from "./types";
@ -69,7 +68,10 @@ export function deconstructRectanguloidElement(
return [sides, []]; return [sides, []];
} }
const center = elementCenterPoint(element); const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const r = rectangle( const r = rectangle(
pointFrom(element.x, element.y), pointFrom(element.x, element.y),
@ -252,7 +254,10 @@ export function deconstructDiamondElement(
return [[topRight, bottomRight, bottomLeft, topLeft], []]; return [[topRight, bottomRight, bottomLeft, topLeft], []];
} }
const center = elementCenterPoint(element); const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const [top, right, bottom, left]: GlobalPoint[] = [ const [top, right, bottom, left]: GlobalPoint[] = [
pointFrom(element.x + topX, element.y + topY), pointFrom(element.x + topX, element.y + topY),
@ -352,10 +357,3 @@ export function deconstructDiamondElement(
return [sides, corners]; return [sides, corners];
} }
export function elementCenterPoint(element: ExcalidrawElement) {
return pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
}

View file

@ -190,18 +190,7 @@ describe("element binding", () => {
// Sever connection // Sever connection
expect(API.getSelectedElement().type).toBe("arrow"); expect(API.getSelectedElement().type).toBe("arrow");
Keyboard.withModifierKeys({ shift: true }, () => { Keyboard.keyPress(KEYS.ARROW_LEFT);
// We have to move a significant distance to get out of the binding zone
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
});
expect(arrow.endBinding).toBe(null); expect(arrow.endBinding).toBe(null);
Keyboard.keyPress(KEYS.ARROW_RIGHT); Keyboard.keyPress(KEYS.ARROW_RIGHT);
expect(arrow.endBinding).toBe(null); expect(arrow.endBinding).toBe(null);

View file

@ -77,9 +77,9 @@ describe("elbow arrow segment move", () => {
expect(arrow.points).toCloselyEqualPoints([ expect(arrow.points).toCloselyEqualPoints([
[0, 0], [0, 0],
[107.93, 0], [110, 0],
[107.93, 185.86], [110, 200],
[185.86, 185.86], [190, 200],
]); ]);
mouse.reset(); mouse.reset();
@ -88,9 +88,9 @@ describe("elbow arrow segment move", () => {
expect(arrow.points).toCloselyEqualPoints([ expect(arrow.points).toCloselyEqualPoints([
[0, 0], [0, 0],
[107.93, 0], [110, 0],
[107.93, 185.86], [110, 200],
[185.86, 185.86], [190, 200],
]); ]);
}); });
@ -198,11 +198,11 @@ describe("elbow arrow routing", () => {
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)], points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
}); });
expect(arrow.points).toCloselyEqualPoints([ expect(arrow.points).toEqual([
[0, 0], [0, 0],
[42.93, 0], [45, 0],
[42.93, 195.7], [45, 200],
[85.86, 195.7], [90, 200],
]); ]);
}); });
}); });
@ -241,9 +241,9 @@ describe("elbow arrow ui", () => {
expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow); expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow);
mouse.reset(); mouse.reset();
mouse.moveTo(-50, -100); mouse.moveTo(-43, -99);
mouse.click(); mouse.click();
mouse.moveTo(50, 100); mouse.moveTo(43, 99);
mouse.click(); mouse.click();
const arrow = h.scene.getSelectedElements( const arrow = h.scene.getSelectedElements(
@ -252,11 +252,11 @@ describe("elbow arrow ui", () => {
expect(arrow.type).toBe("arrow"); expect(arrow.type).toBe("arrow");
expect(arrow.elbowed).toBe(true); expect(arrow.elbowed).toBe(true);
expect(arrow.points).toCloselyEqualPoints([ expect(arrow.points).toEqual([
[0, 0], [0, 0],
[42.93, 0], [45, 0],
[42.93, 153.48], [45, 200],
[85.86, 153.48], [90, 200],
]); ]);
}); });
@ -296,8 +296,9 @@ describe("elbow arrow ui", () => {
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([ expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
[0, 0], [0, 0],
[129, 0], [35, 0],
[129, 131], [35, 165],
[103, 165],
]); ]);
}); });

View file

@ -195,7 +195,7 @@ describe("generic element", () => {
UI.resize(rectangle, "w", [50, 0]); UI.resize(rectangle, "w", [50, 0]);
expect(arrow.endBinding?.elementId).toEqual(rectangle.id); expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80.62, 0); expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0);
}); });
it("resizes with a label", async () => { it("resizes with a label", async () => {
@ -510,13 +510,13 @@ describe("arrow element", () => {
h.state, h.state,
)[0] as ExcalidrawElbowArrowElement; )[0] as ExcalidrawElbowArrowElement;
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.07); expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.86); expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
UI.resize(rectangle, "se", [-200, -150]); UI.resize(rectangle, "se", [-200, -150]);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.07); expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.86); expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
}); });
it("flips the fixed point binding on negative resize for group selection", () => { it("flips the fixed point binding on negative resize for group selection", () => {
@ -538,8 +538,8 @@ describe("arrow element", () => {
h.state, h.state,
)[0] as ExcalidrawElbowArrowElement; )[0] as ExcalidrawElbowArrowElement;
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.07); expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.86); expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
UI.resize([rectangle, arrow], "nw", [300, 350]); UI.resize([rectangle, arrow], "nw", [300, 350]);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0); expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0);
@ -809,7 +809,7 @@ describe("image element", () => {
}); });
API.setElements([image]); API.setElements([image]);
const arrow = UI.createElement("arrow", { const arrow = UI.createElement("arrow", {
x: -29, x: -30,
y: 50, y: 50,
width: 28, width: 28,
height: 5, height: 5,
@ -819,14 +819,14 @@ describe("image element", () => {
UI.resize(image, "ne", [40, 0]); UI.resize(image, "ne", [40, 0]);
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0); expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(31, 0);
const imageWidth = image.width; const imageWidth = image.width;
const scale = 20 / image.height; const scale = 20 / image.height;
UI.resize(image, "nw", [50, 20]); UI.resize(image, "nw", [50, 20]);
expect(arrow.endBinding?.elementId).toEqual(image.id); expect(arrow.endBinding?.elementId).toEqual(image.id);
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo( expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo(
30 + imageWidth * scale, 30 + imageWidth * scale,
0, 0,
); );
@ -1033,11 +1033,11 @@ describe("multiple selection", () => {
expect(leftBoundArrow.x).toBeCloseTo(-110); expect(leftBoundArrow.x).toBeCloseTo(-110);
expect(leftBoundArrow.y).toBeCloseTo(50); expect(leftBoundArrow.y).toBeCloseTo(50);
expect(leftBoundArrow.width).toBeCloseTo(146.46, 0); expect(leftBoundArrow.width).toBeCloseTo(143, 0);
expect(leftBoundArrow.height).toBeCloseTo(7, 0); expect(leftBoundArrow.height).toBeCloseTo(7, 0);
expect(leftBoundArrow.angle).toEqual(0); expect(leftBoundArrow.angle).toEqual(0);
expect(leftBoundArrow.startBinding).toBeNull(); expect(leftBoundArrow.startBinding).toBeNull();
expect(leftBoundArrow.endBinding?.gap).toEqual(FIXED_BINDING_DISTANCE); expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10);
expect(leftBoundArrow.endBinding?.elementId).toBe( expect(leftBoundArrow.endBinding?.elementId).toBe(
leftArrowBinding.elementId, leftArrowBinding.elementId,
); );
@ -1051,7 +1051,7 @@ describe("multiple selection", () => {
expect(rightBoundArrow.height).toBeCloseTo(0); expect(rightBoundArrow.height).toBeCloseTo(0);
expect(rightBoundArrow.angle).toEqual(0); expect(rightBoundArrow.angle).toEqual(0);
expect(rightBoundArrow.startBinding).toBeNull(); expect(rightBoundArrow.startBinding).toBeNull();
expect(rightBoundArrow.endBinding?.gap).toEqual(FIXED_BINDING_DISTANCE); expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
expect(rightBoundArrow.endBinding?.elementId).toBe( expect(rightBoundArrow.endBinding?.elementId).toBe(
rightArrowBinding.elementId, rightArrowBinding.elementId,
); );

View file

@ -91,26 +91,10 @@ export const actionFinalize = register({
multiPointElement.type !== "freedraw" && multiPointElement.type !== "freedraw" &&
appState.lastPointerDownWith !== "touch" appState.lastPointerDownWith !== "touch"
) { ) {
const { x: rx, y: ry, points, lastCommittedPoint } = multiPointElement; const { points, lastCommittedPoint } = multiPointElement;
const lastGlobalPoint = pointFrom<GlobalPoint>(
rx + points[points.length - 1][0],
ry + points[points.length - 1][1],
);
const hoveredElementForBinding = getHoveredElementForBinding(
{
x: lastGlobalPoint[0],
y: lastGlobalPoint[1],
},
elements,
elementsMap,
app.state.zoom,
true,
isElbowArrow(multiPointElement),
);
if ( if (
!hoveredElementForBinding && !lastCommittedPoint ||
(!lastCommittedPoint || points[points.length - 1] !== lastCommittedPoint
points[points.length - 1] !== lastCommittedPoint)
) { ) {
mutateElement(multiPointElement, { mutateElement(multiPointElement, {
points: multiPointElement.points.slice(0, -1), points: multiPointElement.points.slice(0, -1),

View file

@ -1655,7 +1655,6 @@ export const actionChangeArrowType = register({
newElement, newElement,
startHoveredElement, startHoveredElement,
"start", "start",
elementsMap,
) )
: startGlobalPoint; : startGlobalPoint;
const finalEndPoint = endHoveredElement const finalEndPoint = endHoveredElement
@ -1663,7 +1662,6 @@ export const actionChangeArrowType = register({
newElement, newElement,
endHoveredElement, endHoveredElement,
"end", "end",
elementsMap,
) )
: endGlobalPoint; : endGlobalPoint;

View file

@ -5991,25 +5991,15 @@ class App extends React.Component<AppProps, AppState> {
if (isPathALoop(points, this.state.zoom.value)) { if (isPathALoop(points, this.state.zoom.value)) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
} }
// update last uncommitted point // update last uncommitted point
mutateElement( mutateElement(
multiElement, multiElement,
{ {
points: [ points: [
...points.slice(0, -1), ...points.slice(0, -1),
pointTranslate<GlobalPoint, LocalPoint>( pointFrom<LocalPoint>(
LinearElementEditor.getOutlineAvoidingPoint( lastCommittedX + dxFromLastCommitted,
multiElement, lastCommittedY + dyFromLastCommitted,
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
multiElement.points.length - 1,
this,
pointFrom<GlobalPoint>(
multiElement.x + lastCommittedX + dxFromLastCommitted,
multiElement.y + lastCommittedY + dyFromLastCommitted,
),
),
vector(-multiElement.x, -multiElement.y),
), ),
], ],
}, },
@ -7761,34 +7751,18 @@ class App extends React.Component<AppProps, AppState> {
} }
const { x: rx, y: ry, lastCommittedPoint } = multiElement; const { x: rx, y: ry, lastCommittedPoint } = multiElement;
const lastGlobalPoint = pointFrom<GlobalPoint>(
rx + multiElement.points[multiElement.points.length - 1][0],
ry + multiElement.points[multiElement.points.length - 1][1],
);
const hoveredElementForBinding = getHoveredElementForBinding(
{
x: lastGlobalPoint[0],
y: lastGlobalPoint[1],
},
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
true,
isElbowArrow(multiElement),
);
// clicking inside commit zone → finalize arrow // clicking inside commit zone → finalize arrow
if ( if (
!!hoveredElementForBinding || multiElement.points.length > 1 &&
(multiElement.points.length > 1 && lastCommittedPoint &&
lastCommittedPoint && pointDistance(
pointDistance( pointFrom(
pointFrom( pointerDownState.origin.x - rx,
pointerDownState.origin.x - rx, pointerDownState.origin.y - ry,
pointerDownState.origin.y - ry, ),
), lastCommittedPoint,
lastCommittedPoint, ) < LINE_CONFIRM_THRESHOLD
) < LINE_CONFIRM_THRESHOLD)
) { ) {
this.actionManager.executeAction(actionFinalize); this.actionManager.executeAction(actionFinalize);
return; return;
@ -7832,93 +7806,53 @@ class App extends React.Component<AppProps, AppState> {
? [currentItemStartArrowhead, currentItemEndArrowhead] ? [currentItemStartArrowhead, currentItemEndArrowhead]
: [null, null]; : [null, null];
let element: NonDeleted<ExcalidrawLinearElement>; const element =
if (elementType === "arrow") { elementType === "arrow"
const arrow: Mutable<NonDeleted<ExcalidrawArrowElement>> = ? newArrowElement({
newArrowElement({ type: elementType,
type: "arrow", x: gridX,
x: gridX, y: gridY,
y: gridY, strokeColor: this.state.currentItemStrokeColor,
strokeColor: this.state.currentItemStrokeColor, backgroundColor: this.state.currentItemBackgroundColor,
backgroundColor: this.state.currentItemBackgroundColor, fillStyle: this.state.currentItemFillStyle,
fillStyle: this.state.currentItemFillStyle, strokeWidth: this.state.currentItemStrokeWidth,
strokeWidth: this.state.currentItemStrokeWidth, strokeStyle: this.state.currentItemStrokeStyle,
strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness,
roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity,
opacity: this.state.currentItemOpacity, roundness:
roundness: this.state.currentItemArrowType === ARROW_TYPE.round
this.state.currentItemArrowType === ARROW_TYPE.round ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
? { type: ROUNDNESS.PROPORTIONAL_RADIUS } : // note, roundness doesn't have any effect for elbow arrows,
: // note, roundness doesn't have any effect for elbow arrows, // but it's best to set it to null as well
// but it's best to set it to null as well null,
null, startArrowhead,
startArrowhead, endArrowhead,
endArrowhead, locked: false,
locked: false, frameId: topLayerFrame ? topLayerFrame.id : null,
frameId: topLayerFrame ? topLayerFrame.id : null, elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow, fixedSegments:
fixedSegments: this.state.currentItemArrowType === ARROW_TYPE.elbow
this.state.currentItemArrowType === ARROW_TYPE.elbow ? [] : null, ? []
}); : null,
})
const hoveredElement = getHoveredElementForBinding( : newLinearElement({
{ x: gridX, y: gridY }, type: elementType,
this.scene.getNonDeletedElements(), x: gridX,
this.scene.getNonDeletedElementsMap(), y: gridY,
this.state.zoom, strokeColor: this.state.currentItemStrokeColor,
true, backgroundColor: this.state.currentItemBackgroundColor,
this.state.currentItemArrowType === ARROW_TYPE.elbow, fillStyle: this.state.currentItemFillStyle,
); strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
if (hoveredElement) { roughness: this.state.currentItemRoughness,
[arrow.x, arrow.y] = opacity: this.state.currentItemOpacity,
intersectElementWithLineSegment( roundness:
hoveredElement, this.state.currentItemRoundness === "round"
lineSegment( ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
pointFrom<GlobalPoint>(gridX, gridY), : null,
pointFrom<GlobalPoint>( locked: false,
gridX, frameId: topLayerFrame ? topLayerFrame.id : null,
hoveredElement.y + hoveredElement.height / 2, });
),
),
2 * FIXED_BINDING_DISTANCE,
)[0] ??
intersectElementWithLineSegment(
hoveredElement,
lineSegment(
pointFrom<GlobalPoint>(gridX, gridY),
pointFrom<GlobalPoint>(
hoveredElement.x + hoveredElement.width / 2,
gridY,
),
),
2 * FIXED_BINDING_DISTANCE,
)[0] ??
pointFrom<GlobalPoint>(gridX, gridY);
}
element = arrow;
} else {
element = newLinearElement({
type: elementType,
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
roundness:
this.state.currentItemRoundness === "round"
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
: null,
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null,
});
}
this.setState((prevState) => { this.setState((prevState) => {
const nextSelectedElementIds = { const nextSelectedElementIds = {
...prevState.selectedElementIds, ...prevState.selectedElementIds,
@ -8233,6 +8167,12 @@ class App extends React.Component<AppProps, AppState> {
this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y); this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y);
} }
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
);
// for arrows/lines, don't start dragging until a given threshold // for arrows/lines, don't start dragging until a given threshold
// to ensure we don't create a 2-point arrow by mistake when // to ensure we don't create a 2-point arrow by mistake when
// user clicks mouse in a way that it moves a tiny bit (thus // user clicks mouse in a way that it moves a tiny bit (thus
@ -8333,6 +8273,7 @@ class App extends React.Component<AppProps, AppState> {
); );
}, },
linearElementEditor, linearElementEditor,
this.scene,
); );
if (newLinearElementEditor) { if (newLinearElementEditor) {
pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.x = pointerCoords.x;
@ -8719,11 +8660,6 @@ class App extends React.Component<AppProps, AppState> {
} else if (isLinearElement(newElement)) { } else if (isLinearElement(newElement)) {
pointerDownState.drag.hasOccurred = true; pointerDownState.drag.hasOccurred = true;
const points = newElement.points; const points = newElement.points;
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
);
let dx = gridX - newElement.x; let dx = gridX - newElement.x;
let dy = gridY - newElement.y; let dy = gridY - newElement.y;
@ -8740,22 +8676,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [ points: [...points, pointFrom<LocalPoint>(dx, dy)],
...points,
pointTranslate<GlobalPoint, LocalPoint>(
LinearElementEditor.getOutlineAvoidingPoint(
newElement,
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
newElement.points.length - 1,
this,
pointFrom<GlobalPoint>(
newElement.x + dx,
newElement.y + dy,
),
),
vector(-newElement.x, -newElement.y),
),
],
}, },
false, false,
); );
@ -8766,22 +8687,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [ points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
...points.slice(0, -1),
pointTranslate<GlobalPoint, LocalPoint>(
LinearElementEditor.getOutlineAvoidingPoint(
newElement,
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
newElement.points.length - 1,
this,
pointFrom<GlobalPoint>(
newElement.x + dx,
newElement.y + dy,
),
),
vector(-newElement.x, -newElement.y),
),
],
}, },
false, false,
{ isDragging: true }, { isDragging: true },

View file

@ -89,7 +89,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"endBinding": { "endBinding": {
"elementId": "ellipse-1", "elementId": "ellipse-1",
"focus": -0.007519379844961235, "focus": -0.007519379844961235,
"gap": 5, "gap": 11.562288374879595,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -119,7 +119,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"startBinding": { "startBinding": {
"elementId": "id49", "elementId": "id49",
"focus": -0.0813953488372095, "focus": -0.0813953488372095,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1864ab", "strokeColor": "#1864ab",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -145,7 +145,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"endBinding": { "endBinding": {
"elementId": "ellipse-1", "elementId": "ellipse-1",
"focus": 0.10666666666666667, "focus": 0.10666666666666667,
"gap": 5, "gap": 3.8343264684446097,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -175,7 +175,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"startBinding": { "startBinding": {
"elementId": "diamond-1", "elementId": "diamond-1",
"focus": 0, "focus": 0,
"gap": 5, "gap": 4.545343408287929,
}, },
"strokeColor": "#e67700", "strokeColor": "#e67700",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -335,7 +335,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"endBinding": { "endBinding": {
"elementId": "text-2", "elementId": "text-2",
"focus": 0, "focus": 0,
"gap": 5, "gap": 14,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -365,7 +365,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"startBinding": { "startBinding": {
"elementId": "text-1", "elementId": "text-1",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -437,7 +437,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"endBinding": { "endBinding": {
"elementId": "id42", "elementId": "id42",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -467,7 +467,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"startBinding": { "startBinding": {
"elementId": "id41", "elementId": "id41",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -613,7 +613,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"endBinding": { "endBinding": {
"elementId": "id46", "elementId": "id46",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -643,7 +643,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"startBinding": { "startBinding": {
"elementId": "id45", "elementId": "id45",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"endBinding": { "endBinding": {
"elementId": "Alice", "elementId": "Alice",
"focus": -0, "focus": -0,
"gap": 5, "gap": 5.299874999999986,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -1507,7 +1507,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"startBinding": { "startBinding": {
"elementId": "Bob", "elementId": "Bob",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -1538,7 +1538,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"endBinding": { "endBinding": {
"elementId": "B", "elementId": "B",
"focus": 0, "focus": 0,
"gap": 5, "gap": 14,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -1566,7 +1566,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"startBinding": { "startBinding": {
"elementId": "Bob", "elementId": "Bob",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",

View file

@ -433,7 +433,7 @@ describe("Test Transform", () => {
startBinding: { startBinding: {
elementId: rectangle.id, elementId: rectangle.id,
focus: 0, focus: 0,
gap: FIXED_BINDING_DISTANCE, gap: 1,
}, },
endBinding: { endBinding: {
elementId: ellipse.id, elementId: ellipse.id,
@ -518,7 +518,7 @@ describe("Test Transform", () => {
startBinding: { startBinding: {
elementId: text2.id, elementId: text2.id,
focus: 0, focus: 0,
gap: FIXED_BINDING_DISTANCE, gap: 1,
}, },
endBinding: { endBinding: {
elementId: text3.id, elementId: text3.id,
@ -781,7 +781,7 @@ describe("Test Transform", () => {
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({ expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
elementId: "rect-1", elementId: "rect-1",
focus: -0, focus: -0,
gap: FIXED_BINDING_DISTANCE, gap: 14,
}); });
expect(rect.boundElements).toStrictEqual([ expect(rect.boundElements).toStrictEqual([
{ {

View file

@ -198,7 +198,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": "99.58947", "height": "102.35417",
"id": "id172", "id": "id172",
"index": "a2", "index": "a2",
"isDeleted": false, "isDeleted": false,
@ -212,8 +212,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"99.58947", "101.77517",
"99.58947", "102.35417",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -228,8 +228,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 40, "version": 40,
"width": "99.58947", "width": "101.77517",
"x": 0, "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -296,49 +296,47 @@ History {
"endBinding": { "endBinding": {
"elementId": "id171", "elementId": "id171",
"focus": "0.00990", "focus": "0.00990",
"gap": 5, "gap": 1,
}, },
"height": "0.92929", "height": "0.98586",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"92.92893", "98.58579",
"-0.92929", "-0.98586",
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id170", "elementId": "id170",
"focus": "0.02970", "focus": "0.02970",
"gap": 5, "gap": 1,
}, },
"width": "92.92893",
}, },
"inserted": { "inserted": {
"endBinding": { "endBinding": {
"elementId": "id171", "elementId": "id171",
"focus": "-0.02075", "focus": "-0.02000",
"gap": 5, "gap": 1,
}, },
"height": "0.07074", "height": "0.00000",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"92.92893", "98.58579",
"0.07074", "0.00000",
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id170", "elementId": "id170",
"focus": "0.01770", "focus": "0.02000",
"gap": 5, "gap": 1,
}, },
"width": "92.92893",
}, },
}, },
}, },
@ -392,47 +390,43 @@ History {
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
}, },
"height": "99.58947", "height": "102.35417",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"99.58947", "101.77517",
"99.58947", "102.35417",
], ],
], ],
"startBinding": null, "startBinding": null,
"width": "99.58947",
"x": 0,
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {
"endBinding": { "endBinding": {
"elementId": "id171", "elementId": "id171",
"focus": "0.00990", "focus": "0.00990",
"gap": 5, "gap": 1,
}, },
"height": "0.92929", "height": "0.98586",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"92.92893", "98.58579",
"-0.92929", "-0.98586",
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id170", "elementId": "id170",
"focus": "0.02970", "focus": "0.02970",
"gap": 5, "gap": 1,
}, },
"width": "92.92893", "y": "0.99364",
"x": "3.53553",
"y": "0.96033",
}, },
}, },
"id175" => Delta { "id175" => Delta {
@ -864,7 +858,6 @@ History {
0, 0,
], ],
], ],
"width": 0,
}, },
"inserted": { "inserted": {
"points": [ "points": [
@ -873,11 +866,10 @@ History {
0, 0,
], ],
[ [
"85.85786", 100,
0, 0,
], ],
], ],
"width": "85.85786",
}, },
}, },
}, },
@ -934,14 +926,12 @@ History {
], ],
], ],
"startBinding": null, "startBinding": null,
"width": 100,
"x": 150,
}, },
"inserted": { "inserted": {
"endBinding": { "endBinding": {
"elementId": "id166", "elementId": "id166",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"points": [ "points": [
[ [
@ -956,10 +946,8 @@ History {
"startBinding": { "startBinding": {
"elementId": "id165", "elementId": "id165",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"width": 0,
"x": "146.46447",
}, },
}, },
}, },
@ -1253,7 +1241,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": "1.71911", "height": "1.30038",
"id": "id178", "id": "id178",
"index": "Zz", "index": "Zz",
"isDeleted": false, "isDeleted": false,
@ -1267,8 +1255,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"92.92893", "98.58579",
"1.71911", "1.30038",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1291,8 +1279,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 11, "version": 11,
"width": "92.92893", "width": "98.58579",
"x": "3.53553", "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -1625,7 +1613,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": "1.71911", "height": "1.30038",
"id": "id181", "id": "id181",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
@ -1639,8 +1627,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"92.92893", "98.58579",
"1.71911", "1.30038",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1663,8 +1651,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 11, "version": 11,
"width": "92.92893", "width": "98.58579",
"x": "3.53553", "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -1783,7 +1771,7 @@ History {
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": "12.86717", "height": "11.27227",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -1796,8 +1784,8 @@ History {
0, 0,
], ],
[ [
"92.92893", "98.58579",
"12.86717", "11.27227",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1818,8 +1806,8 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": "92.92893", "width": "98.58579",
"x": "3.53553", "x": "0.70711",
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {
@ -2333,12 +2321,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"endBinding": { "endBinding": {
"elementId": "id185", "elementId": "id185",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": "369.21589", "height": "374.05754",
"id": "id186", "id": "id186",
"index": "a2", "index": "a2",
"isDeleted": false, "isDeleted": false,
@ -2352,8 +2340,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"496.84035", "502.78936",
"-369.21589", "-374.05754",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -2364,7 +2352,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"startBinding": { "startBinding": {
"elementId": "id184", "elementId": "id184",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -2372,9 +2360,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 10,
"width": "496.84035", "width": "502.78936",
"x": "2.18463", "x": "-0.83465",
"y": "-38.80748", "y": "-36.58211",
} }
`; `;
@ -2493,7 +2481,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id185", "elementId": "id185",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -2523,7 +2511,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id184", "elementId": "id184",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -15173,7 +15161,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"endBinding": { "endBinding": {
"elementId": "id58", "elementId": "id58",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -15192,7 +15180,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"92.92893", "98.58579",
0, 0,
], ],
], ],
@ -15204,7 +15192,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": { "startBinding": {
"elementId": "id56", "elementId": "id56",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -15212,8 +15200,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 10,
"width": "92.92893", "width": "98.58579",
"x": "3.53553", "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -15544,7 +15532,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id58", "elementId": "id58",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -15574,7 +15562,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id56", "elementId": "id56",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -15871,7 +15859,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"endBinding": { "endBinding": {
"elementId": "id52", "elementId": "id52",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -15890,7 +15878,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"92.92893", "98.58579",
0, 0,
], ],
], ],
@ -15902,7 +15890,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": { "startBinding": {
"elementId": "id50", "elementId": "id50",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -15910,8 +15898,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 10,
"width": "92.92893", "width": "98.58579",
"x": "3.53553", "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -16164,7 +16152,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id52", "elementId": "id52",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -16194,7 +16182,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id50", "elementId": "id50",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -16491,7 +16479,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"endBinding": { "endBinding": {
"elementId": "id64", "elementId": "id64",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -16510,7 +16498,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"92.92893", "98.58579",
0, 0,
], ],
], ],
@ -16522,7 +16510,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": { "startBinding": {
"elementId": "id62", "elementId": "id62",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -16530,8 +16518,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 10,
"width": "92.92893", "width": "98.58579",
"x": "3.53553", "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -16784,7 +16772,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id64", "elementId": "id64",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -16814,7 +16802,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id62", "elementId": "id62",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -17109,7 +17097,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"endBinding": { "endBinding": {
"elementId": "id70", "elementId": "id70",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -17128,7 +17116,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"92.92893", "98.58579",
0, 0,
], ],
], ],
@ -17140,7 +17128,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": { "startBinding": {
"elementId": "id68", "elementId": "id68",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -17148,8 +17136,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 10, "version": 10,
"width": "92.92893", "width": "98.58579",
"x": "3.53553", "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -17212,7 +17200,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id68", "elementId": "id68",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
}, },
"inserted": { "inserted": {
@ -17472,7 +17460,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id70", "elementId": "id70",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -17502,7 +17490,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id68", "elementId": "id68",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -17823,7 +17811,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"endBinding": { "endBinding": {
"elementId": "id77", "elementId": "id77",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -17842,7 +17830,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"92.92893", "98.58579",
0, 0,
], ],
], ],
@ -17854,7 +17842,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": { "startBinding": {
"elementId": "id75", "elementId": "id75",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -17862,8 +17850,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 11, "version": 11,
"width": "92.92893", "width": "98.58579",
"x": "3.53553", "x": "0.70711",
"y": 0, "y": 0,
} }
`; `;
@ -17925,7 +17913,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id77", "elementId": "id77",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"points": [ "points": [
[ [
@ -17940,7 +17928,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id75", "elementId": "id75",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
}, },
"inserted": { "inserted": {
@ -18201,7 +18189,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id77", "elementId": "id77",
"focus": -0, "focus": -0,
"gap": 5, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -18231,7 +18219,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id75", "elementId": "id75",
"focus": 0, "focus": 0,
"gap": 5, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",

View file

@ -191,12 +191,12 @@ exports[`move element > rectangles with binding arrow 7`] = `
"endBinding": { "endBinding": {
"elementId": "id1", "elementId": "id1",
"focus": "-0.46667", "focus": "-0.46667",
"gap": 5, "gap": 10,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": "94.40997", "height": "87.29887",
"id": "id2", "id": "id2",
"index": "a2", "index": "a2",
"isDeleted": false, "isDeleted": false,
@ -210,8 +210,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
0, 0,
], ],
[ [
"93.92893", "86.85786",
"94.40997", "87.29887",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -223,7 +223,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
"startBinding": { "startBinding": {
"elementId": "id0", "elementId": "id0",
"focus": "-0.60000", "focus": "-0.60000",
"gap": 5, "gap": 10,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -232,8 +232,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
"updated": 1, "updated": 1,
"version": 11, "version": 11,
"versionNonce": 1051383431, "versionNonce": 1051383431,
"width": "93.92893", "width": "86.85786",
"x": "103.53553", "x": "107.07107",
"y": "43.53553", "y": "47.07107",
} }
`; `;

View file

@ -4779,12 +4779,12 @@ describe("history", () => {
startBinding: expect.objectContaining({ startBinding: expect.objectContaining({
elementId: rect1.id, elementId: rect1.id,
focus: 0, focus: 0,
gap: FIXED_BINDING_DISTANCE, gap: 1,
}), }),
endBinding: expect.objectContaining({ endBinding: expect.objectContaining({
elementId: rect2.id, elementId: rect2.id,
focus: -0, focus: -0,
gap: FIXED_BINDING_DISTANCE, gap: 1,
}), }),
isDeleted: true, isDeleted: true,
}), }),

View file

@ -1266,7 +1266,7 @@ describe("Test Linear Elements", () => {
mouse.downAt(rect.x, rect.y); mouse.downAt(rect.x, rect.y);
mouse.moveTo(200, 0); mouse.moveTo(200, 0);
mouse.upAt(200, 0); mouse.upAt(200, 0);
expect(arrow.width).toBeCloseTo(206.86, 0); expect(arrow.width).toBeCloseTo(204, 0);
expect(rect.x).toBe(200); expect(rect.x).toBe(200);
expect(rect.y).toBe(0); expect(rect.y).toBe(0);
expect(handleBindTextResizeSpy).toHaveBeenCalledWith( expect(handleBindTextResizeSpy).toHaveBeenCalledWith(

View file

@ -128,10 +128,8 @@ describe("move element", () => {
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
expect([rectA.x, rectA.y]).toEqual([0, 0]); expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([201, 2]); expect([rectB.x, rectB.y]).toEqual([201, 2]);
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[103.54, 43.53]]); expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[107.07, 47.07]]);
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([ expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[86.86, 87.3]]);
[93.93, 94.41],
]);
h.elements.forEach((element) => expect(element).toMatchSnapshot()); h.elements.forEach((element) => expect(element).toMatchSnapshot());
}); });

View file

@ -35,7 +35,7 @@ test("unselected bound arrow updates when rotating its target element", async ()
expect(arrow.endBinding?.elementId).toEqual(rectangle.id); expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
expect(arrow.x).toBeCloseTo(-80); expect(arrow.x).toBeCloseTo(-80);
expect(arrow.y).toBeCloseTo(50); expect(arrow.y).toBeCloseTo(50);
expect(arrow.width).toBeCloseTo(119.58, 1); expect(arrow.width).toBeCloseTo(116.7, 1);
expect(arrow.height).toBeCloseTo(0); expect(arrow.height).toBeCloseTo(0);
}); });
@ -72,13 +72,13 @@ test("unselected bound arrows update when rotating their target elements", async
expect(ellipseArrow.x).toEqual(0); expect(ellipseArrow.x).toEqual(0);
expect(ellipseArrow.y).toEqual(0); expect(ellipseArrow.y).toEqual(0);
expect(ellipseArrow.points[0]).toEqual([0, 0]); expect(ellipseArrow.points[0]).toEqual([0, 0]);
expect(ellipseArrow.points[1][0]).toBeCloseTo(54.36, 1); expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1);
expect(ellipseArrow.points[1][1]).toBeCloseTo(139.61, 1); expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1);
expect(textArrow.endBinding?.elementId).toEqual(text.id); expect(textArrow.endBinding?.elementId).toEqual(text.id);
expect(textArrow.x).toEqual(360); expect(textArrow.x).toEqual(360);
expect(textArrow.y).toEqual(300); expect(textArrow.y).toEqual(300);
expect(textArrow.points[0]).toEqual([0, 0]); expect(textArrow.points[0]).toEqual([0, 0]);
expect(textArrow.points[1][0]).toBeCloseTo(-100.12, 0); expect(textArrow.points[1][0]).toBeCloseTo(-94, 0);
expect(textArrow.points[1][1]).toBeCloseTo(-123.63, 0); expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0);
}); });

View file

@ -157,13 +157,22 @@ export function curveIntersectLineSegment<
return bezierEquation(c, t); return bezierEquation(c, t);
}; };
const solutions = [ let solution = calculate(initial_guesses[0]);
calculate(initial_guesses[0]), if (solution) {
calculate(initial_guesses[1]), return [solution];
calculate(initial_guesses[2]), }
].filter((x, i, a): x is Point => x !== null && a.indexOf(x) === i);
return solutions; solution = calculate(initial_guesses[1]);
if (solution) {
return [solution];
}
solution = calculate(initial_guesses[2]);
if (solution) {
return [solution];
}
return [];
} }
/** /**

View file

@ -91,10 +91,9 @@ export function isPoint(p: unknown): p is LocalPoint | GlobalPoint {
export function pointsEqual<Point extends GlobalPoint | LocalPoint>( export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
a: Point, a: Point,
b: Point, b: Point,
precision = PRECISION,
): boolean { ): boolean {
const abs = Math.abs; const abs = Math.abs;
return abs(a[0] - b[0]) < precision && abs(a[1] - b[1]) < precision; return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION;
} }
/** /**

View file

@ -6,7 +6,7 @@ export const clamp = (value: number, min: number, max: number) => {
export const round = ( export const round = (
value: number, value: number,
precision: number = (Math.log(1 / PRECISION) * Math.LOG10E + 1) | 0, precision: number,
func: "round" | "floor" | "ceil" = "round", func: "round" | "floor" | "ceil" = "round",
) => { ) => {
const multiplier = Math.pow(10, precision); const multiplier = Math.pow(10, precision);