Fixed gap binding

This commit is contained in:
Mark Tolmacs 2025-02-28 22:07:47 +01:00
parent f363fcabd8
commit dca9fbe306
12 changed files with 169 additions and 216 deletions

View file

@ -13,6 +13,8 @@ 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,23 +463,10 @@ export const maybeBindLinearElement = (
} }
}; };
const normalizePointBinding = ( const normalizePointBinding = (binding: { focus: number; gap: number }) => {
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, gap: FIXED_BINDING_DISTANCE,
}; };
}; };
@ -729,7 +716,7 @@ const calculateFocusAndGap = (
return { return {
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint), focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)), gap: FIXED_BINDING_DISTANCE,
}; };
}; };
@ -747,7 +734,7 @@ export const updateBoundElements = (
changedElements?: Map<string, OrderedExcalidrawElement>; changedElements?: Map<string, OrderedExcalidrawElement>;
}, },
) => { ) => {
const { newSize, simultaneouslyUpdated } = options ?? {}; const { simultaneouslyUpdated } = options ?? {};
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated, simultaneouslyUpdated,
); );
@ -780,23 +767,13 @@ 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(element, bindings, true); mutateElement(
element,
{ startBinding: element.startBinding, endBinding: element.endBinding },
true,
);
return; return;
} }
@ -818,7 +795,9 @@ export const updateBoundElements = (
const point = updateBoundPoint( const point = updateBoundPoint(
element, element,
bindingProp, bindingProp,
bindings[bindingProp], bindingProp === "startBinding"
? element.startBinding
: element.endBinding,
bindableElement, bindableElement,
elementsMap, elementsMap,
); );
@ -1040,10 +1019,7 @@ export const avoidRectangularCorner = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
p: GlobalPoint, p: GlobalPoint,
): GlobalPoint => { ): GlobalPoint => {
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
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) {
@ -1226,7 +1202,6 @@ const updateBoundPoint = (
linearElement, linearElement,
bindableElement, bindableElement,
startOrEnd === "startBinding" ? "start" : "end", startOrEnd === "startBinding" ? "start" : "end",
elementsMap,
).fixedPoint; ).fixedPoint;
const globalMidPoint = pointFrom<GlobalPoint>( const globalMidPoint = pointFrom<GlobalPoint>(
bindableElement.x + bindableElement.width / 2, bindableElement.x + bindableElement.width / 2,
@ -1336,7 +1311,6 @@ export const calculateFixedPointForElbowArrowBinding = (
linearElement: NonDeleted<ExcalidrawElbowArrowElement>, linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap,
): { fixedPoint: FixedPoint } => { ): { fixedPoint: FixedPoint } => {
const bounds = [ const bounds = [
hoveredElement.x, hoveredElement.x,
@ -1369,28 +1343,6 @@ 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",

View file

@ -78,8 +78,8 @@ describe("elbow arrow segment move", () => {
expect(arrow.points).toCloselyEqualPoints([ expect(arrow.points).toCloselyEqualPoints([
[0, 0], [0, 0],
[110, 0], [110, 0],
[110, 200], [110, 195.01],
[190, 200], [190, 195.01],
]); ]);
mouse.reset(); mouse.reset();
@ -89,8 +89,8 @@ describe("elbow arrow segment move", () => {
expect(arrow.points).toCloselyEqualPoints([ expect(arrow.points).toCloselyEqualPoints([
[0, 0], [0, 0],
[110, 0], [110, 0],
[110, 200], [110, 195.01],
[190, 200], [190, 195.01],
]); ]);
}); });
@ -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).toEqual([ expect(arrow.points).toCloselyEqualPoints([
[0, 0], [0, 0],
[45, 0], [45, 0],
[45, 200], [45, 199.07],
[90, 200], [90.07, 199.07],
]); ]);
}); });
}); });
@ -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(-43, -99); mouse.moveTo(-50, -100);
mouse.click(); mouse.click();
mouse.moveTo(43, 99); mouse.moveTo(50, 100);
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).toEqual([ expect(arrow.points).toCloselyEqualPoints([
[0, 0], [0, 0],
[45, 0], [45, 0],
[45, 200], [45, 195.01],
[90, 200], [90, 195.01],
]); ]);
}); });
@ -293,12 +293,11 @@ describe("elbow arrow ui", () => {
".drag-input", ".drag-input",
) as HTMLInputElement; ) as HTMLInputElement;
UI.updateInput(inputAngle, String("40")); UI.updateInput(inputAngle, String("40"));
console.log(JSON.stringify(h.elements))
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([ expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
[0, 0], [0, 0],
[35, 0], [109, 0],
[35, 165], [109, 152],
[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, 0); expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80.62, 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); expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.78);
UI.resize(rectangle, "se", [-200, -150]); UI.resize(rectangle, "se", [-200, -150]);
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1); expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.78);
}); });
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); expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.78);
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: -30, x: -29,
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(31, 0); expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 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(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo( expect(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(143, 0); expect(leftBoundArrow.width).toBeCloseTo(146.46, 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).toBeCloseTo(10); expect(leftBoundArrow.endBinding?.gap).toEqual(FIXED_BINDING_DISTANCE);
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).toBeCloseTo(8.0952); expect(rightBoundArrow.endBinding?.gap).toEqual(FIXED_BINDING_DISTANCE);
expect(rightBoundArrow.endBinding?.elementId).toBe( expect(rightBoundArrow.endBinding?.elementId).toBe(
rightArrowBinding.elementId, rightArrowBinding.elementId,
); );

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": 11.562288374879595, "gap": 5,
}, },
"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": 1, "gap": 5,
}, },
"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": 3.8343264684446097, "gap": 5,
}, },
"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": 4.545343408287929, "gap": 5,
}, },
"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": 14, "gap": 5,
}, },
"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": 1, "gap": 5,
}, },
"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": 1, "gap": 5,
}, },
"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": 1, "gap": 5,
}, },
"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": 1, "gap": 5,
}, },
"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": 1, "gap": 5,
}, },
"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.299874999999986, "gap": 5,
}, },
"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": 1, "gap": 5,
}, },
"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": 14, "gap": 5,
}, },
"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": 1, "gap": 5,
}, },
"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: 1, gap: FIXED_BINDING_DISTANCE,
}, },
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: 1, gap: FIXED_BINDING_DISTANCE,
}, },
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: 14, gap: 5,
}); });
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": "102.35417", "height": "99.23572",
"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,
], ],
[ [
"101.77517", "96.42891",
"102.35417", "99.23572",
], ],
], ],
"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": "101.77517", "width": "96.42891",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": 0,
} }
`; `;
@ -296,46 +296,46 @@ History {
"endBinding": { "endBinding": {
"elementId": "id171", "elementId": "id171",
"focus": "0.00990", "focus": "0.00990",
"gap": 1, "gap": 5,
}, },
"height": "0.98586", "height": "0.92998",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"98.58579", "92.92893",
"-0.98586", "-0.92998",
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id170", "elementId": "id170",
"focus": "0.02970", "focus": "0.02970",
"gap": 1, "gap": 5,
}, },
}, },
"inserted": { "inserted": {
"endBinding": { "endBinding": {
"elementId": "id171", "elementId": "id171",
"focus": "-0.02000", "focus": "-0.02000",
"gap": 1, "gap": 5,
}, },
"height": "0.00000", "height": "0.00611",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"98.58579", "92.92893",
"0.00000", "0.00611",
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id170", "elementId": "id170",
"focus": "0.02000", "focus": "0.02000",
"gap": 1, "gap": 5,
}, },
}, },
}, },
@ -390,15 +390,15 @@ History {
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
}, },
"height": "102.35417", "height": "99.23572",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"101.77517", "96.42891",
"102.35417", "99.23572",
], ],
], ],
"startBinding": null, "startBinding": null,
@ -408,25 +408,25 @@ History {
"endBinding": { "endBinding": {
"elementId": "id171", "elementId": "id171",
"focus": "0.00990", "focus": "0.00990",
"gap": 1, "gap": 5,
}, },
"height": "0.98586", "height": "0.93503",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"98.58579", "92.92893",
"-0.98586", "-0.93503",
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id170", "elementId": "id170",
"focus": "0.02970", "focus": "0.02970",
"gap": 1, "gap": 5,
}, },
"y": "0.99364", "y": "0.97365",
}, },
}, },
"id175" => Delta { "id175" => Delta {
@ -931,7 +931,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id166", "elementId": "id166",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"points": [ "points": [
[ [
@ -946,7 +946,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id165", "elementId": "id165",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
}, },
}, },
@ -1241,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.30038", "height": "2.98409",
"id": "id178", "id": "id178",
"index": "Zz", "index": "Zz",
"isDeleted": false, "isDeleted": false,
@ -1255,8 +1255,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"98.58579", "92.92893",
"1.30038", "-2.98409",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1279,9 +1279,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 11, "version": 11,
"width": "98.58579", "width": "92.92893",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": "4.70319",
} }
`; `;
@ -1613,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.30038", "height": "2.98409",
"id": "id181", "id": "id181",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
@ -1627,8 +1627,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"98.58579", "92.92893",
"1.30038", "-2.98409",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1651,9 +1651,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 11, "version": 11,
"width": "98.58579", "width": "92.92893",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": "4.70319",
} }
`; `;
@ -1771,7 +1771,7 @@ History {
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": "11.27227", "height": "22.46459",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -1784,8 +1784,8 @@ History {
0, 0,
], ],
[ [
"98.58579", "93.46683",
"11.27227", "-22.46459",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1806,9 +1806,9 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": "98.58579", "width": "93.46683",
"x": "0.70711", "x": "2.99764",
"y": 0, "y": "35.33176",
}, },
"inserted": { "inserted": {
"isDeleted": true, "isDeleted": true,
@ -2321,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": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": "374.05754", "height": "408.02337",
"id": "id186", "id": "id186",
"index": "a2", "index": "a2",
"isDeleted": false, "isDeleted": false,
@ -2340,8 +2340,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"502.78936", "495.48945",
"-374.05754", "-408.02337",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -2352,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": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -2360,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": "502.78936", "width": "495.48945",
"x": "-0.83465", "x": "3.53553",
"y": "-36.58211", "y": 0,
} }
`; `;
@ -2481,7 +2481,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id185", "elementId": "id185",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -2511,7 +2511,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id184", "elementId": "id184",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -15161,7 +15161,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"endBinding": { "endBinding": {
"elementId": "id58", "elementId": "id58",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -15180,7 +15180,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"98.58579", "92.92893",
0, 0,
], ],
], ],
@ -15192,7 +15192,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": { "startBinding": {
"elementId": "id56", "elementId": "id56",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -15200,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": "98.58579", "width": "92.92893",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": 0,
} }
`; `;
@ -15532,7 +15532,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id58", "elementId": "id58",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -15562,7 +15562,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id56", "elementId": "id56",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -15859,7 +15859,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"endBinding": { "endBinding": {
"elementId": "id52", "elementId": "id52",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -15878,7 +15878,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"98.58579", "92.92893",
0, 0,
], ],
], ],
@ -15890,7 +15890,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": { "startBinding": {
"elementId": "id50", "elementId": "id50",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -15898,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": "98.58579", "width": "92.92893",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": 0,
} }
`; `;
@ -16152,7 +16152,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id52", "elementId": "id52",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -16182,7 +16182,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id50", "elementId": "id50",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -16479,7 +16479,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"endBinding": { "endBinding": {
"elementId": "id64", "elementId": "id64",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -16498,7 +16498,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"98.58579", "92.92893",
0, 0,
], ],
], ],
@ -16510,7 +16510,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": { "startBinding": {
"elementId": "id62", "elementId": "id62",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -16518,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": "98.58579", "width": "92.92893",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": 0,
} }
`; `;
@ -16772,7 +16772,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id64", "elementId": "id64",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -16802,7 +16802,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id62", "elementId": "id62",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -17097,7 +17097,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"endBinding": { "endBinding": {
"elementId": "id70", "elementId": "id70",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -17116,7 +17116,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"98.58579", "92.92893",
0, 0,
], ],
], ],
@ -17128,7 +17128,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": { "startBinding": {
"elementId": "id68", "elementId": "id68",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -17136,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": "98.58579", "width": "92.92893",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": 0,
} }
`; `;
@ -17200,7 +17200,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id68", "elementId": "id68",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
}, },
"inserted": { "inserted": {
@ -17460,7 +17460,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id70", "elementId": "id70",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -17490,7 +17490,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id68", "elementId": "id68",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -17811,7 +17811,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"endBinding": { "endBinding": {
"elementId": "id77", "elementId": "id77",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -17830,7 +17830,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"98.58579", "92.92893",
0, 0,
], ],
], ],
@ -17842,7 +17842,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"startBinding": { "startBinding": {
"elementId": "id75", "elementId": "id75",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@ -17850,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": "98.58579", "width": "92.92893",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": 0,
} }
`; `;
@ -17913,7 +17913,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id77", "elementId": "id77",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"points": [ "points": [
[ [
@ -17928,7 +17928,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id75", "elementId": "id75",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
}, },
"inserted": { "inserted": {
@ -18189,7 +18189,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id77", "elementId": "id77",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -18219,7 +18219,7 @@ History {
"startBinding": { "startBinding": {
"elementId": "id75", "elementId": "id75",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"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": 10, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": "87.29887", "height": "87.97595",
"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,
], ],
[ [
"86.85786", "87.46447",
"87.29887", "87.97595",
], ],
], ],
"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": 10, "gap": 5,
}, },
"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": "86.85786", "width": "87.46447",
"x": "107.07107", "x": 110,
"y": "47.07107", "y": 50,
} }
`; `;

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: 1, gap: FIXED_BINDING_DISTANCE,
}), }),
endBinding: expect.objectContaining({ endBinding: expect.objectContaining({
elementId: rect2.id, elementId: rect2.id,
focus: -0, focus: -0,
gap: 1, gap: FIXED_BINDING_DISTANCE,
}), }),
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(204, 0); expect(arrow.width).toBeCloseTo(206.86, 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

@ -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(116.7, 1); expect(arrow.width).toBeCloseTo(119.58, 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(48.98, 1); expect(ellipseArrow.points[1][0]).toBeCloseTo(54.36, 1);
expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1); expect(ellipseArrow.points[1][1]).toBeCloseTo(139.61, 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(-94, 0); expect(textArrow.points[1][0]).toBeCloseTo(-100.12, 0);
expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0); expect(textArrow.points[1][1]).toBeCloseTo(-123.63, 0);
}); });

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, precision: number = (Math.log(1 / PRECISION) * Math.LOG10E + 1) | 0,
func: "round" | "floor" | "ceil" = "round", func: "round" | "floor" | "ceil" = "round",
) => { ) => {
const multiplier = Math.pow(10, precision); const multiplier = Math.pow(10, precision);