[skip ci] First iteration of bringing over previous changes

This commit is contained in:
Mark Tolmacs 2025-03-24 19:22:45 +01:00
parent 4ee99de2fb
commit fbde68c849
19 changed files with 599 additions and 596 deletions

View file

@ -2,6 +2,7 @@ import { average } from "@excalidraw/math";
import type { import type {
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawElement,
FontFamilyValues, FontFamilyValues,
FontString, FontString,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
@ -1201,3 +1202,6 @@ export const escapeDoubleQuotes = (str: string) => {
export const castArray = <T>(value: T | T[]): T[] => export const castArray = <T>(value: T | T[]): T[] =>
Array.isArray(value) ? value : [value]; Array.isArray(value) ? value : [value];
export const toLocalPoint = (p: GlobalPoint, element: ExcalidrawElement) =>
pointTranslate<GlobalPoint, LocalPoint>(p, vector(-element.x, -element.y));

View file

@ -81,11 +81,10 @@ import type {
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
ExcalidrawTextElement, ExcalidrawTextElement,
ExcalidrawArrowElement, ExcalidrawArrowElement,
OrderedExcalidrawElement,
ExcalidrawElbowArrowElement,
FixedPoint, FixedPoint,
SceneElementsMap, SceneElementsMap,
FixedPointBinding, FixedPointBinding,
ExcalidrawElbowArrowElement,
} from "./types"; } from "./types";
export type SuggestedBinding = export type SuggestedBinding =
@ -108,6 +107,7 @@ export const isBindingEnabled = (appState: AppState): boolean => {
return appState.isBindingEnabled; return appState.isBindingEnabled;
}; };
export const INSIDE_BINDING_BAND_PERCENT = 0.1;
export const FIXED_BINDING_DISTANCE = 5; export const FIXED_BINDING_DISTANCE = 5;
export const BINDING_HIGHLIGHT_THICKNESS = 10; export const BINDING_HIGHLIGHT_THICKNESS = 10;
export const BINDING_HIGHLIGHT_OFFSET = 4; export const BINDING_HIGHLIGHT_OFFSET = 4;
@ -463,26 +463,6 @@ 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;
}
return {
...binding,
gap,
};
};
export const bindLinearElement = ( export const bindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
@ -493,17 +473,25 @@ export const bindLinearElement = (
return; return;
} }
const direction = startOrEnd === "start" ? -1 : 1;
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
const adjacentPointIndex = edgePointIndex - direction;
const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
edgePointIndex,
elementsMap,
);
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
adjacentPointIndex,
elementsMap,
);
let binding: PointBinding | FixedPointBinding = { let binding: PointBinding | FixedPointBinding = {
elementId: hoveredElement.id, elementId: hoveredElement.id,
...normalizePointBinding( focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
calculateFocusAndGap( gap: FIXED_BINDING_DISTANCE,
linearElement,
hoveredElement,
startOrEnd,
elementsMap,
),
hoveredElement,
),
}; };
if (isElbowArrow(linearElement)) { if (isElbowArrow(linearElement)) {
@ -706,33 +694,6 @@ const getAllElementsAtPositionForBinding = (
return elementsAtPosition; return elementsAtPosition;
}; };
const calculateFocusAndGap = (
linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap,
): { focus: number; gap: number } => {
const direction = startOrEnd === "start" ? -1 : 1;
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
const adjacentPointIndex = edgePointIndex - direction;
const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
edgePointIndex,
elementsMap,
);
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
adjacentPointIndex,
elementsMap,
);
return {
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
};
};
// Supports translating, rotating and scaling `changedElement` with bound // Supports translating, rotating and scaling `changedElement` with bound
// linear elements. // linear elements.
// Because scaling involves moving the focus points as well, it is // Because scaling involves moving the focus points as well, it is
@ -743,11 +704,9 @@ export const updateBoundElements = (
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
options?: { options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[]; simultaneouslyUpdated?: readonly ExcalidrawElement[];
newSize?: { width: number; height: number };
changedElements?: Map<string, OrderedExcalidrawElement>;
}, },
) => { ) => {
const { newSize, simultaneouslyUpdated } = options ?? {}; const { simultaneouslyUpdated } = options ?? {};
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated, simultaneouslyUpdated,
); );
@ -781,22 +740,13 @@ export const updateBoundElements = (
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 +768,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,
); );
@ -848,10 +800,10 @@ export const updateBoundElements = (
updates, updates,
{ {
...(changedElement.id === element.startBinding?.elementId ...(changedElement.id === element.startBinding?.elementId
? { startBinding: bindings.startBinding } ? { startBinding: element.startBinding }
: {}), : {}),
...(changedElement.id === element.endBinding?.elementId ...(changedElement.id === element.endBinding?.elementId
? { endBinding: bindings.endBinding } ? { endBinding: element.endBinding }
: {}), : {}),
}, },
elementsMap as NonDeletedSceneElementsMap, elementsMap as NonDeletedSceneElementsMap,
@ -885,7 +837,6 @@ export const getHeadingForElbowArrowSnap = (
otherPoint: Readonly<GlobalPoint>, otherPoint: Readonly<GlobalPoint>,
bindableElement: ExcalidrawBindableElement | undefined | null, bindableElement: ExcalidrawBindableElement | undefined | null,
aabb: Bounds | undefined | null, aabb: Bounds | undefined | null,
elementsMap: ElementsMap,
origPoint: GlobalPoint, origPoint: GlobalPoint,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
): Heading => { ): Heading => {
@ -895,12 +846,7 @@ export const getHeadingForElbowArrowSnap = (
return otherPointHeading; return otherPointHeading;
} }
const distance = getDistanceForBinding( const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
origPoint,
bindableElement,
elementsMap,
zoom,
);
if (!distance) { if (!distance) {
return vectorToHeading( return vectorToHeading(
@ -920,7 +866,6 @@ export const getHeadingForElbowArrowSnap = (
const getDistanceForBinding = ( const getDistanceForBinding = (
point: Readonly<GlobalPoint>, point: Readonly<GlobalPoint>,
bindableElement: ExcalidrawBindableElement, bindableElement: ExcalidrawBindableElement,
elementsMap: ElementsMap,
zoom?: AppState["zoom"], zoom?: AppState["zoom"],
) => { ) => {
const distance = distanceToBindableElement(bindableElement, point); const distance = distanceToBindableElement(bindableElement, point);
@ -935,40 +880,47 @@ const getDistanceForBinding = (
}; };
export const bindPointToSnapToElementOutline = ( export const bindPointToSnapToElementOutline = (
arrow: ExcalidrawElbowArrowElement, linearElement: ExcalidrawLinearElement,
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(
linearElement.points.length > 0,
"Arrow should have at least 1 point",
);
} }
const elbowed = isElbowArrow(linearElement);
const aabb = aabbForElement(bindableElement); const aabb = aabbForElement(bindableElement);
const localP =
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
const globalP = pointFrom<GlobalPoint>(
arrow.x + localP[0],
arrow.y + localP[1],
);
const edgePoint = isRectanguloidElement(bindableElement)
? avoidRectangularCorner(bindableElement, globalP)
: globalP;
const elbowed = isElbowArrow(arrow);
const center = getCenterForBounds(aabb); const center = getCenterForBounds(aabb);
const adjacentPointIdx = startOrEnd === "start" ? 1 : arrow.points.length - 2;
const adjacentPoint = pointRotateRads( const pointIdx = startOrEnd === "start" ? 0 : linearElement.points.length - 1;
const p = pointFrom<GlobalPoint>(
linearElement.x + linearElement.points[pointIdx][0],
linearElement.y + linearElement.points[pointIdx][1],
);
const edgePoint = avoidRectangularCorner(bindableElement, p);
const adjacentPointIdx =
startOrEnd === "start" ? 1 : linearElement.points.length - 2;
const adjacentPoint =
linearElement.points.length === 1
? center
: pointRotateRads(
pointFrom<GlobalPoint>( pointFrom<GlobalPoint>(
arrow.x + arrow.points[adjacentPointIdx][0], linearElement.x + linearElement.points[adjacentPointIdx][0],
arrow.y + arrow.points[adjacentPointIdx][1], linearElement.y + linearElement.points[adjacentPointIdx][1],
), ),
center, center,
arrow.angle ?? 0, linearElement.angle ?? 0,
); );
let intersection: GlobalPoint | null = null; let intersection: GlobalPoint | null = null;
if (elbowed) { if (elbowed) {
const isHorizontal = headingIsHorizontal( const isHorizontal = headingIsHorizontal(
headingForPointFromElement(bindableElement, aabb, globalP), headingForPointFromElement(bindableElement, aabb, p),
); );
const otherPoint = pointFrom<GlobalPoint>( const otherPoint = pointFrom<GlobalPoint>(
isHorizontal ? center[0] : edgePoint[0], isHorizontal ? center[0] : edgePoint[0],
@ -1033,6 +985,28 @@ export const bindPointToSnapToElementOutline = (
); );
} }
const isInside = isPointInShape(
edgePoint,
getElementShape(
{
...bindableElement,
x:
bindableElement.x +
bindableElement.width * INSIDE_BINDING_BAND_PERCENT,
y:
bindableElement.y +
bindableElement.height * INSIDE_BINDING_BAND_PERCENT,
width: bindableElement.width * (1 - INSIDE_BINDING_BAND_PERCENT * 2),
height: bindableElement.height * (1 - INSIDE_BINDING_BAND_PERCENT * 2),
} as ExcalidrawBindableElement,
elementsMap,
),
);
if (!isInside) {
return intersection;
}
return edgePoint; return edgePoint;
}; };
@ -1040,6 +1014,10 @@ export const avoidRectangularCorner = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
p: GlobalPoint, p: GlobalPoint,
): GlobalPoint => { ): GlobalPoint => {
if (!isRectanguloidElement(element)) {
return p;
}
const center = pointFrom<GlobalPoint>( const center = pointFrom<GlobalPoint>(
element.x + element.width / 2, element.x + element.width / 2,
element.y + element.height / 2, element.y + element.height / 2,
@ -1200,6 +1178,45 @@ export const snapToMid = (
return p; return p;
}; };
export const getOutlineAvoidingPoint = (
element: NonDeleted<ExcalidrawLinearElement>,
coords: GlobalPoint,
pointIndex: number,
scene: Scene,
zoom: AppState["zoom"],
fallback?: GlobalPoint,
): GlobalPoint => {
const elementsMap = scene.getNonDeletedElementsMap();
const hoveredElement = getHoveredElementForBinding(
{ x: coords[0], y: coords[1] },
scene.getNonDeletedElements(),
elementsMap,
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",
elementsMap,
);
}
return fallback ?? coords;
};
const updateBoundPoint = ( const updateBoundPoint = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "startBinding" | "endBinding", startOrEnd: "startBinding" | "endBinding",
@ -1263,11 +1280,13 @@ const updateBoundPoint = (
let newEdgePoint: GlobalPoint; let newEdgePoint: GlobalPoint;
// The linear element was not originally pointing inside the bound shape, // // The linear element was not originally pointing inside the bound shape,
// we can point directly at the focus point // // we can point directly at the focus point
if (binding.gap === 0) { // if (binding.gap === 0) {
newEdgePoint = focusPointAbsolute; // newEdgePoint = focusPointAbsolute;
} else { // } else {
// ...
// }
const edgePointAbsolute = const edgePointAbsolute =
LinearElementEditor.getPointAtIndexGlobalCoordinates( LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement, linearElement,
@ -1290,15 +1309,13 @@ const updateBoundPoint = (
adjacentPoint, adjacentPoint,
pointFromVector( pointFromVector(
vectorScale( vectorScale(
vectorNormalize( vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)),
vectorFromPoint(focusPointAbsolute, adjacentPoint),
),
interceptorLength, interceptorLength,
), ),
adjacentPoint, adjacentPoint,
), ),
), ),
binding.gap, FIXED_BINDING_DISTANCE,
).sort( ).sort(
(g, h) => (g, h) =>
pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint), pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint),
@ -1323,7 +1340,6 @@ const updateBoundPoint = (
// Shouldn't happend, but just in case // Shouldn't happend, but just in case
newEdgePoint = edgePointAbsolute; newEdgePoint = edgePointAbsolute;
} }
}
return LinearElementEditor.pointFromAbsoluteCoords( return LinearElementEditor.pointFromAbsoluteCoords(
linearElement, linearElement,
@ -1333,7 +1349,7 @@ const updateBoundPoint = (
}; };
export const calculateFixedPointForElbowArrowBinding = ( export const calculateFixedPointForElbowArrowBinding = (
linearElement: NonDeleted<ExcalidrawElbowArrowElement>, linearElement: NonDeleted<ExcalidrawArrowElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap, elementsMap: ElementsMap,
@ -1348,6 +1364,7 @@ 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,
@ -1369,28 +1386,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

@ -1254,6 +1254,7 @@ const getElbowArrowData = (
"start", "start",
arrow.startBinding?.fixedPoint, arrow.startBinding?.fixedPoint,
origStartGlobalPoint, origStartGlobalPoint,
elementsMap,
hoveredStartElement, hoveredStartElement,
options?.isDragging, options?.isDragging,
); );
@ -1267,6 +1268,7 @@ const getElbowArrowData = (
"end", "end",
arrow.endBinding?.fixedPoint, arrow.endBinding?.fixedPoint,
origEndGlobalPoint, origEndGlobalPoint,
elementsMap,
hoveredEndElement, hoveredEndElement,
options?.isDragging, options?.isDragging,
); );
@ -2212,6 +2214,7 @@ const getGlobalPoint = (
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
fixedPointRatio: [number, number] | undefined | null, fixedPointRatio: [number, number] | undefined | null,
initialPoint: GlobalPoint, initialPoint: GlobalPoint,
elementsMap: NonDeletedSceneElementsMap,
element?: ExcalidrawBindableElement | null, element?: ExcalidrawBindableElement | null,
isDragging?: boolean, isDragging?: boolean,
): GlobalPoint => { ): GlobalPoint => {
@ -2221,6 +2224,7 @@ const getGlobalPoint = (
arrow, arrow,
element, element,
startOrEnd, startOrEnd,
elementsMap,
); );
return snapToMid(element, snapPoint); return snapToMid(element, snapPoint);
@ -2240,7 +2244,7 @@ const getGlobalPoint = (
distanceToBindableElement(element, fixedGlobalPoint) - distanceToBindableElement(element, fixedGlobalPoint) -
FIXED_BINDING_DISTANCE, FIXED_BINDING_DISTANCE,
) > 0.01 ) > 0.01
? bindPointToSnapToElementOutline(arrow, element, startOrEnd) ? bindPointToSnapToElementOutline(arrow, element, startOrEnd, elementsMap)
: fixedGlobalPoint; : fixedGlobalPoint;
} }
@ -2268,7 +2272,6 @@ const getBindPointHeading = (
number, number,
], ],
), ),
elementsMap,
origPoint, origPoint,
); );

