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

@ -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",

View file

@ -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],
]);
});

View file

@ -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,
);