Fixed point binding for simple arrows

This commit is contained in:
Mark Tolmacs
2025-06-18 19:21:00 +02:00
parent 2535d73054
commit 7b457238da
4 changed files with 284 additions and 76 deletions

View File

@@ -88,8 +88,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"endArrowhead": "arrow",
"endBinding": {
"elementId": "ellipse-1",
"focus": -0.007519379844961235,
"gap": 11.562288374879595,
"fixedPoint": [
0.04,
0.4633333333333333,
],
"focus": 0,
"gap": 0,
},
"fillStyle": "solid",
"frameId": null,
@@ -174,8 +178,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"startArrowhead": null,
"startBinding": {
"elementId": "diamond-1",
"fixedPoint": [
0.9357142857142857,
0.5001,
],
"focus": 0,
"gap": 4.535423522449215,
"gap": 0,
},
"strokeColor": "#e67700",
"strokeStyle": "solid",
@@ -1539,8 +1547,12 @@ exports[`Test Transform > should transform the elements correctly when linear el
"endArrowhead": "arrow",
"endBinding": {
"elementId": "B",
"fixedPoint": [
0.46387050630528887,
0.48466257668711654,
],
"focus": 0,
"gap": 32,
"gap": 0,
},
"fillStyle": "solid",
"frameId": null,

View File

@@ -28,9 +28,14 @@ import { LinearElementEditor } from "@excalidraw/element";
import { bumpVersion } from "@excalidraw/element";
import { getContainerElement } from "@excalidraw/element";
import { detectLineHeight } from "@excalidraw/element";
import {
isPointInElement,
calculateFixedPointForNonElbowArrowBinding,
} from "@excalidraw/element";
import {
isArrowBoundToElement,
isArrowElement,
isBindableElement,
isElbowArrow,
isFixedPointBinding,
isLinearElement,
@@ -521,6 +526,87 @@ const repairFrameMembership = (
}
};
/**
* Migrates old PointBinding to FixedPointBinding for non-elbow arrows
* when arrow endpoints are inside bindable shapes.
*
* NOTE mutates element.
*/
const migratePointBindingToFixedPoint = (
element: Mutable<ExcalidrawElement>,
elementsMap: Map<string, Mutable<ExcalidrawElement>>,
) => {
if (!isArrowElement(element) || isElbowArrow(element)) {
return;
}
let shouldUpdateElement = false;
let newStartBinding: FixedPointBinding | PointBinding | null =
element.startBinding;
let newEndBinding: FixedPointBinding | PointBinding | null =
element.endBinding;
// Check start binding
if (element.startBinding && !isFixedPointBinding(element.startBinding)) {
const boundElement = elementsMap.get(element.startBinding.elementId);
if (boundElement && isBindableElement(boundElement)) {
const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
0,
elementsMap,
);
if (isPointInElement(edgePoint, boundElement, elementsMap)) {
const { fixedPoint } = calculateFixedPointForNonElbowArrowBinding(
element,
boundElement,
"start",
elementsMap,
);
newStartBinding = {
elementId: element.startBinding.elementId,
focus: 0,
gap: 0,
fixedPoint,
};
shouldUpdateElement = true;
}
}
}
// Check end binding
if (element.endBinding && !isFixedPointBinding(element.endBinding)) {
const boundElement = elementsMap.get(element.endBinding.elementId);
if (boundElement && isBindableElement(boundElement)) {
const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
-1,
elementsMap,
);
if (isPointInElement(edgePoint, boundElement, elementsMap)) {
const { fixedPoint } = calculateFixedPointForNonElbowArrowBinding(
element,
boundElement,
"end",
elementsMap,
);
newEndBinding = {
elementId: element.endBinding.elementId,
focus: 0,
gap: 0,
fixedPoint,
};
shouldUpdateElement = true;
}
}
}
if (shouldUpdateElement) {
(element as Mutable<ExcalidrawLinearElement>).startBinding =
newStartBinding;
(element as Mutable<ExcalidrawLinearElement>).endBinding = newEndBinding;
}
};
export const restoreElements = (
elements: ImportedDataState["elements"],
/** NOTE doesn't serve for reconciliation */
@@ -620,6 +706,9 @@ export const restoreElements = (
(element as Mutable<ExcalidrawLinearElement>).endBinding = null;
}
}
// Migrate old PointBinding to FixedPointBinding for non-elbow arrows
migratePointBindingToFixedPoint(element, restoredElementsMap);
}
// NOTE (mtolmacs): Temporary fix for extremely large arrows

View File

@@ -194,7 +194,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "99.19972",
"height": 150,
"id": "id4",
"index": "a2",
"isDeleted": false,
@@ -208,8 +208,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
"98.40611",
"99.19972",
"124.00500",
150,
],
],
"roughness": 1,
@@ -224,7 +224,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
"updated": 1,
"version": 35,
"width": "98.40611",
"width": "124.00500",
"x": 1,
"y": 0,
}
@@ -323,15 +323,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"focus": 0,
"gap": 1,
},
"height": "68.58402",
"height": "104.34908",
"points": [
[
0,
0,
],
[
98,
"68.58402",
"124.00500",
"104.34908",
],
],
"startBinding": {
@@ -347,7 +347,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"focus": "-0.02000",
"gap": 1,
},
"height": "0.00656",
"height": "0.00849",
"points": [
[
0,
@@ -355,7 +355,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
],
[
"98.00000",
"-0.00656",
"-0.00849",
],
],
"startBinding": {
@@ -398,15 +398,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
},
"id4": {
"deleted": {
"height": "99.19972",
"height": 150,
"points": [
[
0,
0,
],
[
"98.40611",
"99.19972",
"124.00500",
150,
],
],
"startBinding": null,
@@ -414,15 +414,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"y": 0,
},
"inserted": {
"height": "68.58402",
"height": "104.34908",
"points": [
[
0,
0,
],
[
98,
"68.58402",
"124.00500",
"104.34908",
],
],
"startBinding": {
@@ -431,7 +431,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gap": 1,
},
"version": 33,
"y": "35.82151",
"y": "45.65092",
},
},
},
@@ -1207,7 +1207,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "1.36342",
"height": "49.99000",
"id": "id4",
"index": "Zz",
"isDeleted": false,
@@ -1221,8 +1221,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
98,
"1.36342",
"150.01000",
"49.99000",
],
],
"roughness": 1,
@@ -1242,10 +1242,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 10,
"width": 98,
"x": 1,
"y": 0,
"version": 11,
"width": "150.01000",
"x": 0,
"y": "0.01000",
}
`;
@@ -1569,7 +1569,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "1.36342",
"height": "49.99000",
"id": "id5",
"index": "a0",
"isDeleted": false,
@@ -1583,8 +1583,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
98,
"1.36342",
"249.99000",
"-49.99000",
],
],
"roughness": 1,
@@ -1605,9 +1605,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
"updated": 1,
"version": 11,
"width": 98,
"x": 1,
"y": 0,
"width": "249.99000",
"x": "-49.99000",
"y": 50,
}
`;
@@ -1719,7 +1719,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "1.36342",
"height": "49.99000",
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": null,
@@ -1732,8 +1732,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
98,
"1.36342",
"249.99000",
"-49.99000",
],
],
"roughness": 1,
@@ -1753,9 +1753,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "arrow",
"version": 11,
"width": 98,
"x": 1,
"y": 0,
"width": "249.99000",
"x": "-49.99000",
"y": 50,
},
"inserted": {
"isDeleted": true,