View file

@ -42,6 +42,7 @@ import type { Mutable } from "@excalidraw/common/utility-types";
import { import {
bindOrUnbindLinearElement, bindOrUnbindLinearElement,
getHoveredElementForBinding, getHoveredElementForBinding,
getOutlineAvoidingPoint,
isBindingEnabled, isBindingEnabled,
} from "./binding"; } from "./binding";
import { import {
@ -252,27 +253,28 @@ export class LinearElementEditor {
pointSceneCoords: { x: number; y: number }[], pointSceneCoords: { x: number; y: number }[],
) => void, ) => void,
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
scene: Scene,
): LinearElementEditor | null { ): LinearElementEditor | null {
if (!linearElementEditor) { if (!linearElementEditor) {
return null; return null;
} }
const { elementId } = linearElementEditor; const { elementId } = linearElementEditor;
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = app.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 (
isElbowArrow(element) && elbowed &&
!linearElementEditor.pointerDownState.lastClickedIsEndPoint && !linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
linearElementEditor.pointerDownState.lastClickedPoint !== 0 linearElementEditor.pointerDownState.lastClickedPoint !== 0
) { ) {
return null; return null;
} }
const selectedPointsIndices = isElbowArrow(element) const selectedPointsIndices = elbowed
? [ ? [
!!linearElementEditor.selectedPointsIndices?.includes(0) !!linearElementEditor.selectedPointsIndices?.includes(0)
? 0 ? 0
@ -282,7 +284,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 = isElbowArrow(element) const lastClickedPoint = elbowed
? linearElementEditor.pointerDownState.lastClickedPoint > 0 ? linearElementEditor.pointerDownState.lastClickedPoint > 0
? element.points.length - 1 ? element.points.length - 1
: 0 : 0
@ -334,19 +336,43 @@ export class LinearElementEditor {
LinearElementEditor.movePoints( LinearElementEditor.movePoints(
element, element,
selectedPointsIndices.map((pointIndex) => { selectedPointsIndices.map((pointIndex) => {
const newPointPosition: LocalPoint = let newPointPosition = pointFrom<LocalPoint>(
pointIndex === lastClickedPoint
? LinearElementEditor.createPointAt(
element,
elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
)
: pointFrom(
element.points[pointIndex][0] + deltaX, element.points[pointIndex][0] + deltaX,
element.points[pointIndex][1] + deltaY, element.points[pointIndex][1] + deltaY,
); );
// 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 = getOutlineAvoidingPoint(
element,
pointFrom<GlobalPoint>(
element.x + element.points[pointIndex][0] + deltaX,
element.y + element.points[pointIndex][1] + deltaY,
),
pointIndex,
app.scene,
app.state.zoom,
);
}
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

@ -969,10 +969,7 @@ export const resizeSingleElement = (
mutateElement(latestElement, updates, shouldInformMutation); mutateElement(latestElement, updates, shouldInformMutation);
updateBoundElements(latestElement, elementsMap as SceneElementsMap, { updateBoundElements(latestElement, elementsMap as SceneElementsMap);
// TODO: confirm with MARK if this actually makes sense
newSize: { width: nextWidth, height: nextHeight },
});
if (boundTextElement && boundTextFont != null) { if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, { mutateElement(boundTextElement, {
@ -1525,7 +1522,7 @@ export const resizeMultipleElements = (
element, element,
update: { boundTextFontSize, ...update }, update: { boundTextFontSize, ...update },
} of elementsAndUpdates) { } of elementsAndUpdates) {
const { width, height, angle } = update; const { angle } = update;
mutateElement(element, update, false, { mutateElement(element, update, false, {
// needed for the fixed binding point udpate to take effect // needed for the fixed binding point udpate to take effect
@ -1534,7 +1531,6 @@ export const resizeMultipleElements = (
updateBoundElements(element, elementsMap as SceneElementsMap, { updateBoundElements(element, elementsMap as SceneElementsMap, {
simultaneouslyUpdated: elementsToUpdate, simultaneouslyUpdated: elementsToUpdate,
newSize: { width, height },
}); });
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);

View file

@ -190,7 +190,18 @@ describe("element binding", () => {
// Sever connection // Sever connection
expect(API.getSelectedElement().type).toBe("arrow"); expect(API.getSelectedElement().type).toBe("arrow");
Keyboard.withModifierKeys({ shift: true }, () => {
// 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);
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

@ -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(81, 0);
}); });
it("resizes with a label", async () => { it("resizes with a label", async () => {
@ -826,8 +826,9 @@ describe("image element", () => {
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(
30 + imageWidth * scale, expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(
30 + imageWidth * scale + 1,
0, 0,
); );
}); });
@ -1033,11 +1034,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, 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).toBeCloseTo(5);
expect(leftBoundArrow.endBinding?.elementId).toBe( expect(leftBoundArrow.endBinding?.elementId).toBe(
leftArrowBinding.elementId, leftArrowBinding.elementId,
); );

View file

@ -1,4 +1,4 @@
import { pointFrom } from "@excalidraw/math"; import { type GlobalPoint, pointFrom } from "@excalidraw/math";
import { import {
maybeBindLinearElement, maybeBindLinearElement,
@ -91,10 +91,26 @@ export const actionFinalize = register({
multiPointElement.type !== "freedraw" && multiPointElement.type !== "freedraw" &&
appState.lastPointerDownWith !== "touch" appState.lastPointerDownWith !== "touch"
) { ) {
const { points, lastCommittedPoint } = multiPointElement; const { x: rx, y: ry, 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 (
!lastCommittedPoint || !hoveredElementForBinding &&
points[points.length - 1] !== lastCommittedPoint (!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,6 +1655,7 @@ export const actionChangeArrowType = register({
newElement, newElement,
startHoveredElement, startHoveredElement,
"start", "start",
elementsMap,
) )
: startGlobalPoint; : startGlobalPoint;
const finalEndPoint = endHoveredElement const finalEndPoint = endHoveredElement
@ -1662,6 +1663,7 @@ export const actionChangeArrowType = register({
newElement, newElement,
endHoveredElement, endHoveredElement,
"end", "end",
elementsMap,
) )
: endGlobalPoint; : endGlobalPoint;

View file

@ -1508,9 +1508,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
) { ) {
for (const element of changed.values()) { for (const element of changed.values()) {
if (!element.isDeleted && isBindableElement(element)) { if (!element.isDeleted && isBindableElement(element)) {
updateBoundElements(element, elements, { updateBoundElements(element, elements);
changedElements: changed,
});
} }
} }
} }

View file

@ -16,6 +16,7 @@ import {
vectorSubtract, vectorSubtract,
vectorDot, vectorDot,
vectorNormalize, vectorNormalize,
lineSegment,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { isPointInShape } from "@excalidraw/utils/collision"; import { isPointInShape } from "@excalidraw/utils/collision";
import { getSelectionBoxShape } from "@excalidraw/utils/shape"; import { getSelectionBoxShape } from "@excalidraw/utils/shape";
@ -302,7 +303,7 @@ import {
import { isNonDeletedElement } from "@excalidraw/element"; import { isNonDeletedElement } from "@excalidraw/element";
import type { LocalPoint, Radians } from "@excalidraw/math"; import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
import type { import type {
ExcalidrawBindableElement, ExcalidrawBindableElement,
@ -5997,9 +5998,19 @@ class App extends React.Component<AppProps, AppState> {
{ {
points: [ points: [
...points.slice(0, -1), ...points.slice(0, -1),
pointFrom<LocalPoint>( toLocalPoint(
lastCommittedX + dxFromLastCommitted, getOutlineAvoidingPoint(
lastCommittedY + dyFromLastCommitted, multiElement,
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
multiElement.points.length - 1,
this.scene,
this.state.zoom,
pointFrom<GlobalPoint>(
multiElement.x + lastCommittedX + dxFromLastCommitted,
multiElement.y + lastCommittedY + dyFromLastCommitted,
),
),
multiElement,
), ),
], ],
}, },
@ -7751,10 +7762,26 @@ 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 (
multiElement.points.length > 1 && hoveredElementForBinding ||
(multiElement.points.length > 1 &&
lastCommittedPoint && lastCommittedPoint &&
pointDistance( pointDistance(
pointFrom( pointFrom(
@ -7762,7 +7789,7 @@ class App extends React.Component<AppProps, AppState> {
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;
@ -7806,10 +7833,11 @@ class App extends React.Component<AppProps, AppState> {
? [currentItemStartArrowhead, currentItemEndArrowhead] ? [currentItemStartArrowhead, currentItemEndArrowhead]
: [null, null]; : [null, null];
const element = let element: NonDeleted<ExcalidrawLinearElement>;
elementType === "arrow" if (elementType === "arrow") {
? newArrowElement({ const arrow: Mutable<NonDeleted<ExcalidrawArrowElement>> =
type: elementType, newArrowElement({
type: "arrow",
x: gridX, x: gridX,
y: gridY, y: gridY,
strokeColor: this.state.currentItemStrokeColor, strokeColor: this.state.currentItemStrokeColor,
@ -7831,11 +7859,48 @@ class App extends React.Component<AppProps, AppState> {
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 },
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
true,
this.state.currentItemArrowType === ARROW_TYPE.elbow,
);
if (hoveredElement) {
[arrow.x, arrow.y] =
intersectElementWithLineSegment(
hoveredElement,
lineSegment(
pointFrom<GlobalPoint>(gridX, gridY),
pointFrom<GlobalPoint>(
gridX,
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, type: elementType,
x: gridX, x: gridX,
y: gridY, y: gridY,
@ -7853,6 +7918,7 @@ class App extends React.Component<AppProps, AppState> {
locked: false, locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null, frameId: topLayerFrame ? topLayerFrame.id : null,
}); });
}
this.setState((prevState) => { this.setState((prevState) => {
const nextSelectedElementIds = { const nextSelectedElementIds = {
...prevState.selectedElementIds, ...prevState.selectedElementIds,
@ -8167,12 +8233,6 @@ 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
@ -8273,7 +8333,6 @@ 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;
@ -8660,6 +8719,11 @@ 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;
@ -8676,7 +8740,23 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [...points, pointFrom<LocalPoint>(dx, dy)], points: [
...points,
toLocalPoint(
getOutlineAvoidingPoint(
newElement,
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
newElement.points.length - 1,
this.scene,
this.state.zoom,
pointFrom<GlobalPoint>(
newElement.x + dx,
newElement.y + dy,
),
),
newElement,
),
],
}, },
false, false,
); );
@ -8687,7 +8767,23 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)], points: [
...points.slice(0, -1),
toLocalPoint(
getOutlineAvoidingPoint(
newElement,
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
newElement.points.length - 1,
this.scene,
this.state.zoom,
pointFrom<GlobalPoint>(
newElement.x + dx,
newElement.y + dy,
),
),
newElement,
),
],
}, },
false, false,
{ isDragging: true }, { isDragging: true },
@ -10725,12 +10821,6 @@ class App extends React.Component<AppProps, AppState> {
updateBoundElements( updateBoundElements(
croppingElement, croppingElement,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
{
newSize: {
width: croppingElement.width,
height: croppingElement.height,
},
},
); );
this.setState({ this.setState({

View file

@ -87,9 +87,7 @@ const resizeElementInGroup = (
); );
if (boundTextElement) { if (boundTextElement) {
const newFontSize = boundTextElement.fontSize * scale; const newFontSize = boundTextElement.fontSize * scale;
updateBoundElements(latestElement, elementsMap, { updateBoundElements(latestElement, elementsMap);
newSize: { width: updates.width, height: updates.height },
});
const latestBoundTextElement = elementsMap.get(boundTextElement.id); const latestBoundTextElement = elementsMap.get(boundTextElement.id);
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) { if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
mutateElement( mutateElement(

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: FIXED_BINDING_DISTANCE,
}); });
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.58947",
"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", "99.58947",
"102.35417", "99.58947",
], ],
], ],
"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": "99.58947",
"x": "0.70711", "x": 0,
"y": 0, "y": 0,
} }
`; `;
@ -295,47 +295,47 @@ History {
"deleted": { "deleted": {
"endBinding": { "endBinding": {
"elementId": "id171", "elementId": "id171",
"focus": "0.00990", "focus": "0.01099",
"gap": 1, "gap": 5,
}, },
"height": "0.98586", "height": "0.96335",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"98.58579", "92.92893",
"-0.98586", "-0.96335",
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id170", "elementId": "id170",
"focus": "0.02970", "focus": "0.03005",
"gap": 1, "gap": 5,
}, },
}, },
"inserted": { "inserted": {
"endBinding": { "endBinding": {
"elementId": "id171", "elementId": "id171",
"focus": "-0.02000", "focus": "-0.02041",
"gap": 1, "gap": 5,
}, },
"height": "0.00000", "height": "0.03665",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"98.58579", "92.92893",
"0.00000", "0.03665",
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id170", "elementId": "id170",
"focus": "0.02000", "focus": "0.01884",
"gap": 1, "gap": 5,
}, },
}, },
}, },
@ -390,43 +390,47 @@ History {
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
}, },
"height": "102.35417", "height": "99.58947",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"101.77517", "99.58947",
"102.35417", "99.58947",
], ],
], ],
"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.01099",
"gap": 1, "gap": 5,
}, },
"height": "0.98586", "height": "0.96335",
"points": [ "points": [
[ [
0, 0,
0, 0,
], ],
[ [
"98.58579", "92.92893",
"-0.98586", "-0.96335",
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id170", "elementId": "id170",
"focus": "0.02970", "focus": "0.03005",
"gap": 1, "gap": 5,
}, },
"y": "0.99364", "width": "92.92893",
"x": "3.53553",
"y": "0.96335",
}, },
}, },
"id175" => Delta { "id175" => Delta {
@ -566,7 +570,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -580,7 +584,7 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "96.46447",
"x": 0, "x": 0,
"y": 0, "y": 0,
}, },
@ -804,7 +808,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -820,8 +824,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 30, "version": 30,
"width": 0, "width": "96.46447",
"x": "149.29289", "x": 150,
"y": 0, "y": 0,
} }
`; `;
@ -854,7 +858,7 @@ History {
0, 0,
], ],
[ [
0, "0.00000",
0, 0,
], ],
], ],
@ -866,7 +870,7 @@ History {
0, 0,
], ],
[ [
100, "92.92893",
0, 0,
], ],
], ],
@ -921,17 +925,19 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
"startBinding": null, "startBinding": null,
"width": "96.46447",
"x": 150,
}, },
"inserted": { "inserted": {
"endBinding": { "endBinding": {
"elementId": "id166", "elementId": "id166",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"points": [ "points": [
[ [
@ -939,15 +945,17 @@ History {
0, 0,
], ],
[ [
0, "0.00000",
0, 0,
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id165", "elementId": "id165",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"width": "0.00000",
"x": "146.46447",
}, },
}, },
}, },
@ -1074,7 +1082,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -1088,7 +1096,7 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "96.46447",
"x": 0, "x": 0,
"y": 0, "y": 0,
}, },
@ -1241,7 +1249,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": "1.71911",
"id": "id178", "id": "id178",
"index": "Zz", "index": "Zz",
"isDeleted": false, "isDeleted": false,
@ -1255,8 +1263,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"98.58579", "92.92893",
"1.30038", "1.71911",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1279,8 +1287,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": "98.58579", "width": "92.92893",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": 0,
} }
`; `;
@ -1613,7 +1621,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": "1.71911",
"id": "id181", "id": "id181",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
@ -1627,8 +1635,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"98.58579", "92.92893",
"1.30038", "1.71911",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1651,8 +1659,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": "98.58579", "width": "92.92893",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": 0,
} }
`; `;
@ -1771,7 +1779,7 @@ History {
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": "11.27227", "height": "12.86717",
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -1784,8 +1792,8 @@ History {
0, 0,
], ],
[ [
"98.58579", "92.92893",
"11.27227", "12.86717",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -1806,8 +1814,8 @@ History {
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": "98.58579", "width": "92.92893",
"x": "0.70711", "x": "3.53553",
"y": 0, "y": 0,
}, },
"inserted": { "inserted": {
@ -2321,12 +2329,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": "369.21589",
"id": "id186", "id": "id186",
"index": "a2", "index": "a2",
"isDeleted": false, "isDeleted": false,
@ -2340,8 +2348,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0, 0,
], ],
[ [
"502.78936", "496.84035",
"-374.05754", "-369.21589",
], ],
], ],
"roughness": 1, "roughness": 1,
@ -2352,7 +2360,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 +2368,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": "496.84035",
"x": "-0.83465", "x": "2.18463",
"y": "-36.58211", "y": "-38.80748",
} }
`; `;
@ -2481,7 +2489,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id185", "elementId": "id185",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -2499,7 +2507,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -2511,13 +2519,13 @@ History {
"startBinding": { "startBinding": {
"elementId": "id184", "elementId": "id184",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "96.46447",
"x": 0, "x": 0,
"y": 0, "y": 0,
}, },
@ -15161,7 +15169,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 +15188,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"98.58579", "92.92893",
0, 0,
], ],
], ],
@ -15192,7 +15200,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 +15208,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,
} }
`; `;
@ -15242,7 +15250,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -15255,7 +15263,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -15532,7 +15540,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id58", "elementId": "id58",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -15550,7 +15558,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -15562,13 +15570,13 @@ History {
"startBinding": { "startBinding": {
"elementId": "id56", "elementId": "id56",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "96.46447",
"x": 0, "x": 0,
"y": 0, "y": 0,
}, },
@ -15859,7 +15867,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 +15886,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"98.58579", "92.92893",
0, 0,
], ],
], ],
@ -15890,7 +15898,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 +15906,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 +16160,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id52", "elementId": "id52",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -16170,7 +16178,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -16182,13 +16190,13 @@ History {
"startBinding": { "startBinding": {
"elementId": "id50", "elementId": "id50",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "96.46447",
"x": 0, "x": 0,
"y": 0, "y": 0,
}, },
@ -16479,7 +16487,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 +16506,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"98.58579", "92.92893",
0, 0,
], ],
], ],
@ -16510,7 +16518,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 +16526,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 +16780,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id64", "elementId": "id64",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -16790,7 +16798,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -16802,13 +16810,13 @@ History {
"startBinding": { "startBinding": {
"elementId": "id62", "elementId": "id62",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "96.46447",
"x": 0, "x": 0,
"y": 0, "y": 0,
}, },
@ -17097,7 +17105,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 +17124,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"98.58579", "92.92893",
0, 0,
], ],
], ],
@ -17128,7 +17136,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 +17144,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,
} }
`; `;
@ -17193,14 +17201,14 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id68", "elementId": "id68",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
}, },
"inserted": { "inserted": {
@ -17210,7 +17218,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -17460,7 +17468,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id70", "elementId": "id70",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -17478,7 +17486,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -17490,13 +17498,13 @@ History {
"startBinding": { "startBinding": {
"elementId": "id68", "elementId": "id68",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "96.46447",
"x": 0, "x": 0,
"y": 0, "y": 0,
}, },
@ -17811,7 +17819,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 +17838,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
0, 0,
], ],
[ [
"98.58579", "92.92893",
0, 0,
], ],
], ],
@ -17842,7 +17850,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 +17858,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 +17921,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id77", "elementId": "id77",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"points": [ "points": [
[ [
@ -17921,14 +17929,14 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
"startBinding": { "startBinding": {
"elementId": "id75", "elementId": "id75",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
}, },
"inserted": { "inserted": {
@ -17939,7 +17947,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -18189,7 +18197,7 @@ History {
"endBinding": { "endBinding": {
"elementId": "id77", "elementId": "id77",
"focus": -0, "focus": -0,
"gap": 1, "gap": 5,
}, },
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
@ -18207,7 +18215,7 @@ History {
0, 0,
], ],
[ [
100, "96.46447",
0, 0,
], ],
], ],
@ -18219,13 +18227,13 @@ History {
"startBinding": { "startBinding": {
"elementId": "id75", "elementId": "id75",
"focus": 0, "focus": 0,
"gap": 1, "gap": 5,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"width": 100, "width": "96.46447",
"x": 0, "x": 0,
"y": 0, "y": 0,
}, },

View file

@ -44,14 +44,3 @@ exports[`Test Linear Elements > Test bound text element > should resize and posi
"Online whiteboard "Online whiteboard
collaboration made easy" collaboration made easy"
`; `;
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 1`] = `
"Online whiteboard
collaboration made easy"
`;
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 2`] = `
"Online whiteboard
collaboration made
easy"
`;

View file

@ -101,139 +101,3 @@ exports[`move element > rectangle 5`] = `
"y": 40, "y": 40,
} }
`; `;
exports[`move element > rectangles with binding arrow 5`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id2",
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 100,
"id": "id0",
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 3,
},
"seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 1723083209,
"width": 100,
"x": 0,
"y": 0,
}
`;
exports[`move element > rectangles with binding arrow 6`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id2",
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 300,
"id": "id1",
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 3,
},
"seed": 1150084233,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 7,
"versionNonce": 745419401,
"width": 300,
"x": 201,
"y": 2,
}
`;
exports[`move element > rectangles with binding arrow 7`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id1",
"focus": "-0.46667",
"gap": 10,
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "87.29887",
"id": "id2",
"index": "a2",
"isDeleted": false,
"lastCommittedPoint": null,
"link": null,
"locked": false,
"opacity": 100,
"points": [
[
0,
0,
],
[
"86.85786",
"87.29887",
],
],
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 1604849351,
"startArrowhead": null,
"startBinding": {
"elementId": "id0",
"focus": "-0.60000",
"gap": 10,
},
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 11,
"versionNonce": 1051383431,
"width": "86.85786",
"x": "107.07107",
"y": "47.07107",
}
`;

View file

@ -52,6 +52,8 @@ import * as StaticScene from "../renderer/staticScene";
import { Snapshot, CaptureUpdateAction } from "../store"; import { Snapshot, CaptureUpdateAction } from "../store";
import { AppStateChange, ElementsChange } from "../change"; import { AppStateChange, ElementsChange } from "../change";
import { FIXED_BINDING_DISTANCE } from "../element/binding.js";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { Keyboard, Pointer, UI } from "./helpers/ui"; import { Keyboard, Pointer, UI } from "./helpers/ui";
import { import {
@ -4779,12 +4781,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

@ -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.6, 1);
expect(arrow.height).toBeCloseTo(0); expect(arrow.height).toBeCloseTo(0);
}); });