Fix all tests

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

fix(transform): Fix group resize and rotate

fix(binding): Harmonize binding param usage

fix: Center focus point

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

chore: Trigger build

Remove binding gap

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs
2025-08-16 20:54:16 +02:00
parent dc7025e33e
commit da9d099993
9 changed files with 349 additions and 768 deletions

View File

@@ -35,10 +35,8 @@ import {
import {
bindingBorderTest,
getHoveredElementForBinding,
getHoveredElementForBindingAndIfItsPrecise,
hitElementItself,
intersectElementWithLineSegment,
maxBindingDistanceFromOutline,
} from "./collision";
import { distanceToElement } from "./distance";
import {
@@ -114,7 +112,6 @@ export type BindingStrategy =
};
export const FIXED_BINDING_DISTANCE = 5;
export const BINDING_HIGHLIGHT_THICKNESS = 10;
export const shouldEnableBindingForPointerEvent = (
event: React.PointerEvent<HTMLElement>,
@@ -202,7 +199,6 @@ const bindOrUnbindBindingElementEdge = (
const getOriginalBindingsIfStillCloseToBindingEnds = (
linearElement: NonDeleted<ExcalidrawArrowElement>,
elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"],
): (NonDeleted<ExcalidrawElement> | null)[] =>
(["start", "end"] as const).map((edge) => {
const coors = tupleToCoors(
@@ -224,7 +220,6 @@ const getOriginalBindingsIfStillCloseToBindingEnds = (
element,
pointFrom<GlobalPoint>(coors.x, coors.y),
elementsMap,
zoom,
)
) {
return element;
@@ -328,30 +323,16 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
draggingPoints.get(startDragged ? startIdx : endIdx)!.point,
elementsMap,
);
const { hovered, hit } = getHoveredElementForBindingAndIfItsPrecise(
point,
elements,
elementsMap,
appState.zoom,
true,
);
const hit = getHoveredElementForBinding(point, elements, elementsMap);
// With new arrows this handles the binding at arrow creation
if (startDragged) {
if (hovered) {
if (hit) {
start = {
element: hovered,
mode: "inside",
focusPoint: point,
};
} else {
start = {
element: hovered,
mode: "orbit",
focusPoint: point,
};
}
if (hit) {
start = {
element: hit,
mode: "inside",
focusPoint: point,
};
} else {
start = { mode: null };
}
@@ -365,41 +346,24 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
appState?.selectedLinearElement?.pointerDownState.arrowOriginalStartPoint;
// Inside -> inside binding
if (hovered && hit && arrow.startBinding?.elementId === hovered.id) {
if (hit && arrow.startBinding?.elementId === hit.id) {
const center = pointFrom<GlobalPoint>(
hovered.x + hovered.width / 2,
hovered.y + hovered.height / 2,
hit.x + hit.width / 2,
hit.y + hit.height / 2,
);
return {
start: {
mode: "inside",
element: hovered,
element: hit,
focusPoint: arrowOriginalStartPoint ?? center,
},
end: { mode: "inside", element: hovered, focusPoint: point },
};
}
// Inside -> orbit binding
if (hovered && !hit && arrow.startBinding?.elementId === hovered.id) {
const center = pointFrom<GlobalPoint>(
hovered.x + hovered.width / 2,
hovered.y + hovered.height / 2,
);
return {
start: {
mode: globalBindMode === "inside" ? "inside" : "orbit",
element: hovered,
focusPoint: arrowOriginalStartPoint ?? center,
},
end: { mode: null },
end: { mode: "inside", element: hit, focusPoint: point },
};
}
// Inside -> outside binding
if (arrow.startBinding && arrow.startBinding.elementId !== hovered?.id) {
if (arrow.startBinding && arrow.startBinding.elementId !== hit?.id) {
const otherElement = elementsMap.get(
arrow.startBinding.elementId,
) as ExcalidrawBindableElement;
@@ -411,21 +375,26 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
const other: BindingStrategy = {
mode: otherIsInsideBinding ? "inside" : "orbit",
element: otherElement,
focusPoint: snapToCenter(
otherElement,
elementsMap,
arrowOriginalStartPoint ?? pointFrom<GlobalPoint>(arrow.x, arrow.y),
),
focusPoint: otherIsInsideBinding
? arrowOriginalStartPoint ?? pointFrom<GlobalPoint>(arrow.x, arrow.y)
: snapToCenter(
otherElement,
elementsMap,
arrowOriginalStartPoint ??
pointFrom<GlobalPoint>(arrow.x, arrow.y),
),
};
// We are hovering another element with the end point
let current: BindingStrategy;
if (hovered) {
if (hit) {
const isInsideBinding = globalBindMode === "inside";
current = {
mode: isInsideBinding ? "inside" : "orbit",
element: hovered,
focusPoint: snapToCenter(hovered, elementsMap, point),
element: hit,
focusPoint: isInsideBinding
? point
: snapToCenter(hit, elementsMap, point),
};
} else {
current = { mode: null };
@@ -439,13 +408,13 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
// No start binding
if (!arrow.startBinding) {
if (hovered) {
if (hit) {
const isInsideBinding =
globalBindMode === "inside" || isAlwaysInsideBinding(hovered);
globalBindMode === "inside" || isAlwaysInsideBinding(hit);
end = {
mode: isInsideBinding ? "inside" : "orbit",
element: hovered,
element: hit,
focusPoint: point,
};
} else {
@@ -466,7 +435,6 @@ const bindingStrategyForSimpleArrowEndpointDragging = (
oppositeBinding: FixedPointBinding | null,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
zoom: AppState["zoom"],
globalBindMode?: AppState["bindMode"],
opts?: {
newArrow?: boolean;
@@ -476,23 +444,14 @@ const bindingStrategyForSimpleArrowEndpointDragging = (
let current: BindingStrategy = { mode: undefined };
let other: BindingStrategy = { mode: undefined };
const { hovered, hit } = getHoveredElementForBindingAndIfItsPrecise(
point,
elements,
elementsMap,
zoom,
true,
);
const hit = getHoveredElementForBinding(point, elements, elementsMap);
// If the global bind mode is in free binding mode, just bind
// where the pointer is and keep the other end intact
if (
globalBindMode === "inside" ||
(hovered && isAlwaysInsideBinding(hovered))
) {
current = hovered
if (globalBindMode === "inside" || (hit && isAlwaysInsideBinding(hit))) {
current = hit
? {
element: hovered,
element: hit,
focusPoint: point,
mode: "inside",
}
@@ -503,90 +462,58 @@ const bindingStrategyForSimpleArrowEndpointDragging = (
// Dragged point is outside of any bindable element
// so we break any existing binding
if (!hovered) {
if (!hit) {
return { current: { mode: null }, other };
}
// Dragged point is on the binding gap of a bindable element
if (!hit) {
// If the opposite binding (if exists) is on the same element
if (oppositeBinding) {
if (oppositeBinding.elementId === hovered.id) {
return { current: { mode: null }, other };
}
// The opposite binding is on a different element
// eslint-disable-next-line no-else-return
else {
current = {
element: hovered,
mode: "orbit",
focusPoint: opts?.newArrow
? pointFrom<GlobalPoint>(
hovered.x + hovered.width / 2,
hovered.y + hovered.height / 2,
)
: point,
};
return { current, other };
}
}
// No opposite binding or the opposite binding is on a different element
current = { element: hovered, mode: "orbit", focusPoint: point };
}
// The dragged point is inside the hovered bindable element
else {
// The opposite binding is on the same element
// eslint-disable-next-line no-lonely-if
if (oppositeBinding) {
if (oppositeBinding.elementId === hovered.id) {
// The opposite binding is on the binding gap of the same element
if (oppositeBinding.mode !== "inside") {
current = { element: hovered, mode: "orbit", focusPoint: point };
other = { mode: null };
return { current, other };
}
// The opposite binding is inside the same element
// eslint-disable-next-line no-else-return
else {
current = { element: hovered, mode: "inside", focusPoint: point };
// The opposite binding is on the same element
// eslint-disable-next-line no-lonely-if
if (oppositeBinding) {
if (oppositeBinding.elementId === hit.id) {
// The opposite binding is on the binding gap of the same element
if (oppositeBinding.mode !== "inside") {
current = { element: hit, mode: "orbit", focusPoint: point };
other = { mode: null };
return { current, other };
}
return { current, other };
}
// The opposite binding is on a different element
// The opposite binding is inside the same element
// eslint-disable-next-line no-else-return
else {
current = {
element: hovered,
mode: "orbit",
focusPoint: opts?.newArrow
? pointFrom<GlobalPoint>(
hovered.x + hovered.width / 2,
hovered.y + hovered.height / 2,
)
: point,
};
current = { element: hit, mode: "inside", focusPoint: point };
return { current, other };
}
}
// The opposite binding is on a different element or no binding
// The opposite binding is on a different element
// eslint-disable-next-line no-else-return
else {
current = {
element: hovered,
element: hit,
mode: "orbit",
focusPoint: opts?.newArrow
? pointFrom<GlobalPoint>(
hovered.x + hovered.width / 2,
hovered.y + hovered.height / 2,
hit.x + hit.width / 2,
hit.y + hit.height / 2,
)
: point,
};
return { current, other };
}
}
// The opposite binding is on a different element or no binding
else {
current = {
element: hit,
mode: "orbit",
focusPoint: opts?.newArrow
? pointFrom<GlobalPoint>(hit.x + hit.width / 2, hit.y + hit.height / 2)
: point,
};
}
// Must return as only one endpoint is dragged, therefore
// the end binding strategy might accidentally gets overriden
@@ -649,7 +576,6 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
p,
elements,
elementsMap,
appState.zoom,
);
const current: BindingStrategy = hoveredElement
? {
@@ -697,7 +623,6 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
arrow.endBinding,
elementsMap,
elements,
appState.zoom,
globalBindMode,
opts,
);
@@ -719,7 +644,6 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
arrow.startBinding,
elementsMap,
elements,
appState.zoom,
globalBindMode,
opts,
);
@@ -748,7 +672,6 @@ export const bindOrUnbindBindingElements = (
export const getSuggestedBindingsForBindingElements = (
selectedElements: NonDeleted<ExcalidrawElement>[],
elementsMap: NonDeletedSceneElementsMap,
zoom: AppState["zoom"],
): SuggestedBinding[] => {
// HOT PATH: Bail out if selected elements list is too large
if (selectedElements.length > 50) {
@@ -759,11 +682,7 @@ export const getSuggestedBindingsForBindingElements = (
selectedElements
.filter(isArrowElement)
.flatMap((element) =>
getOriginalBindingsIfStillCloseToBindingEnds(
element,
elementsMap,
zoom,
),
getOriginalBindingsIfStillCloseToBindingEnds(element, elementsMap),
)
.filter(
(element): element is NonDeleted<ExcalidrawBindableElement> =>
@@ -785,7 +704,6 @@ export const maybeSuggestBindingsForBindingElementAtCoords = (
linearElement: NonDeleted<ExcalidrawArrowElement>,
startOrEndOrBoth: "start" | "end" | "both",
scene: Scene,
zoom: AppState["zoom"],
): ExcalidrawBindableElement[] => {
const startCoords = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
@@ -801,13 +719,11 @@ export const maybeSuggestBindingsForBindingElementAtCoords = (
startCoords,
scene.getNonDeletedElements(),
scene.getNonDeletedElementsMap(),
zoom,
);
const endHovered = getHoveredElementForBinding(
endCoords,
scene.getNonDeletedElements(),
scene.getNonDeletedElementsMap(),
zoom,
);
const suggestedBindings = [];
@@ -1075,7 +991,6 @@ export const getHeadingForElbowArrowSnap = (
aabb: Bounds | undefined | null,
origPoint: GlobalPoint,
elementsMap: ElementsMap,
zoom?: AppState["zoom"],
): Heading => {
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
@@ -1084,14 +999,8 @@ export const getHeadingForElbowArrowSnap = (
}
const d = distanceToElement(bindableElement, elementsMap, origPoint);
const bindDistance = maxBindingDistanceFromOutline(
bindableElement,
bindableElement.width,
bindableElement.height,
zoom,
);
const distance = d > bindDistance ? null : d;
const distance = d > 0 ? null : d;
if (!distance) {
return vectorToHeading(

View File

@@ -25,7 +25,7 @@ import type {
Radians,
} from "@excalidraw/math";
import type { AppState, FrameNameBounds } from "@excalidraw/excalidraw/types";
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
import { isPathALoop } from "./utils";
import {
@@ -58,8 +58,6 @@ import { LinearElementEditor } from "./linearElementEditor";
import { distanceToElement } from "./distance";
import { BINDING_HIGHLIGHT_THICKNESS, FIXED_BINDING_DISTANCE } from "./binding";
import type {
ElementsMap,
ExcalidrawBindableElement,
@@ -206,40 +204,12 @@ export const hitElementBoundText = (
return isPointInElement(point, boundTextElement, elementsMap);
};
export const maxBindingDistanceFromOutline = (
element: ExcalidrawElement,
elementWidth: number,
elementHeight: number,
zoom?: AppState["zoom"],
): number => {
const zoomValue = zoom?.value && zoom.value < 1 ? zoom.value : 1;
// Aligns diamonds with rectangles
const shapeRatio = element.type === "diamond" ? 1 / Math.sqrt(2) : 1;
const smallerDimension = shapeRatio * Math.min(elementWidth, elementHeight);
return Math.max(
16,
// bigger bindable boundary for bigger elements
Math.min(0.25 * smallerDimension, 32),
// keep in sync with the zoomed highlight
BINDING_HIGHLIGHT_THICKNESS / zoomValue + FIXED_BINDING_DISTANCE,
);
};
export const bindingBorderTest = (
element: NonDeleted<ExcalidrawBindableElement>,
[x, y]: Readonly<GlobalPoint>,
elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"],
): boolean => {
const p = pointFrom<GlobalPoint>(x, y);
const threshold = maxBindingDistanceFromOutline(
element,
element.width,
element.height,
zoom,
);
const shouldTestInside =
// disable fullshape snapping for frame elements so we
// can bind to frame children
@@ -247,12 +217,7 @@ export const bindingBorderTest = (
// PERF: Run a cheap test to see if the binding element
// is even close to the element
const bounds = [
x - threshold,
y - threshold,
x + threshold,
y + threshold,
] as Bounds;
const bounds = [x - 1, y - 1, x + 1, y + 1] as Bounds;
const elementBounds = getElementBounds(element, elementsMap);
if (!doBoundsIntersect(bounds, elementBounds)) {
return false;
@@ -267,15 +232,14 @@ export const bindingBorderTest = (
const distance = distanceToElement(element, elementsMap, p);
return shouldTestInside
? intersections.length === 0 || distance <= threshold
: intersections.length > 0 && distance <= threshold;
? intersections.length === 0
: intersections.length > 0 && distance <= 1;
};
export const getHoveredElementForBinding = (
point: Readonly<GlobalPoint>,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
elementsMap: NonDeletedSceneElementsMap,
zoom?: AppState["zoom"],
): NonDeleted<ExcalidrawBindableElement> | null => {
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
// We need to to hit testing from front (end of the array) to back (beginning of the array)
@@ -291,7 +255,7 @@ export const getHoveredElementForBinding = (
if (
isBindableElement(element, false) &&
bindingBorderTest(element, point, elementsMap, zoom)
bindingBorderTest(element, point, elementsMap)
) {
candidateElements.push(element);
}
@@ -313,36 +277,6 @@ export const getHoveredElementForBinding = (
.pop() as NonDeleted<ExcalidrawBindableElement>;
};
export const getHoveredElementForBindingAndIfItsPrecise = (
point: GlobalPoint,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
elementsMap: NonDeletedSceneElementsMap,
zoom: AppState["zoom"],
shouldTestInside: boolean = true,
): {
hovered: NonDeleted<ExcalidrawBindableElement> | null;
hit: boolean;
} => {
const hoveredElement = getHoveredElementForBinding(
point,
elements,
elementsMap,
zoom,
);
// TODO: Optimize this to avoid recalculating the point - element distance
const hit =
!!hoveredElement &&
hitElementItself({
element: hoveredElement,
elementsMap,
point,
threshold: 0,
overrideShouldTestInside: shouldTestInside,
});
return { hovered: hoveredElement, hit };
};
/**
* Intersect a line with an element for binding test
*

View File

@@ -1216,19 +1216,9 @@ const getElbowArrowData = (
if (options?.isDragging) {
const elements = Array.from(elementsMap.values());
hoveredStartElement =
getHoveredElement(
origStartGlobalPoint,
elementsMap,
elements,
options?.zoom,
) || null;
getHoveredElement(origStartGlobalPoint, elementsMap, elements) || null;
hoveredEndElement =
getHoveredElement(
origEndGlobalPoint,
elementsMap,
elements,
options?.zoom,
) || null;
getHoveredElement(origEndGlobalPoint, elementsMap, elements) || null;
} else {
hoveredStartElement = arrow.startBinding
? getBindableElementForId(arrow.startBinding.elementId, elementsMap) ||
@@ -2262,9 +2252,8 @@ const getHoveredElement = (
origPoint: GlobalPoint,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
zoom?: AppState["zoom"],
) => {
return getHoveredElementForBinding(origPoint, elements, elementsMap, zoom);
return getHoveredElementForBinding(origPoint, elements, elementsMap);
};
const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean =>

View File

@@ -465,7 +465,6 @@ export class LinearElementEditor {
? "start"
: "end",
app.scene,
app.state.zoom,
);
}
}
@@ -2001,7 +2000,6 @@ const pointDraggingUpdates = (
newGlobalPointPosition,
elements,
elementsMap,
app.state.zoom,
);
const otherGlobalPoint =
LinearElementEditor.getPointAtIndexGlobalCoordinates(
@@ -2011,12 +2009,7 @@ const pointDraggingUpdates = (
);
const otherPointInsideElement =
!!hoveredElement &&
!!bindingBorderTest(
hoveredElement,
otherGlobalPoint,
elementsMap,
app.state.zoom,
);
!!bindingBorderTest(hoveredElement, otherGlobalPoint, elementsMap);
if (
isBindingEnabled(app.state) &&

View File

@@ -20,7 +20,11 @@ import type { PointerDownState } from "@excalidraw/excalidraw/types";
import type { Mutable } from "@excalidraw/common/utility-types";
import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
import {
getArrowLocalFixedPoints,
unbindBindingElement,
updateBoundElements,
} from "./binding";
import {
getElementAbsoluteCoords,
getCommonBounds,
@@ -229,12 +233,18 @@ const rotateSingleElement = (
if (isBindingElement(element)) {
update = {
...update,
startBinding: null,
endBinding: null,
} as ElementUpdate<ExcalidrawArrowElement>;
if (element.startBinding) {
unbindBindingElement(element, "start", scene);
}
if (element.endBinding) {
unbindBindingElement(element, "end", scene);
}
}
scene.mutateElement(element, update);
if (boundTextElementId) {
const textElement =
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
@@ -399,6 +409,11 @@ const rotateMultipleElements = (
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
}
const rotatedElementsMap = new Map<
ExcalidrawElement["id"],
NonDeletedExcalidrawElement
>(elements.map((element) => [element.id, element]));
for (const element of elements) {
if (!isFrameLikeElement(element)) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
@@ -429,6 +444,19 @@ const rotateMultipleElements = (
simultaneouslyUpdated: elements,
});
if (isBindingElement(element)) {
if (element.startBinding) {
if (!rotatedElementsMap.has(element.startBinding.elementId)) {
unbindBindingElement(element, "start", scene);
}
}
if (element.endBinding) {
if (!rotatedElementsMap.has(element.endBinding.elementId)) {
unbindBindingElement(element, "end", scene);
}
}
}
const boundText = getBoundTextElement(element, elementsMap);
if (boundText && !isArrowElement(element)) {
scene.mutateElement(boundText, {
@@ -845,8 +873,11 @@ export const resizeSingleElement = (
if (latestElement.startBinding) {
updates = {
...updates,
startBinding: null,
} as ElementUpdate<ExcalidrawArrowElement>;
if (latestElement.startBinding) {
unbindBindingElement(latestElement, "start", scene);
}
}
if (latestElement.endBinding) {
@@ -1408,6 +1439,10 @@ export const resizeMultipleElements = (
}
const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
const resizedElementsMap = new Map<
ExcalidrawElement["id"],
NonDeletedExcalidrawElement
>(elementsAndUpdates.map(({ element }) => [element.id, element]));
for (const {
element,
@@ -1421,6 +1456,19 @@ export const resizeMultipleElements = (
simultaneouslyUpdated: elementsToUpdate,
});
if (isBindingElement(element)) {
if (element.startBinding) {
if (!resizedElementsMap.has(element.startBinding.elementId)) {
unbindBindingElement(element, "start", scene);
}
}
if (element.endBinding) {
if (!resizedElementsMap.has(element.endBinding.elementId)) {
unbindBindingElement(element, "end", scene);
}
}
}
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && boundTextFontSize) {
scene.mutateElement(boundTextElement, {

View File

@@ -4537,7 +4537,6 @@ class App extends React.Component<AppProps, AppState> {
(element) => element.id !== elbowArrow?.id || step !== 0,
),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
),
});
@@ -4766,7 +4765,6 @@ class App extends React.Component<AppProps, AppState> {
pointFrom<GlobalPoint>(scenePointer.x, scenePointer.y),
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
);
if (hoveredElement && !this.bindModeHandler) {
@@ -6098,7 +6096,6 @@ class App extends React.Component<AppProps, AppState> {
newElement,
"end",
this.scene,
this.state.zoom,
),
});
} else {
@@ -6203,7 +6200,6 @@ class App extends React.Component<AppProps, AppState> {
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
this.scene.getNonDeletedElements(),
elementsMap,
this.state.zoom,
);
// Timed bind mode handler for arrow elements
@@ -7991,7 +7987,6 @@ class App extends React.Component<AppProps, AppState> {
),
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
);
this.setState({
@@ -8197,7 +8192,6 @@ class App extends React.Component<AppProps, AppState> {
lastGlobalPoint,
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
);
// clicking inside commit zone → finalize arrow
@@ -8318,7 +8312,6 @@ class App extends React.Component<AppProps, AppState> {
point,
this.scene.getNonDeletedElements(),
elementsMap,
this.state.zoom,
);
this.scene.mutateElement(element, {
@@ -8764,7 +8757,6 @@ class App extends React.Component<AppProps, AppState> {
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
this.scene.getNonDeletedElements(),
elementsMap,
this.state.zoom,
);
// Timed bind mode handler for arrow elements
@@ -9141,7 +9133,6 @@ class App extends React.Component<AppProps, AppState> {
suggestedBindings: getSuggestedBindingsForBindingElements(
selectedElements,
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
),
});
}
@@ -9451,7 +9442,6 @@ class App extends React.Component<AppProps, AppState> {
newElement,
"end",
this.scene,
this.state.zoom,
),
});
}
@@ -11002,7 +10992,6 @@ class App extends React.Component<AppProps, AppState> {
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
);
this.setState({
suggestedBindings:
@@ -11625,7 +11614,6 @@ class App extends React.Component<AppProps, AppState> {
const suggestedBindings = getSuggestedBindingsForBindingElements(
selectedElements,
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
);
const elementsToHighlight = new Set<ExcalidrawElement>();

View File

@@ -16,10 +16,7 @@ import {
throttleRAF,
} from "@excalidraw/common";
import {
FIXED_BINDING_DISTANCE,
maxBindingDistanceFromOutline,
} from "@excalidraw/element";
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element";
import { LinearElementEditor } from "@excalidraw/element";
import {
getOmitSidesForDevice,
@@ -197,12 +194,7 @@ const renderBindingHighlightForBindableElement = (
elementsMap: ElementsMap,
zoom: InteractiveCanvasAppState["zoom"],
) => {
const padding = maxBindingDistanceFromOutline(
element,
element.width,
element.height,
zoom,
);
const padding = 5;
context.fillStyle = "rgba(0,0,0,.05)";
@@ -251,14 +243,9 @@ const renderBindingHighlightForSuggestedPointBinding = (
elementsMap: ElementsMap,
zoom: InteractiveCanvasAppState["zoom"],
) => {
const [element, startOrEnd, bindableElement] = suggestedBinding;
const [element, startOrEnd] = suggestedBinding;
const threshold = maxBindingDistanceFromOutline(
bindableElement,
bindableElement.width,
bindableElement.height,
zoom,
);
const threshold = 0;
context.strokeStyle = "rgba(0,0,0,0)";
context.fillStyle = "rgba(0,0,0,.05)";

View File

@@ -143,7 +143,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 5,
"version": 13,
"width": 100,
"x": -100,
"y": -50,
@@ -173,7 +173,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"version": 9,
"width": 100,
"x": 100,
"y": -50,
@@ -188,11 +188,18 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": null,
"endBinding": {
"elementId": "id15",
"fixedPoint": [
"0.50000",
1,
],
"mode": "orbit",
},
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "0.03787",
"height": "113.98784",
"id": "id4",
"index": "a2",
"isDeleted": false,
@@ -208,7 +215,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
],
[
95,
"0.03787",
"113.98784",
],
],
"roughness": 1,
@@ -222,23 +229,58 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
1,
"0.50010",
],
"mode": "orbit",
"mode": "inside",
},
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 16,
"version": 37,
"width": 95,
"x": 5,
"y": "0.01199",
"x": 0,
"y": "0.01000",
}
`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of elements 1`] = `3`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] element 3 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id4",
"type": "arrow",
},
],
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 50,
"id": "id15",
"index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 10,
"width": 50,
"x": 100,
"y": 100,
}
`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of renders 1`] = `12`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of elements 1`] = `4`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] number of renders 1`] = `21`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and the arrow got bound to a different element in the meantime > [end of test] redo stack 1`] = `
[
@@ -256,7 +298,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"id1": {
"deleted": {
"boundElements": [],
"version": 4,
"version": 9,
},
"inserted": {
"boundElements": [
@@ -265,13 +307,35 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
},
],
"version": 3,
"version": 8,
},
},
"id15": {
"deleted": {
"boundElements": [
{
"id": "id4",
"type": "arrow",
},
],
"version": 9,
},
"inserted": {
"boundElements": [],
"version": 8,
},
},
"id4": {
"deleted": {
"endBinding": null,
"height": "0.88851",
"endBinding": {
"elementId": "id15",
"fixedPoint": [
"0.50000",
1,
],
"mode": "orbit",
},
"height": "100.79596",
"points": [
[
0,
@@ -279,29 +343,30 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
],
[
90,
"0.88851",
"100.79596",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.60000",
],
"mode": "orbit",
},
"version": 14,
"version": 36,
"width": 90,
},
"inserted": {
"endBinding": {
"elementId": "id1",
"fixedPoint": [
0,
"0.50010",
"0.60000",
],
"mode": "orbit",
},
"height": "0.00047",
"height": "0.00000",
"points": [
[
0,
@@ -309,23 +374,24 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
],
[
90,
"0.00047",
"0.00000",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.60000",
],
"mode": "orbit",
},
"version": 12,
"version": 33,
"width": 90,
},
},
},
},
"id": "id17",
"id": "id22",
},
{
"appState": AppStateDelta {
@@ -340,7 +406,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"updated": {
"id4": {
"deleted": {
"height": "0.03787",
"height": "113.98784",
"points": [
[
0,
@@ -348,7 +414,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
],
[
95,
"0.03787",
"113.98784",
],
],
"startBinding": {
@@ -357,13 +423,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
1,
"0.50010",
],
"mode": "orbit",
"mode": "inside",
},
"version": 16,
"version": 37,
"width": 95,
"x": 0,
"y": "0.01000",
},
"inserted": {
"height": "0.88851",
"height": "100.79596",
"points": [
[
0,
@@ -371,24 +439,26 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
],
[
90,
"0.88851",
"100.79596",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.60000",
],
"mode": "orbit",
},
"version": 14,
"version": 36,
"width": 90,
"x": 5,
"y": "15.52629",
},
},
},
},
"id": "id18",
"id": "id23",
},
]
`;
@@ -570,136 +640,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
},
"id": "id6",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id4": {
"deleted": {
"height": "0.95000",
"points": [
[
0,
0,
],
[
95,
"-0.95000",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "inside",
},
"version": 7,
"width": 95,
"x": 5,
"y": "0.95000",
},
"inserted": {
"height": 0,
"points": [
[
0,
0,
],
[
100,
0,
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "inside",
},
"version": 6,
"width": 100,
"x": 0,
"y": 0,
},
},
},
},
"id": "id9",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id4": {
"deleted": {
"height": "0.00950",
"points": [
[
0,
0,
],
[
95,
"-0.00950",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "orbit",
},
"version": 9,
"y": "0.00950",
},
"inserted": {
"height": "0.95000",
"points": [
[
0,
0,
],
[
95,
"-0.95000",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "inside",
},
"version": 7,
"y": "0.95000",
},
},
},
},
"id": "id11",
},
]
`;
@@ -846,9 +786,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 5,
"version": 14,
"width": 100,
"x": -100,
"x": 150,
"y": -50,
}
`;
@@ -876,9 +816,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"version": 9,
"width": 100,
"x": 100,
"x": 150,
"y": -50,
}
`;
@@ -895,7 +835,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": "0.88851",
"height": "0.01000",
"id": "id4",
"index": "a2",
"isDeleted": false,
@@ -910,8 +850,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
0,
],
[
90,
"0.88851",
0,
"-0.01000",
],
],
"roughness": 1,
@@ -925,23 +865,23 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
1,
"0.50010",
],
"mode": "orbit",
"mode": "inside",
},
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 18,
"width": 90,
"x": 5,
"y": "0.05936",
"version": 30,
"width": 0,
"x": 250,
"y": "0.01000",
}
`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of elements 1`] = `3`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of renders 1`] = `14`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] number of renders 1`] = `23`;
exports[`history > multiplayer undo/redo > conflicts in arrows and their bindable elements > should rebind bindings when both are updated through the history and there are no conflicting updates in the meantime > [end of test] redo stack 1`] = `
[
@@ -959,7 +899,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"id1": {
"deleted": {
"boundElements": [],
"version": 4,
"version": 9,
},
"inserted": {
"boundElements": [
@@ -968,62 +908,66 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"type": "arrow",
},
],
"version": 3,
"version": 8,
},
},
"id4": {
"deleted": {
"endBinding": null,
"height": "0.00900",
"height": "3.00000",
"points": [
[
0,
0,
],
[
90,
"-0.00900",
-45,
"-3.00000",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.60000",
],
"mode": "orbit",
},
"version": 16,
"version": 29,
"width": 45,
"y": "3.00000",
},
"inserted": {
"endBinding": {
"elementId": "id1",
"fixedPoint": [
0,
"0.50010",
"0.60000",
],
"mode": "orbit",
},
"height": "0.04676",
"height": 0,
"points": [
[
0,
0,
],
[
90,
"-0.04676",
0,
0,
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.60000",
],
"mode": "orbit",
},
"version": 14,
"version": 28,
"width": 0,
"y": "9.99861",
},
},
},
@@ -1043,15 +987,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"updated": {
"id4": {
"deleted": {
"height": "0.88851",
"height": "0.01000",
"points": [
[
0,
0,
],
[
90,
"0.88851",
0,
"-0.01000",
],
],
"startBinding": {
@@ -1060,33 +1004,37 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
1,
"0.50010",
],
"mode": "orbit",
"mode": "inside",
},
"version": 18,
"y": "0.05936",
"version": 30,
"width": 0,
"x": 250,
"y": "0.01000",
},
"inserted": {
"height": "0.00900",
"height": "3.00000",
"points": [
[
0,
0,
],
[
90,
"-0.00900",
-45,
"-3.00000",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
"0.60000",
],
"mode": "orbit",
},
"version": 16,
"y": "0.00950",
"version": 29,
"width": 45,
"x": 145,
"y": "3.00000",
},
},
},
@@ -1273,262 +1221,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
},
"id": "id6",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id4": {
"deleted": {
"height": "0.95000",
"points": [
[
0,
0,
],
[
95,
"-0.95000",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "inside",
},
"version": 7,
"width": 95,
"x": 5,
"y": "0.95000",
},
"inserted": {
"height": 0,
"points": [
[
0,
0,
],
[
100,
0,
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "inside",
},
"version": 6,
"width": 100,
"x": 0,
"y": 0,
},
},
},
},
"id": "id9",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id4": {
"deleted": {
"height": 0,
"points": [
[
0,
0,
],
[
95,
0,
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "inside",
},
"version": 8,
"y": 0,
},
"inserted": {
"height": "0.95000",
"points": [
[
0,
0,
],
[
95,
"-0.95000",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "inside",
},
"version": 7,
"y": "0.95000",
},
},
},
},
"id": "id11",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id4": {
"deleted": {
"height": "0.00950",
"points": [
[
0,
0,
],
[
95,
"-0.00950",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "orbit",
},
"version": 10,
"y": "0.00950",
},
"inserted": {
"height": 0,
"points": [
[
0,
0,
],
[
95,
0,
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "inside",
},
"version": 8,
"y": 0,
},
},
},
},
"id": "id13",
},
{
"appState": AppStateDelta {
"delta": Delta {
"deleted": {},
"inserted": {},
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"id4": {
"deleted": {
"height": "0.93837",
"points": [
[
0,
0,
],
[
90,
"0.93837",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "orbit",
},
"version": 11,
"width": 90,
},
"inserted": {
"height": "0.00950",
"points": [
[
0,
0,
],
[
95,
"-0.00950",
],
],
"startBinding": {
"elementId": "id0",
"fixedPoint": [
1,
"0.50010",
],
"mode": "orbit",
},
"version": 10,
"width": 95,
},
},
},
},
"id": "id16",
},
]
`;

View File

@@ -4557,16 +4557,30 @@ describe("history", () => {
// create start binding
mouse.downAt(0, 0);
mouse.moveTo(0, 1);
mouse.moveTo(0, 0);
mouse.moveTo(0, 10);
mouse.moveTo(0, 10);
mouse.up();
// create end binding
mouse.downAt(100, 0);
mouse.moveTo(100, 1);
mouse.moveTo(100, 0);
mouse.moveTo(100, 10);
mouse.moveTo(100, 10);
mouse.up();
expect(
(h.elements[2] as ExcalidrawElbowArrowElement).startBinding
?.fixedPoint,
).not.toEqual([1, 0.5001]);
expect(
(h.elements[2] as ExcalidrawElbowArrowElement).startBinding?.mode,
).toBe("orbit");
expect(
(h.elements[2] as ExcalidrawElbowArrowElement).endBinding,
).not.toEqual([1, 0.5001]);
expect(
(h.elements[2] as ExcalidrawElbowArrowElement).endBinding?.mode,
).toBe("orbit");
expect(h.elements).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -4606,12 +4620,16 @@ describe("history", () => {
expect(h.elements).toEqual([
expect.objectContaining({
id: rect1.id,
boundElements: [],
boundElements: [{ id: arrowId, type: "arrow" }],
}),
expect.objectContaining({ id: rect2.id, boundElements: [] }),
expect.objectContaining({
id: arrowId,
startBinding: null,
startBinding: expect.objectContaining({
elementId: rect1.id,
fixedPoint: [1, 0.5001],
mode: "inside",
}),
endBinding: null,
}),
]);
@@ -4656,13 +4674,13 @@ describe("history", () => {
id: arrowId,
startBinding: expect.objectContaining({
elementId: rect1.id,
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
fixedPoint: [1, 0.6],
mode: "orbit",
}),
endBinding: expect.objectContaining({
elementId: rect2.id,
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
fixedPoint: [0, 0.6],
mode: "orbit",
}),
}),
]),
@@ -4675,12 +4693,21 @@ describe("history", () => {
expect(h.elements).toEqual([
expect.objectContaining({
id: rect1.id,
boundElements: [],
boundElements: [
expect.objectContaining({
id: arrowId,
type: "arrow",
}),
],
}),
expect.objectContaining({ id: rect2.id, boundElements: [] }),
expect.objectContaining({
id: arrowId,
startBinding: null,
startBinding: expect.objectContaining({
elementId: rect1.id,
fixedPoint: [1, 0.5001],
mode: "inside",
}),
endBinding: null,
}),
]);
@@ -4699,13 +4726,13 @@ describe("history", () => {
// create start binding
mouse.downAt(0, 0);
mouse.moveTo(0, 1);
mouse.upAt(0, 0);
mouse.moveTo(0, 10);
mouse.upAt(0, 10);
// create end binding
mouse.downAt(100, 0);
mouse.moveTo(100, 1);
mouse.upAt(100, 0);
mouse.moveTo(100, 10);
mouse.upAt(100, 10);
expect(h.elements).toEqual(
expect.arrayContaining([
@@ -4746,12 +4773,21 @@ describe("history", () => {
expect(h.elements).toEqual([
expect.objectContaining({
id: rect1.id,
boundElements: [],
boundElements: [
expect.objectContaining({
id: arrowId,
type: "arrow",
}),
],
}),
expect.objectContaining({ id: rect2.id, boundElements: [] }),
expect.objectContaining({
id: arrowId,
startBinding: null,
startBinding: expect.objectContaining({
elementId: rect1.id,
fixedPoint: [1, 0.5001],
mode: "inside",
}),
endBinding: null,
}),
]);
@@ -4799,14 +4835,14 @@ describe("history", () => {
id: arrowId,
startBinding: expect.objectContaining({
elementId: rect1.id,
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
fixedPoint: [1, 0.6],
mode: "orbit",
}),
// rebound with previous rectangle
endBinding: expect.objectContaining({
elementId: rect2.id,
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
fixedPoint: [0, 0.6],
mode: "orbit",
}),
}),
expect.objectContaining({
@@ -4824,7 +4860,12 @@ describe("history", () => {
expect.arrayContaining([
expect.objectContaining({
id: rect1.id,
boundElements: [],
boundElements: [
expect.objectContaining({
id: arrowId,
type: "arrow",
}),
],
}),
expect.objectContaining({
id: rect2.id,
@@ -4832,16 +4873,16 @@ describe("history", () => {
}),
expect.objectContaining({
id: arrowId,
startBinding: null,
startBinding: expect.objectContaining({
elementId: rect1.id,
fixedPoint: [1, 0.5001],
mode: "inside",
}),
endBinding: expect.objectContaining({
// now we are back in the previous state!
elementId: remoteContainer.id,
fixedPoint: [
expect.toBeNonNaNNumber(),
expect.toBeNonNaNNumber(),
],
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
fixedPoint: [0.5, 1],
mode: "orbit",
}),
}),
expect.objectContaining({