mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Fixed gap binding
This commit is contained in:
parent
f363fcabd8
commit
dca9fbe306
12 changed files with 169 additions and 216 deletions
|
@ -463,23 +463,10 @@ export const maybeBindLinearElement = (
|
|||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
const normalizePointBinding = (binding: { focus: number; gap: number }) => {
|
||||
return {
|
||||
...binding,
|
||||
gap,
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -729,7 +716,7 @@ const calculateFocusAndGap = (
|
|||
|
||||
return {
|
||||
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>;
|
||||
},
|
||||
) => {
|
||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||
const { simultaneouslyUpdated } = options ?? {};
|
||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||
simultaneouslyUpdated,
|
||||
);
|
||||
|
@ -780,23 +767,13 @@ export const updateBoundElements = (
|
|||
startBounds = getElementBounds(startBindingElement, 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
|
||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||
mutateElement(element, bindings, true);
|
||||
mutateElement(
|
||||
element,
|
||||
{ startBinding: element.startBinding, endBinding: element.endBinding },
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -818,7 +795,9 @@ export const updateBoundElements = (
|
|||
const point = updateBoundPoint(
|
||||
element,
|
||||
bindingProp,
|
||||
bindings[bindingProp],
|
||||
bindingProp === "startBinding"
|
||||
? element.startBinding
|
||||
: element.endBinding,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
|
@ -1040,10 +1019,7 @@ export const avoidRectangularCorner = (
|
|||
element: ExcalidrawBindableElement,
|
||||
p: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||
|
||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||
|
@ -1226,7 +1202,6 @@ const updateBoundPoint = (
|
|||
linearElement,
|
||||
bindableElement,
|
||||
startOrEnd === "startBinding" ? "start" : "end",
|
||||
elementsMap,
|
||||
).fixedPoint;
|
||||
const globalMidPoint = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
|
@ -1336,7 +1311,6 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
): { fixedPoint: FixedPoint } => {
|
||||
const bounds = [
|
||||
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 = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "start" | "end",
|
||||
|
|
|
@ -78,8 +78,8 @@ describe("elbow arrow segment move", () => {
|
|||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[110, 0],
|
||||
[110, 200],
|
||||
[190, 200],
|
||||
[110, 195.01],
|
||||
[190, 195.01],
|
||||
]);
|
||||
|
||||
mouse.reset();
|
||||
|
@ -89,8 +89,8 @@ describe("elbow arrow segment move", () => {
|
|||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[110, 0],
|
||||
[110, 200],
|
||||
[190, 200],
|
||||
[110, 195.01],
|
||||
[190, 195.01],
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -198,11 +198,11 @@ describe("elbow arrow routing", () => {
|
|||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
||||
});
|
||||
|
||||
expect(arrow.points).toEqual([
|
||||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[45, 0],
|
||||
[45, 200],
|
||||
[90, 200],
|
||||
[45, 199.07],
|
||||
[90.07, 199.07],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -241,9 +241,9 @@ describe("elbow arrow ui", () => {
|
|||
expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow);
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(-43, -99);
|
||||
mouse.moveTo(-50, -100);
|
||||
mouse.click();
|
||||
mouse.moveTo(43, 99);
|
||||
mouse.moveTo(50, 100);
|
||||
mouse.click();
|
||||
|
||||
const arrow = h.scene.getSelectedElements(
|
||||
|
@ -252,11 +252,11 @@ describe("elbow arrow ui", () => {
|
|||
|
||||
expect(arrow.type).toBe("arrow");
|
||||
expect(arrow.elbowed).toBe(true);
|
||||
expect(arrow.points).toEqual([
|
||||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[45, 0],
|
||||
[45, 200],
|
||||
[90, 200],
|
||||
[45, 195.01],
|
||||
[90, 195.01],
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -293,12 +293,11 @@ describe("elbow arrow ui", () => {
|
|||
".drag-input",
|
||||
) as HTMLInputElement;
|
||||
UI.updateInput(inputAngle, String("40"));
|
||||
|
||||
console.log(JSON.stringify(h.elements))
|
||||
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
||||
[0, 0],
|
||||
[35, 0],
|
||||
[35, 165],
|
||||
[103, 165],
|
||||
[109, 0],
|
||||
[109, 152],
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -195,7 +195,7 @@ describe("generic element", () => {
|
|||
UI.resize(rectangle, "w", [50, 0]);
|
||||
|
||||
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 () => {
|
||||
|
@ -510,13 +510,13 @@ describe("arrow element", () => {
|
|||
h.state,
|
||||
)[0] as ExcalidrawElbowArrowElement;
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.78);
|
||||
|
||||
UI.resize(rectangle, "se", [-200, -150]);
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.78);
|
||||
});
|
||||
|
||||
it("flips the fixed point binding on negative resize for group selection", () => {
|
||||
|
@ -538,8 +538,8 @@ describe("arrow element", () => {
|
|||
h.state,
|
||||
)[0] as ExcalidrawElbowArrowElement;
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.78);
|
||||
|
||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0);
|
||||
|
@ -809,7 +809,7 @@ describe("image element", () => {
|
|||
});
|
||||
API.setElements([image]);
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: -30,
|
||||
x: -29,
|
||||
y: 50,
|
||||
width: 28,
|
||||
height: 5,
|
||||
|
@ -819,14 +819,14 @@ describe("image element", () => {
|
|||
|
||||
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 scale = 20 / image.height;
|
||||
UI.resize(image, "nw", [50, 20]);
|
||||
|
||||
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,
|
||||
0,
|
||||
);
|
||||
|
@ -1033,11 +1033,11 @@ describe("multiple selection", () => {
|
|||
|
||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||
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.angle).toEqual(0);
|
||||
expect(leftBoundArrow.startBinding).toBeNull();
|
||||
expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10);
|
||||
expect(leftBoundArrow.endBinding?.gap).toEqual(FIXED_BINDING_DISTANCE);
|
||||
expect(leftBoundArrow.endBinding?.elementId).toBe(
|
||||
leftArrowBinding.elementId,
|
||||
);
|
||||
|
@ -1051,7 +1051,7 @@ describe("multiple selection", () => {
|
|||
expect(rightBoundArrow.height).toBeCloseTo(0);
|
||||
expect(rightBoundArrow.angle).toEqual(0);
|
||||
expect(rightBoundArrow.startBinding).toBeNull();
|
||||
expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
|
||||
expect(rightBoundArrow.endBinding?.gap).toEqual(FIXED_BINDING_DISTANCE);
|
||||
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
||||
rightArrowBinding.elementId,
|
||||
);
|
||||
|
|
|
@ -89,7 +89,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"endBinding": {
|
||||
"elementId": "ellipse-1",
|
||||
"focus": -0.007519379844961235,
|
||||
"gap": 11.562288374879595,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -119,7 +119,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"startBinding": {
|
||||
"elementId": "id49",
|
||||
"focus": -0.0813953488372095,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1864ab",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -145,7 +145,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"endBinding": {
|
||||
"elementId": "ellipse-1",
|
||||
"focus": 0.10666666666666667,
|
||||
"gap": 3.8343264684446097,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -175,7 +175,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"startBinding": {
|
||||
"elementId": "diamond-1",
|
||||
"focus": 0,
|
||||
"gap": 4.545343408287929,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#e67700",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -335,7 +335,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||
"endBinding": {
|
||||
"elementId": "text-2",
|
||||
"focus": 0,
|
||||
"gap": 14,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -365,7 +365,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||
"startBinding": {
|
||||
"elementId": "text-1",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -437,7 +437,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||
"endBinding": {
|
||||
"elementId": "id42",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -467,7 +467,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||
"startBinding": {
|
||||
"elementId": "id41",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -613,7 +613,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||
"endBinding": {
|
||||
"elementId": "id46",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -643,7 +643,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||
"startBinding": {
|
||||
"elementId": "id45",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"endBinding": {
|
||||
"elementId": "Alice",
|
||||
"focus": -0,
|
||||
"gap": 5.299874999999986,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -1507,7 +1507,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"startBinding": {
|
||||
"elementId": "Bob",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -1538,7 +1538,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"endBinding": {
|
||||
"elementId": "B",
|
||||
"focus": 0,
|
||||
"gap": 14,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -1566,7 +1566,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"startBinding": {
|
||||
"elementId": "Bob",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
|
|
@ -433,7 +433,7 @@ describe("Test Transform", () => {
|
|||
startBinding: {
|
||||
elementId: rectangle.id,
|
||||
focus: 0,
|
||||
gap: 1,
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
},
|
||||
endBinding: {
|
||||
elementId: ellipse.id,
|
||||
|
@ -518,7 +518,7 @@ describe("Test Transform", () => {
|
|||
startBinding: {
|
||||
elementId: text2.id,
|
||||
focus: 0,
|
||||
gap: 1,
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
},
|
||||
endBinding: {
|
||||
elementId: text3.id,
|
||||
|
@ -781,7 +781,7 @@ describe("Test Transform", () => {
|
|||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||
elementId: "rect-1",
|
||||
focus: -0,
|
||||
gap: 14,
|
||||
gap: 5,
|
||||
});
|
||||
expect(rect.boundElements).toStrictEqual([
|
||||
{
|
||||
|
|
|
@ -198,7 +198,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "102.35417",
|
||||
"height": "99.23572",
|
||||
"id": "id172",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
|
@ -212,8 +212,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
0,
|
||||
],
|
||||
[
|
||||
"101.77517",
|
||||
"102.35417",
|
||||
"96.42891",
|
||||
"99.23572",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -228,8 +228,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 40,
|
||||
"width": "101.77517",
|
||||
"x": "0.70711",
|
||||
"width": "96.42891",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -296,46 +296,46 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id171",
|
||||
"focus": "0.00990",
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"height": "0.98586",
|
||||
"height": "0.92998",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"-0.98586",
|
||||
"92.92893",
|
||||
"-0.92998",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id170",
|
||||
"focus": "0.02970",
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id171",
|
||||
"focus": "-0.02000",
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"height": "0.00000",
|
||||
"height": "0.00611",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"0.00000",
|
||||
"92.92893",
|
||||
"0.00611",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id170",
|
||||
"focus": "0.02000",
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -390,15 +390,15 @@ History {
|
|||
"focus": 0,
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "102.35417",
|
||||
"height": "99.23572",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"101.77517",
|
||||
"102.35417",
|
||||
"96.42891",
|
||||
"99.23572",
|
||||
],
|
||||
],
|
||||
"startBinding": null,
|
||||
|
@ -408,25 +408,25 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id171",
|
||||
"focus": "0.00990",
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"height": "0.98586",
|
||||
"height": "0.93503",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"-0.98586",
|
||||
"92.92893",
|
||||
"-0.93503",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id170",
|
||||
"focus": "0.02970",
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"y": "0.99364",
|
||||
"y": "0.97365",
|
||||
},
|
||||
},
|
||||
"id175" => Delta {
|
||||
|
@ -931,7 +931,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id166",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"points": [
|
||||
[
|
||||
|
@ -946,7 +946,7 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id165",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1241,7 +1241,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "1.30038",
|
||||
"height": "2.98409",
|
||||
"id": "id178",
|
||||
"index": "Zz",
|
||||
"isDeleted": false,
|
||||
|
@ -1255,8 +1255,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"1.30038",
|
||||
"92.92893",
|
||||
"-2.98409",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -1279,9 +1279,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"y": 0,
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": "4.70319",
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -1613,7 +1613,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "1.30038",
|
||||
"height": "2.98409",
|
||||
"id": "id181",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
|
@ -1627,8 +1627,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"1.30038",
|
||||
"92.92893",
|
||||
"-2.98409",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -1651,9 +1651,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"y": 0,
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": "4.70319",
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -1771,7 +1771,7 @@ History {
|
|||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "11.27227",
|
||||
"height": "22.46459",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
|
@ -1784,8 +1784,8 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"11.27227",
|
||||
"93.46683",
|
||||
"-22.46459",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -1806,9 +1806,9 @@ History {
|
|||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"y": 0,
|
||||
"width": "93.46683",
|
||||
"x": "2.99764",
|
||||
"y": "35.33176",
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
@ -2321,12 +2321,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"endBinding": {
|
||||
"elementId": "id185",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "374.05754",
|
||||
"height": "408.02337",
|
||||
"id": "id186",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
|
@ -2340,8 +2340,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
0,
|
||||
],
|
||||
[
|
||||
"502.78936",
|
||||
"-374.05754",
|
||||
"495.48945",
|
||||
"-408.02337",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -2352,7 +2352,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"startBinding": {
|
||||
"elementId": "id184",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -2360,9 +2360,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "502.78936",
|
||||
"x": "-0.83465",
|
||||
"y": "-36.58211",
|
||||
"width": "495.48945",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -2481,7 +2481,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id185",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -2511,7 +2511,7 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id184",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -15161,7 +15161,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"endBinding": {
|
||||
"elementId": "id58",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -15180,7 +15180,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -15192,7 +15192,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"startBinding": {
|
||||
"elementId": "id56",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -15200,8 +15200,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -15532,7 +15532,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id58",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -15562,7 +15562,7 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id56",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -15859,7 +15859,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"endBinding": {
|
||||
"elementId": "id52",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -15878,7 +15878,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -15890,7 +15890,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"startBinding": {
|
||||
"elementId": "id50",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -15898,8 +15898,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -16152,7 +16152,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id52",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -16182,7 +16182,7 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id50",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -16479,7 +16479,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"endBinding": {
|
||||
"elementId": "id64",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -16498,7 +16498,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -16510,7 +16510,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"startBinding": {
|
||||
"elementId": "id62",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -16518,8 +16518,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -16772,7 +16772,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id64",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -16802,7 +16802,7 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id62",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -17097,7 +17097,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"endBinding": {
|
||||
"elementId": "id70",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -17116,7 +17116,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -17128,7 +17128,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"startBinding": {
|
||||
"elementId": "id68",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -17136,8 +17136,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -17200,7 +17200,7 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id68",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -17460,7 +17460,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id70",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -17490,7 +17490,7 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id68",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -17811,7 +17811,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"endBinding": {
|
||||
"elementId": "id77",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -17830,7 +17830,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -17842,7 +17842,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"startBinding": {
|
||||
"elementId": "id75",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -17850,8 +17850,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -17913,7 +17913,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id77",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"points": [
|
||||
[
|
||||
|
@ -17928,7 +17928,7 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id75",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -18189,7 +18189,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id77",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -18219,7 +18219,7 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id75",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
|
|
@ -191,12 +191,12 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||
"endBinding": {
|
||||
"elementId": "id1",
|
||||
"focus": "-0.46667",
|
||||
"gap": 10,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "87.29887",
|
||||
"height": "87.97595",
|
||||
"id": "id2",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
|
@ -210,8 +210,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||
0,
|
||||
],
|
||||
[
|
||||
"86.85786",
|
||||
"87.29887",
|
||||
"87.46447",
|
||||
"87.97595",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -223,7 +223,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"focus": "-0.60000",
|
||||
"gap": 10,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -232,8 +232,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||
"updated": 1,
|
||||
"version": 11,
|
||||
"versionNonce": 1051383431,
|
||||
"width": "86.85786",
|
||||
"x": "107.07107",
|
||||
"y": "47.07107",
|
||||
"width": "87.46447",
|
||||
"x": 110,
|
||||
"y": 50,
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -4779,12 +4779,12 @@ describe("history", () => {
|
|||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: 0,
|
||||
gap: 1,
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: -0,
|
||||
gap: 1,
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
}),
|
||||
isDeleted: true,
|
||||
}),
|
||||
|
|
|
@ -1266,7 +1266,7 @@ describe("Test Linear Elements", () => {
|
|||
mouse.downAt(rect.x, rect.y);
|
||||
mouse.moveTo(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.y).toBe(0);
|
||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||
|
|
|
@ -35,7 +35,7 @@ test("unselected bound arrow updates when rotating its target element", async ()
|
|||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||
expect(arrow.x).toBeCloseTo(-80);
|
||||
expect(arrow.y).toBeCloseTo(50);
|
||||
expect(arrow.width).toBeCloseTo(116.7, 1);
|
||||
expect(arrow.width).toBeCloseTo(119.58, 1);
|
||||
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.y).toEqual(0);
|
||||
expect(ellipseArrow.points[0]).toEqual([0, 0]);
|
||||
expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1);
|
||||
expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1);
|
||||
expect(ellipseArrow.points[1][0]).toBeCloseTo(54.36, 1);
|
||||
expect(ellipseArrow.points[1][1]).toBeCloseTo(139.61, 1);
|
||||
|
||||
expect(textArrow.endBinding?.elementId).toEqual(text.id);
|
||||
expect(textArrow.x).toEqual(360);
|
||||
expect(textArrow.y).toEqual(300);
|
||||
expect(textArrow.points[0]).toEqual([0, 0]);
|
||||
expect(textArrow.points[1][0]).toBeCloseTo(-94, 0);
|
||||
expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0);
|
||||
expect(textArrow.points[1][0]).toBeCloseTo(-100.12, 0);
|
||||
expect(textArrow.points[1][1]).toBeCloseTo(-123.63, 0);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ export const clamp = (value: number, min: number, max: number) => {
|
|||
|
||||
export const round = (
|
||||
value: number,
|
||||
precision: number,
|
||||
precision: number = (Math.log(1 / PRECISION) * Math.LOG10E + 1) | 0,
|
||||
func: "round" | "floor" | "ceil" = "round",
|
||||
) => {
|
||||
const multiplier = Math.pow(10, precision);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue