Compare commits
52 Commits
master
...
mtolmacs/f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a5a74be45d | ||
![]() |
3f9c6299a0 | ||
![]() |
3068787ac4 | ||
![]() |
c2b78346c1 | ||
![]() |
44df764a88 | ||
![]() |
cc01e16e52 | ||
![]() |
6b9fa5bcc5 | ||
![]() |
06b3750a2f | ||
![]() |
c3924a8f8c | ||
![]() |
eecabccf8d | ||
![]() |
ccda36a0e3 | ||
![]() |
37653484a1 | ||
![]() |
b2e19055bf | ||
![]() |
3d40221dc1 | ||
![]() |
5cc5c626df | ||
![]() |
9a599cfc05 | ||
![]() |
88d4c4fe8d | ||
![]() |
4efa6f69e5 | ||
![]() |
a79eb06939 | ||
![]() |
bcbd418154 | ||
![]() |
db9e501d35 | ||
![]() |
ce10087edc | ||
![]() |
76a782bd52 | ||
![]() |
d6d4d00f60 | ||
![]() |
4ea534a732 | ||
![]() |
e90350b7d1 | ||
![]() |
ea5ad1412c | ||
![]() |
c8ade51b53 | ||
![]() |
6e520fdbb9 | ||
![]() |
fdd7420e65 | ||
![]() |
f4abdc751e | ||
![]() |
946d3ddf87 | ||
![]() |
fbde68c849 | ||
![]() |
4ee99de2fb | ||
![]() |
4d1e2c2bbb | ||
![]() |
1a87aa8e55 | ||
![]() |
528e6aa2df | ||
![]() |
8c9666b8ab | ||
![]() |
8ac508af11 | ||
![]() |
cbe6705c98 | ||
![]() |
1819661828 | ||
![]() |
373b940e75 | ||
![]() |
2f02d72741 | ||
![]() |
a54322a34f | ||
![]() |
5c1fc2f4fb | ||
![]() |
63d53fc242 | ||
![]() |
e1812c4c91 | ||
![]() |
e459ea0cc7 | ||
![]() |
f354285d69 | ||
![]() |
03b91deb4a | ||
![]() |
dca9fbe306 | ||
![]() |
f363fcabd8 |
@ -18,7 +18,7 @@ import {
|
|||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { isCurve } from "@excalidraw/math/curve";
|
import { isCurve } from "@excalidraw/math/curve";
|
||||||
|
|
||||||
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
import type { DebugElement } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { Curve } from "@excalidraw/math";
|
import type { Curve } from "@excalidraw/math";
|
||||||
|
|
||||||
|
@ -9,3 +9,4 @@ export * from "./promise-pool";
|
|||||||
export * from "./random";
|
export * from "./random";
|
||||||
export * from "./url";
|
export * from "./url";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
|
export * from "./visualdebug";
|
||||||
|
@ -437,26 +437,12 @@ export const _generateElementShape = (
|
|||||||
: [pointFrom<LocalPoint>(0, 0)];
|
: [pointFrom<LocalPoint>(0, 0)];
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
// NOTE (mtolmacs): Temporary fix for extremely big arrow shapes
|
|
||||||
if (
|
|
||||||
!points.every(
|
|
||||||
(point) => Math.abs(point[0]) <= 1e6 && Math.abs(point[1]) <= 1e6,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
console.error(
|
|
||||||
`Elbow arrow with extreme point positions detected. Arrow not rendered.`,
|
|
||||||
element.id,
|
|
||||||
JSON.stringify(points),
|
|
||||||
);
|
|
||||||
shape = [];
|
|
||||||
} else {
|
|
||||||
shape = [
|
shape = [
|
||||||
generator.path(
|
generator.path(
|
||||||
generateElbowArrowShape(points, 16),
|
generateElbowArrowShape(points, 16),
|
||||||
generateRoughOptions(element, true),
|
generateRoughOptions(element, true),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
|
||||||
} else if (!element.roundness) {
|
} else if (!element.roundness) {
|
||||||
// curve is always the first element
|
// curve is always the first element
|
||||||
// this simplifies finding the curve for an element
|
// this simplifies finding the curve for an element
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
PRECISION,
|
PRECISION,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { isPointOnShape } from "@excalidraw/utils/collision";
|
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
@ -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;
|
||||||
@ -223,13 +223,17 @@ const bindOrUnbindLinearElementEdge = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
export const getOriginalBindingsIfStillCloseToArrowEnds = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
edge: "start" | "end",
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
): NonDeleted<ExcalidrawElement> | null => {
|
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
||||||
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
["start", "end"].map((edge) => {
|
||||||
|
const coors = getLinearElementEdgeCoors(
|
||||||
|
linearElement,
|
||||||
|
edge as "start" | "end",
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
const elementId =
|
const elementId =
|
||||||
edge === "start"
|
edge === "start"
|
||||||
? linearElement.startBinding?.elementId
|
? linearElement.startBinding?.elementId
|
||||||
@ -245,21 +249,7 @@ const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
});
|
||||||
|
|
||||||
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
zoom?: AppState["zoom"],
|
|
||||||
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
|
||||||
["start", "end"].map((edge) =>
|
|
||||||
getOriginalBindingIfStillCloseOfLinearElementEdge(
|
|
||||||
linearElement,
|
|
||||||
edge as "start" | "end",
|
|
||||||
elementsMap,
|
|
||||||
zoom,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const getBindingStrategyForDraggingArrowEndpoints = (
|
const getBindingStrategyForDraggingArrowEndpoints = (
|
||||||
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
@ -275,7 +265,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
||||||
const start = startDragged
|
const start = startDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -285,7 +275,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
: null // If binding is disabled and start is dragged, break all binds
|
: null // If binding is disabled and start is dragged, break all binds
|
||||||
: !isElbowArrow(selectedElement)
|
: !isElbowArrow(selectedElement)
|
||||||
? // We have to update the focus and gap of the binding, so let's rebind
|
? // We have to update the focus and gap of the binding, so let's rebind
|
||||||
getElligibleElementForBindingElement(
|
getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -295,7 +285,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
: "keep";
|
: "keep";
|
||||||
const end = endDragged
|
const end = endDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -305,7 +295,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
: null // If binding is disabled and end is dragged, break all binds
|
: null // If binding is disabled and end is dragged, break all binds
|
||||||
: !isElbowArrow(selectedElement)
|
: !isElbowArrow(selectedElement)
|
||||||
? // We have to update the focus and gap of the binding, so let's rebind
|
? // We have to update the focus and gap of the binding, so let's rebind
|
||||||
getElligibleElementForBindingElement(
|
getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -336,7 +326,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
);
|
);
|
||||||
const start = startIsClose
|
const start = startIsClose
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -347,7 +337,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
: null;
|
: null;
|
||||||
const end = endIsClose
|
const end = endIsClose
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -428,10 +418,47 @@ export const getSuggestedBindingsForArrows = (
|
|||||||
export const maybeBindLinearElement = (
|
export const maybeBindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
pointerCoords: { x: number; y: number },
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
): void => {
|
): void => {
|
||||||
|
const start = tupleToCoors(
|
||||||
|
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
linearElement,
|
||||||
|
0,
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const end = tupleToCoors(
|
||||||
|
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
linearElement,
|
||||||
|
-1,
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const otherHoveredElement = getHoveredElementForBinding(
|
||||||
|
start,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
appState.zoom,
|
||||||
|
isElbowArrow(linearElement),
|
||||||
|
isElbowArrow(linearElement),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hoveredElement = getHoveredElementForBinding(
|
||||||
|
end,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
appState.zoom,
|
||||||
|
isElbowArrow(linearElement),
|
||||||
|
isElbowArrow(linearElement),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Inside the same element there is no binding to the shape
|
||||||
|
if (hoveredElement === otherHoveredElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (appState.startBoundElement != null) {
|
if (appState.startBoundElement != null) {
|
||||||
bindLinearElement(
|
bindLinearElement(
|
||||||
linearElement,
|
linearElement,
|
||||||
@ -441,15 +468,6 @@ export const maybeBindLinearElement = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoveredElement = getHoveredElementForBinding(
|
|
||||||
pointerCoords,
|
|
||||||
elements,
|
|
||||||
elementsMap,
|
|
||||||
appState.zoom,
|
|
||||||
isElbowArrow(linearElement),
|
|
||||||
isElbowArrow(linearElement),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hoveredElement !== null) {
|
if (hoveredElement !== null) {
|
||||||
if (
|
if (
|
||||||
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
|
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
|
||||||
@ -463,26 +481,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 +491,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)) {
|
||||||
@ -517,8 +523,26 @@ export const bindLinearElement = (
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const points = Array.from(linearElement.points);
|
||||||
|
|
||||||
|
if (isArrowElement(linearElement)) {
|
||||||
|
const [x, y] = bindPointToSnapToElementOutline(
|
||||||
|
linearElement,
|
||||||
|
hoveredElement,
|
||||||
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
points[edgePointIndex] = LinearElementEditor.createPointAt(
|
||||||
|
linearElement,
|
||||||
|
elementsMap,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
mutateElement(linearElement, {
|
mutateElement(linearElement, {
|
||||||
|
points,
|
||||||
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -706,33 +730,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 +740,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 +776,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 +804,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 +836,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 +873,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 +882,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 +902,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 +916,54 @@ 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 otherPointIdx =
|
||||||
|
startOrEnd === "start" ? linearElement.points.length - 1 : 0;
|
||||||
|
const otherPoint = pointFrom<GlobalPoint>(
|
||||||
|
linearElement.x + linearElement.points[otherPointIdx][0],
|
||||||
|
linearElement.y + linearElement.points[otherPointIdx][1],
|
||||||
|
);
|
||||||
|
|
||||||
|
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],
|
||||||
@ -1016,6 +1011,14 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
return edgePoint;
|
return edgePoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shape = getElementShape(bindableElement, elementsMap);
|
||||||
|
const pointInShape = isPointInShape(edgePoint, shape);
|
||||||
|
const otherPointInShape = isPointInShape(otherPoint, shape);
|
||||||
|
|
||||||
|
if (pointInShape && otherPointInShape) {
|
||||||
|
return edgePoint;
|
||||||
|
}
|
||||||
|
|
||||||
if (elbowed) {
|
if (elbowed) {
|
||||||
const scalar =
|
const scalar =
|
||||||
pointDistanceSq(edgePoint, center) -
|
pointDistanceSq(edgePoint, center) -
|
||||||
@ -1033,6 +1036,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 +1065,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 +1229,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 +1331,6 @@ const updateBoundPoint = (
|
|||||||
|
|
||||||
let newEdgePoint: GlobalPoint;
|
let newEdgePoint: GlobalPoint;
|
||||||
|
|
||||||
// The linear element was not originally pointing inside the bound shape,
|
|
||||||
// we can point directly at the focus point
|
|
||||||
if (binding.gap === 0) {
|
|
||||||
newEdgePoint = focusPointAbsolute;
|
|
||||||
} else {
|
|
||||||
const edgePointAbsolute =
|
const edgePointAbsolute =
|
||||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
linearElement,
|
linearElement,
|
||||||
@ -1290,15 +1353,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 +1384,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 +1393,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 +1408,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,29 +1430,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const maybeCalculateNewGapWhenScaling = (
|
const getEligibleElementForBindingElement = (
|
||||||
changedElement: ExcalidrawBindableElement,
|
|
||||||
currentBinding: PointBinding | null | undefined,
|
|
||||||
newSize: { width: number; height: number } | undefined,
|
|
||||||
): PointBinding | null | undefined => {
|
|
||||||
if (currentBinding == null || newSize == null) {
|
|
||||||
return currentBinding;
|
|
||||||
}
|
|
||||||
const { width: newWidth, height: newHeight } = newSize;
|
|
||||||
const { width, height } = changedElement;
|
|
||||||
const newGap = Math.max(
|
|
||||||
1,
|
|
||||||
Math.min(
|
|
||||||
maxBindingGap(changedElement, newWidth, newHeight),
|
|
||||||
currentBinding.gap *
|
|
||||||
(newWidth < newHeight ? newWidth / width : newHeight / height),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { ...currentBinding, gap: newGap };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getElligibleElementForBindingElement = (
|
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
tupleToCoors,
|
tupleToCoors,
|
||||||
getSizeFromPoints,
|
getSizeFromPoints,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
|
isTestEnv,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
@ -50,7 +51,6 @@ import { isBindableElement } from "./typeChecks";
|
|||||||
import {
|
import {
|
||||||
type ExcalidrawElbowArrowElement,
|
type ExcalidrawElbowArrowElement,
|
||||||
type NonDeletedSceneElementsMap,
|
type NonDeletedSceneElementsMap,
|
||||||
type SceneElementsMap,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { aabbForElement, pointInsideBounds } from "./shapes";
|
import { aabbForElement, pointInsideBounds } from "./shapes";
|
||||||
@ -63,7 +63,6 @@ import type {
|
|||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
FixedSegment,
|
FixedSegment,
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
||||||
@ -877,8 +876,6 @@ const handleEndpointDrag = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_POS = 1e6;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@ -899,51 +896,7 @@ export const updateElbowArrowPoints = (
|
|||||||
return { points: updates.points ?? arrow.points };
|
return { points: updates.points ?? arrow.points };
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE (mtolmacs): This is a temporary check to ensure that the incoming elbow
|
if (isDevEnv() || isTestEnv()) {
|
||||||
// arrow size is valid. This check will be removed once the issue is identified
|
|
||||||
if (
|
|
||||||
arrow.x < -MAX_POS ||
|
|
||||||
arrow.x > MAX_POS ||
|
|
||||||
arrow.y < -MAX_POS ||
|
|
||||||
arrow.y > MAX_POS ||
|
|
||||||
arrow.x + (updates?.points?.[updates?.points?.length - 1]?.[0] ?? 0) <
|
|
||||||
-MAX_POS ||
|
|
||||||
arrow.x + (updates?.points?.[updates?.points?.length - 1]?.[0] ?? 0) >
|
|
||||||
MAX_POS ||
|
|
||||||
arrow.y + (updates?.points?.[updates?.points?.length - 1]?.[1] ?? 0) <
|
|
||||||
-MAX_POS ||
|
|
||||||
arrow.y + (updates?.points?.[updates?.points?.length - 1]?.[1] ?? 0) >
|
|
||||||
MAX_POS ||
|
|
||||||
arrow.x + (arrow?.points?.[arrow?.points?.length - 1]?.[0] ?? 0) <
|
|
||||||
-MAX_POS ||
|
|
||||||
arrow.x + (arrow?.points?.[arrow?.points?.length - 1]?.[0] ?? 0) >
|
|
||||||
MAX_POS ||
|
|
||||||
arrow.y + (arrow?.points?.[arrow?.points?.length - 1]?.[1] ?? 0) <
|
|
||||||
-MAX_POS ||
|
|
||||||
arrow.y + (arrow?.points?.[arrow?.points?.length - 1]?.[1] ?? 0) > MAX_POS
|
|
||||||
) {
|
|
||||||
console.error(
|
|
||||||
"Elbow arrow (or update) is outside reasonable bounds (> 1e6)",
|
|
||||||
{
|
|
||||||
arrow,
|
|
||||||
updates,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// @ts-ignore See above note
|
|
||||||
arrow.x = clamp(arrow.x, -MAX_POS, MAX_POS);
|
|
||||||
// @ts-ignore See above note
|
|
||||||
arrow.y = clamp(arrow.y, -MAX_POS, MAX_POS);
|
|
||||||
if (updates.points) {
|
|
||||||
updates.points = updates.points.map(([x, y]) =>
|
|
||||||
pointFrom<LocalPoint>(
|
|
||||||
clamp(x, -MAX_POS, MAX_POS),
|
|
||||||
clamp(y, -MAX_POS, MAX_POS),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!import.meta.env.PROD) {
|
|
||||||
invariant(
|
invariant(
|
||||||
!updates.points || updates.points.length >= 2,
|
!updates.points || updates.points.length >= 2,
|
||||||
"Updated point array length must match the arrow point length, contain " +
|
"Updated point array length must match the arrow point length, contain " +
|
||||||
@ -1221,19 +1174,31 @@ const getElbowArrowData = (
|
|||||||
if (options?.isDragging) {
|
if (options?.isDragging) {
|
||||||
const elements = Array.from(elementsMap.values());
|
const elements = Array.from(elementsMap.values());
|
||||||
hoveredStartElement =
|
hoveredStartElement =
|
||||||
getHoveredElement(
|
getHoveredElementForBinding(
|
||||||
origStartGlobalPoint,
|
tupleToCoors(origStartGlobalPoint),
|
||||||
elementsMap,
|
|
||||||
elements,
|
elements,
|
||||||
|
elementsMap,
|
||||||
options?.zoom,
|
options?.zoom,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
) || null;
|
) || null;
|
||||||
hoveredEndElement =
|
hoveredEndElement =
|
||||||
getHoveredElement(
|
getHoveredElementForBinding(
|
||||||
origEndGlobalPoint,
|
tupleToCoors(origEndGlobalPoint),
|
||||||
elementsMap,
|
|
||||||
elements,
|
elements,
|
||||||
|
elementsMap,
|
||||||
options?.zoom,
|
options?.zoom,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
) || null;
|
) || null;
|
||||||
|
|
||||||
|
// Inside the same element there is no binding to the shape
|
||||||
|
if (hoveredStartElement === hoveredEndElement) {
|
||||||
|
hoveredStartElement = null;
|
||||||
|
hoveredEndElement = null;
|
||||||
|
arrow.startBinding = null;
|
||||||
|
arrow.endBinding = null;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
hoveredStartElement = arrow.startBinding
|
hoveredStartElement = arrow.startBinding
|
||||||
? getBindableElementForId(arrow.startBinding.elementId, elementsMap) ||
|
? getBindableElementForId(arrow.startBinding.elementId, elementsMap) ||
|
||||||
@ -1254,6 +1219,7 @@ const getElbowArrowData = (
|
|||||||
"start",
|
"start",
|
||||||
arrow.startBinding?.fixedPoint,
|
arrow.startBinding?.fixedPoint,
|
||||||
origStartGlobalPoint,
|
origStartGlobalPoint,
|
||||||
|
elementsMap,
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
options?.isDragging,
|
options?.isDragging,
|
||||||
);
|
);
|
||||||
@ -1267,20 +1233,19 @@ const getElbowArrowData = (
|
|||||||
"end",
|
"end",
|
||||||
arrow.endBinding?.fixedPoint,
|
arrow.endBinding?.fixedPoint,
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
|
elementsMap,
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
options?.isDragging,
|
options?.isDragging,
|
||||||
);
|
);
|
||||||
const startHeading = getBindPointHeading(
|
const startHeading = getBindPointHeading(
|
||||||
startGlobalPoint,
|
startGlobalPoint,
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
elementsMap,
|
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
origStartGlobalPoint,
|
origStartGlobalPoint,
|
||||||
);
|
);
|
||||||
const endHeading = getBindPointHeading(
|
const endHeading = getBindPointHeading(
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
startGlobalPoint,
|
startGlobalPoint,
|
||||||
elementsMap,
|
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
);
|
);
|
||||||
@ -2110,29 +2075,6 @@ const normalizeArrowElementUpdate = (
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// NOTE (mtolmacs): This is a temporary check to see if the normalization
|
|
||||||
// creates an overly large arrow. This should be removed once we have an answer.
|
|
||||||
if (
|
|
||||||
offsetX < -MAX_POS ||
|
|
||||||
offsetX > MAX_POS ||
|
|
||||||
offsetY < -MAX_POS ||
|
|
||||||
offsetY > MAX_POS ||
|
|
||||||
offsetX + points[points.length - 1][0] < -MAX_POS ||
|
|
||||||
offsetY + points[points.length - 1][0] > MAX_POS ||
|
|
||||||
offsetX + points[points.length - 1][1] < -MAX_POS ||
|
|
||||||
offsetY + points[points.length - 1][1] > MAX_POS
|
|
||||||
) {
|
|
||||||
console.error(
|
|
||||||
"Elbow arrow normalization is outside reasonable bounds (> 1e6)",
|
|
||||||
{
|
|
||||||
x: offsetX,
|
|
||||||
y: offsetY,
|
|
||||||
points,
|
|
||||||
...getSizeFromPoints(points),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
points = points.map(([x, y]) =>
|
points = points.map(([x, y]) =>
|
||||||
pointFrom<LocalPoint>(clamp(x, -1e6, 1e6), clamp(y, -1e6, 1e6)),
|
pointFrom<LocalPoint>(clamp(x, -1e6, 1e6), clamp(y, -1e6, 1e6)),
|
||||||
);
|
);
|
||||||
@ -2212,6 +2154,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 +2164,7 @@ const getGlobalPoint = (
|
|||||||
arrow,
|
arrow,
|
||||||
element,
|
element,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
return snapToMid(element, snapPoint);
|
return snapToMid(element, snapPoint);
|
||||||
@ -2240,7 +2184,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2250,7 +2194,6 @@ const getGlobalPoint = (
|
|||||||
const getBindPointHeading = (
|
const getBindPointHeading = (
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
otherPoint: GlobalPoint,
|
otherPoint: GlobalPoint,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
): Heading =>
|
): Heading =>
|
||||||
@ -2268,26 +2211,9 @@ const getBindPointHeading = (
|
|||||||
number,
|
number,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
elementsMap,
|
|
||||||
origPoint,
|
origPoint,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getHoveredElement = (
|
|
||||||
origPoint: GlobalPoint,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
zoom?: AppState["zoom"],
|
|
||||||
) => {
|
|
||||||
return getHoveredElementForBinding(
|
|
||||||
tupleToCoors(origPoint),
|
|
||||||
elements,
|
|
||||||
elementsMap,
|
|
||||||
zoom,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean =>
|
const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean =>
|
||||||
a[0] === b[0] && a[1] === b[1];
|
a[0] === b[0] && a[1] === b[1];
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
@ -56,6 +57,7 @@ import { headingIsHorizontal, vectorToHeading } from "./heading";
|
|||||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
import { bumpVersion, mutateElement } from "./mutateElement";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import {
|
import {
|
||||||
|
isArrowElement,
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
@ -252,27 +254,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 +285,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,7 +337,7 @@ export class LinearElementEditor {
|
|||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(
|
||||||
element,
|
element,
|
||||||
selectedPointsIndices.map((pointIndex) => {
|
selectedPointsIndices.map((pointIndex) => {
|
||||||
const newPointPosition: LocalPoint =
|
let newPointPosition: LocalPoint =
|
||||||
pointIndex === lastClickedPoint
|
pointIndex === lastClickedPoint
|
||||||
? LinearElementEditor.createPointAt(
|
? LinearElementEditor.createPointAt(
|
||||||
element,
|
element,
|
||||||
@ -347,6 +350,46 @@ export class LinearElementEditor {
|
|||||||
element.points[pointIndex][0] + deltaX,
|
element.points[pointIndex][0] + deltaX,
|
||||||
element.points[pointIndex][1] + deltaY,
|
element.points[pointIndex][1] + deltaY,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (pointIndex === 0 || pointIndex === element.points.length - 1) {
|
||||||
|
const [, , , , cx, cy] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const newGlobalPointPosition = pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + newPointPosition[0],
|
||||||
|
element.y + newPointPosition[1],
|
||||||
|
),
|
||||||
|
pointFrom<GlobalPoint>(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const avoidancePoint = getOutlineAvoidingPoint(
|
||||||
|
element,
|
||||||
|
newGlobalPointPosition,
|
||||||
|
pointIndex,
|
||||||
|
app.scene,
|
||||||
|
app.state.zoom,
|
||||||
|
);
|
||||||
|
|
||||||
|
newPointPosition = LinearElementEditor.createPointAt(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
!isArrowElement(element) ||
|
||||||
|
avoidancePoint[0] === newGlobalPointPosition[0]
|
||||||
|
? newGlobalPointPosition[0] -
|
||||||
|
linearElementEditor.pointerOffset.x
|
||||||
|
: avoidancePoint[0],
|
||||||
|
!isArrowElement(element) ||
|
||||||
|
avoidancePoint[1] === newGlobalPointPosition[1]
|
||||||
|
? newGlobalPointPosition[1] -
|
||||||
|
linearElementEditor.pointerOffset.y
|
||||||
|
: avoidancePoint[1],
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
index: pointIndex,
|
index: pointIndex,
|
||||||
point: newPointPosition,
|
point: newPointPosition,
|
||||||
@ -426,6 +469,7 @@ export class LinearElementEditor {
|
|||||||
editingLinearElement: LinearElementEditor,
|
editingLinearElement: LinearElementEditor,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
shouldBind?: boolean,
|
||||||
): LinearElementEditor {
|
): LinearElementEditor {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
const elements = scene.getNonDeletedElements();
|
||||||
@ -488,6 +532,19 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldBind) {
|
||||||
|
const element = scene.getElement(editingLinearElement.elementId);
|
||||||
|
if (isBindingElement(element) && isBindingEnabled(appState)) {
|
||||||
|
bindOrUnbindLinearElement(
|
||||||
|
element,
|
||||||
|
bindings.startBindingElement || "keep",
|
||||||
|
bindings.endBindingElement || "keep",
|
||||||
|
elementsMap,
|
||||||
|
scene,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...editingLinearElement,
|
...editingLinearElement,
|
||||||
...bindings,
|
...bindings,
|
||||||
|
@ -98,28 +98,6 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||||||
...rest
|
...rest
|
||||||
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
||||||
) => {
|
) => {
|
||||||
// NOTE (mtolmacs): This is a temporary check to detect extremely large
|
|
||||||
// element position or sizing
|
|
||||||
if (
|
|
||||||
x < -1e6 ||
|
|
||||||
x > 1e6 ||
|
|
||||||
y < -1e6 ||
|
|
||||||
y > 1e6 ||
|
|
||||||
width < -1e6 ||
|
|
||||||
width > 1e6 ||
|
|
||||||
height < -1e6 ||
|
|
||||||
height > 1e6
|
|
||||||
) {
|
|
||||||
console.error("New element size or position is too large", {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
// @ts-ignore
|
|
||||||
points: rest.points,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// assign type to guard against excess properties
|
// assign type to guard against excess properties
|
||||||
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
||||||
id: rest.id || randomId(),
|
id: rest.id || randomId(),
|
||||||
|
@ -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);
|
||||||
|
@ -130,7 +130,7 @@ export const isLinearElementType = (
|
|||||||
export const isBindingElement = (
|
export const isBindingElement = (
|
||||||
element?: ExcalidrawElement | null,
|
element?: ExcalidrawElement | null,
|
||||||
includeLocked = true,
|
includeLocked = true,
|
||||||
): element is ExcalidrawLinearElement => {
|
): element is ExcalidrawArrowElement => {
|
||||||
return (
|
return (
|
||||||
element != null &&
|
element != null &&
|
||||||
(!element.locked || includeLocked === true) &&
|
(!element.locked || includeLocked === true) &&
|
||||||
|
@ -10,6 +10,8 @@ import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
|||||||
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
|
import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
|
import "@excalidraw/utils/test-utils";
|
||||||
|
|
||||||
import { getTransformHandles } from "../src/transformHandles";
|
import { getTransformHandles } from "../src/transformHandles";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@ -18,7 +20,9 @@ const mouse = new Pointer("mouse");
|
|||||||
|
|
||||||
describe("element binding", () => {
|
describe("element binding", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
mouse.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create valid binding if duplicate start/end points", async () => {
|
it("should create valid binding if duplicate start/end points", async () => {
|
||||||
@ -89,8 +93,16 @@ describe("element binding", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//@TODO fix the test with rotation
|
// UX RATIONALE: We are not aware of any use-case where the user would want to
|
||||||
it.skip("rotation of arrow should rebind both ends", () => {
|
// have the arrow rebind after rotation but not when the arrow shaft is
|
||||||
|
// dragged so either the start or the end point is in the binding range of a
|
||||||
|
// bindable element. So to remain consistent, we only "rebind" if at the end
|
||||||
|
// of the rotation the original binding would remain the same (i.e. like we
|
||||||
|
// would've evaluated binding only at the end of the operation).
|
||||||
|
it(
|
||||||
|
"rotation of arrow should not rebind on both ends if rotated enough to" +
|
||||||
|
" not be in the binding range of the original elements",
|
||||||
|
() => {
|
||||||
const rectLeft = UI.createElement("rectangle", {
|
const rectLeft = UI.createElement("rectangle", {
|
||||||
x: 0,
|
x: 0,
|
||||||
width: 200,
|
width: 200,
|
||||||
@ -123,12 +135,13 @@ describe("element binding", () => {
|
|||||||
mouse.up();
|
mouse.up();
|
||||||
expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
|
expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
|
||||||
expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
|
expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
|
||||||
expect(arrow.startBinding?.elementId).toBe(rectRight.id);
|
expect(arrow.startBinding).toBe(null);
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectLeft.id);
|
expect(arrow.endBinding).toBe(null);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// TODO fix & reenable once we rewrite tests to work with concurrency
|
// TODO fix & reenable once we rewrite tests to work with concurrency
|
||||||
it.skip(
|
it(
|
||||||
"editing arrow and moving its head to bind it to element A, finalizing the" +
|
"editing arrow and moving its head to bind it to element A, finalizing the" +
|
||||||
"editing by clicking on element A should end up selecting A",
|
"editing by clicking on element A should end up selecting A",
|
||||||
async () => {
|
async () => {
|
||||||
@ -142,7 +155,10 @@ describe("element binding", () => {
|
|||||||
mouse.up(0, 80);
|
mouse.up(0, 80);
|
||||||
|
|
||||||
// Edit arrow with multi-point
|
// Edit arrow with multi-point
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
mouse.doubleClick();
|
mouse.doubleClick();
|
||||||
|
});
|
||||||
|
|
||||||
// move arrow head
|
// move arrow head
|
||||||
mouse.down();
|
mouse.down();
|
||||||
mouse.up(0, 10);
|
mouse.up(0, 10);
|
||||||
@ -152,16 +168,12 @@ describe("element binding", () => {
|
|||||||
// the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
|
// the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
expect(h.state.editingLinearElement).not.toBe(null);
|
expect(h.state.editingLinearElement).not.toBe(null);
|
||||||
mouse.down(0, 0);
|
mouse.click();
|
||||||
await new Promise((r) => setTimeout(r, 100));
|
|
||||||
expect(h.state.editingLinearElement).toBe(null);
|
|
||||||
expect(API.getSelectedElement().type).toBe("rectangle");
|
|
||||||
mouse.up();
|
|
||||||
expect(API.getSelectedElement().type).toBe("rectangle");
|
expect(API.getSelectedElement().type).toBe("rectangle");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("should unbind arrow when moving it with keyboard", () => {
|
it("should not move bound arrows when moving it with keyboard", () => {
|
||||||
const rectangle = UI.createElement("rectangle", {
|
const rectangle = UI.createElement("rectangle", {
|
||||||
x: 75,
|
x: 75,
|
||||||
y: 0,
|
y: 0,
|
||||||
@ -187,13 +199,19 @@ describe("element binding", () => {
|
|||||||
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
||||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
||||||
|
expect(API.getSelectedElement().type).toBe("arrow");
|
||||||
|
|
||||||
// Sever connection
|
// Sever connection
|
||||||
expect(API.getSelectedElement().type).toBe("arrow");
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
// We have to move a significant distance to get out of the binding zone
|
||||||
|
Array.from({ length: 10 }).forEach(() => {
|
||||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||||
expect(arrow.endBinding).toBe(null);
|
});
|
||||||
Keyboard.keyPress(KEYS.ARROW_RIGHT);
|
});
|
||||||
expect(arrow.endBinding).toBe(null);
|
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
||||||
|
expect(arrow.x).toBe(0);
|
||||||
|
expect(arrow.y).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should unbind on bound element deletion", () => {
|
it("should unbind on bound element deletion", () => {
|
||||||
@ -481,4 +499,86 @@ describe("element binding", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// UX RATIONALE: The arrow might be outside of the shape at high zoom and you
|
||||||
|
// won't see what's going on.
|
||||||
|
it(
|
||||||
|
"allow non-binding simple (complex) arrow creation while start and end" +
|
||||||
|
" points are in the same shape",
|
||||||
|
() => {
|
||||||
|
const rect = UI.createElement("rectangle", {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow = UI.createElement("arrow", {
|
||||||
|
x: 5,
|
||||||
|
y: 5,
|
||||||
|
height: 95,
|
||||||
|
width: 95,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(arrow.startBinding).toBe(null);
|
||||||
|
expect(arrow.endBinding).toBe(null);
|
||||||
|
expect(rect.boundElements).toEqual(null);
|
||||||
|
expect(arrow.points).toCloselyEqualPoints([
|
||||||
|
[0, 0],
|
||||||
|
[92.2855, 92.2855],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rect2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 300,
|
||||||
|
y: 300,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
backgroundColor: "red",
|
||||||
|
fillStyle: "solid",
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect2]);
|
||||||
|
|
||||||
|
const arrow2 = UI.createElement("arrow", {
|
||||||
|
x: 305,
|
||||||
|
y: 305,
|
||||||
|
height: 95,
|
||||||
|
width: 95,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(arrow2.startBinding).toBe(null);
|
||||||
|
expect(arrow2.endBinding).toBe(null);
|
||||||
|
expect(rect2.boundElements).toEqual(null);
|
||||||
|
expect(arrow2.points).toCloselyEqualPoints([
|
||||||
|
[0, 0],
|
||||||
|
[92.2855, 92.2855],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rect3 = UI.createElement("rectangle", {
|
||||||
|
x: 0,
|
||||||
|
y: 300,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow3 = UI.createElement("arrow", {
|
||||||
|
x: 10,
|
||||||
|
y: 310,
|
||||||
|
height: 85,
|
||||||
|
width: 84,
|
||||||
|
elbowed: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(arrow3.startBinding).toBe(null);
|
||||||
|
expect(arrow3.endBinding).toBe(null);
|
||||||
|
expect(rect3.boundElements).toEqual(null);
|
||||||
|
expect(arrow3.points).toCloselyEqualPoints([
|
||||||
|
[0, 0],
|
||||||
|
[0, 42.5],
|
||||||
|
[84, 42.5],
|
||||||
|
[84, 85],
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
@ -10,11 +10,14 @@ import {
|
|||||||
|
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
import {
|
||||||
|
actionDuplicateSelection,
|
||||||
|
actionSelectAll,
|
||||||
|
} from "@excalidraw/excalidraw/actions";
|
||||||
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import { Keyboard, Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
act,
|
act,
|
||||||
@ -28,7 +31,10 @@ import type { LocalPoint } from "@excalidraw/math";
|
|||||||
import { mutateElement } from "../src/mutateElement";
|
import { mutateElement } from "../src/mutateElement";
|
||||||
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
||||||
|
|
||||||
import type { ExcalidrawLinearElement } from "../src/types";
|
import type {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
} from "../src/types";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
@ -408,6 +414,122 @@ describe("duplicating multiple elements", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("elbow arrow duplication", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps arrow shape when the whole set of arrow and bindables are duplicated", async () => {
|
||||||
|
UI.createElement("rectangle", {
|
||||||
|
x: -150,
|
||||||
|
y: -150,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
UI.createElement("rectangle", {
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(-43, -99);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(43, 99);
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
const arrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawArrowElement;
|
||||||
|
const originalArrowId = arrow.id;
|
||||||
|
|
||||||
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.app.actionManager.executeAction(actionSelectAll);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(h.elements.length).toEqual(6);
|
||||||
|
|
||||||
|
const duplicatedArrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[2] as ExcalidrawArrowElement;
|
||||||
|
|
||||||
|
expect(duplicatedArrow.id).not.toBe(originalArrowId);
|
||||||
|
expect(duplicatedArrow.type).toBe("arrow");
|
||||||
|
expect(duplicatedArrow.elbowed).toBe(true);
|
||||||
|
expect(duplicatedArrow.points).toEqual([
|
||||||
|
[0, 0],
|
||||||
|
[45, 0],
|
||||||
|
[45, 200],
|
||||||
|
[90, 200],
|
||||||
|
]);
|
||||||
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes arrow shape to unbind variant if only the connected elbow arrow is duplicated", async () => {
|
||||||
|
UI.createElement("rectangle", {
|
||||||
|
x: -150,
|
||||||
|
y: -150,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
UI.createElement("rectangle", {
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(-43, -99);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(43, 99);
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
const arrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawArrowElement;
|
||||||
|
const originalArrowId = arrow.id;
|
||||||
|
|
||||||
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(h.elements.length).toEqual(4);
|
||||||
|
|
||||||
|
const duplicatedArrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawArrowElement;
|
||||||
|
|
||||||
|
expect(duplicatedArrow.id).not.toBe(originalArrowId);
|
||||||
|
expect(duplicatedArrow.type).toBe("arrow");
|
||||||
|
expect(duplicatedArrow.elbowed).toBe(true);
|
||||||
|
expect(duplicatedArrow.points).toEqual([
|
||||||
|
[0, 0],
|
||||||
|
[0, 100],
|
||||||
|
[90, 100],
|
||||||
|
[90, 200],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("duplication z-order", () => {
|
describe("duplication z-order", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<Excalidraw />);
|
await render(<Excalidraw />);
|
||||||
|
@ -3,14 +3,11 @@ import { pointFrom } from "@excalidraw/math";
|
|||||||
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||||
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
|
||||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
|
||||||
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
act,
|
|
||||||
fireEvent,
|
fireEvent,
|
||||||
GlobalTestState,
|
GlobalTestState,
|
||||||
queryByTestId,
|
queryByTestId,
|
||||||
@ -301,114 +298,4 @@ describe("elbow arrow ui", () => {
|
|||||||
[103, 165],
|
[103, 165],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps arrow shape when the whole set of arrow and bindables are duplicated", async () => {
|
|
||||||
UI.createElement("rectangle", {
|
|
||||||
x: -150,
|
|
||||||
y: -150,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
UI.createElement("rectangle", {
|
|
||||||
x: 50,
|
|
||||||
y: 50,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
UI.clickTool("arrow");
|
|
||||||
UI.clickOnTestId("elbow-arrow");
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
mouse.moveTo(-43, -99);
|
|
||||||
mouse.click();
|
|
||||||
mouse.moveTo(43, 99);
|
|
||||||
mouse.click();
|
|
||||||
|
|
||||||
const arrow = h.scene.getSelectedElements(
|
|
||||||
h.state,
|
|
||||||
)[0] as ExcalidrawArrowElement;
|
|
||||||
const originalArrowId = arrow.id;
|
|
||||||
|
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
h.app.actionManager.executeAction(actionSelectAll);
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(6);
|
|
||||||
|
|
||||||
const duplicatedArrow = h.scene.getSelectedElements(
|
|
||||||
h.state,
|
|
||||||
)[2] as ExcalidrawArrowElement;
|
|
||||||
|
|
||||||
expect(duplicatedArrow.id).not.toBe(originalArrowId);
|
|
||||||
expect(duplicatedArrow.type).toBe("arrow");
|
|
||||||
expect(duplicatedArrow.elbowed).toBe(true);
|
|
||||||
expect(duplicatedArrow.points).toEqual([
|
|
||||||
[0, 0],
|
|
||||||
[45, 0],
|
|
||||||
[45, 200],
|
|
||||||
[90, 200],
|
|
||||||
]);
|
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("changes arrow shape to unbind variant if only the connected elbow arrow is duplicated", async () => {
|
|
||||||
UI.createElement("rectangle", {
|
|
||||||
x: -150,
|
|
||||||
y: -150,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
UI.createElement("rectangle", {
|
|
||||||
x: 50,
|
|
||||||
y: 50,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
UI.clickTool("arrow");
|
|
||||||
UI.clickOnTestId("elbow-arrow");
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
mouse.moveTo(-43, -99);
|
|
||||||
mouse.click();
|
|
||||||
mouse.moveTo(43, 99);
|
|
||||||
mouse.click();
|
|
||||||
|
|
||||||
const arrow = h.scene.getSelectedElements(
|
|
||||||
h.state,
|
|
||||||
)[0] as ExcalidrawArrowElement;
|
|
||||||
const originalArrowId = arrow.id;
|
|
||||||
|
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(4);
|
|
||||||
|
|
||||||
const duplicatedArrow = h.scene.getSelectedElements(
|
|
||||||
h.state,
|
|
||||||
)[0] as ExcalidrawArrowElement;
|
|
||||||
|
|
||||||
expect(duplicatedArrow.id).not.toBe(originalArrowId);
|
|
||||||
expect(duplicatedArrow.type).toBe("arrow");
|
|
||||||
expect(duplicatedArrow.elbowed).toBe(true);
|
|
||||||
expect(duplicatedArrow.points).toEqual([
|
|
||||||
[0, 0],
|
|
||||||
[0, 100],
|
|
||||||
[90, 100],
|
|
||||||
[90, 200],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -15,6 +15,8 @@ import {
|
|||||||
unmountComponent,
|
unmountComponent,
|
||||||
} from "@excalidraw/excalidraw/tests/test-utils";
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
|
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element/binding";
|
||||||
|
|
||||||
import type { LocalPoint } from "@excalidraw/math";
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { isLinearElement } from "../src/typeChecks";
|
import { isLinearElement } from "../src/typeChecks";
|
||||||
@ -195,7 +197,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 +828,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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1003,14 +1006,14 @@ describe("multiple selection", () => {
|
|||||||
size: 100,
|
size: 100,
|
||||||
});
|
});
|
||||||
const leftBoundArrow = UI.createElement("arrow", {
|
const leftBoundArrow = UI.createElement("arrow", {
|
||||||
x: -110,
|
x: -100 - FIXED_BINDING_DISTANCE,
|
||||||
y: 50,
|
y: 50,
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 0,
|
height: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rightBoundArrow = UI.createElement("arrow", {
|
const rightBoundArrow = UI.createElement("arrow", {
|
||||||
x: 210,
|
x: 200 + FIXED_BINDING_DISTANCE,
|
||||||
y: 50,
|
y: 50,
|
||||||
width: -100,
|
width: -100,
|
||||||
height: 0,
|
height: 0,
|
||||||
@ -1031,27 +1034,29 @@ describe("multiple selection", () => {
|
|||||||
shift: true,
|
shift: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
expect(leftBoundArrow.x).toBeCloseTo(-100 - FIXED_BINDING_DISTANCE);
|
||||||
expect(leftBoundArrow.y).toBeCloseTo(50);
|
expect(leftBoundArrow.y).toBeCloseTo(50);
|
||||||
expect(leftBoundArrow.width).toBeCloseTo(143, 0);
|
expect(leftBoundArrow.width).toBeCloseTo(146 - FIXED_BINDING_DISTANCE, 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,
|
||||||
);
|
);
|
||||||
expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
||||||
|
|
||||||
expect(rightBoundArrow.x).toBeCloseTo(210);
|
expect(rightBoundArrow.x).toBeCloseTo(210 - FIXED_BINDING_DISTANCE);
|
||||||
expect(rightBoundArrow.y).toBeCloseTo(
|
expect(rightBoundArrow.y).toBeCloseTo(
|
||||||
(selectionHeight - 50) * (1 - scale) + 50,
|
(selectionHeight - 50) * (1 - scale) + 50,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
expect(rightBoundArrow.width).toBeCloseTo(100 * scale);
|
//console.log(JSON.stringify(h.elements));
|
||||||
|
expect(rightBoundArrow.width).toBeCloseTo(100 * scale, 0);
|
||||||
expect(rightBoundArrow.height).toBeCloseTo(0);
|
expect(rightBoundArrow.height).toBeCloseTo(0);
|
||||||
expect(rightBoundArrow.angle).toEqual(0);
|
expect(rightBoundArrow.angle).toEqual(0);
|
||||||
expect(rightBoundArrow.startBinding).toBeNull();
|
expect(rightBoundArrow.startBinding).toBeNull();
|
||||||
expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
|
expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(FIXED_BINDING_DISTANCE);
|
||||||
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
||||||
rightArrowBinding.elementId,
|
rightArrowBinding.elementId,
|
||||||
);
|
);
|
||||||
@ -1338,8 +1343,8 @@ describe("multiple selection", () => {
|
|||||||
|
|
||||||
expect(boundArrow.x).toBeCloseTo(380 * scaleX);
|
expect(boundArrow.x).toBeCloseTo(380 * scaleX);
|
||||||
expect(boundArrow.y).toBeCloseTo(240 * scaleY);
|
expect(boundArrow.y).toBeCloseTo(240 * scaleY);
|
||||||
expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX);
|
expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX - 2, 0);
|
||||||
expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY);
|
expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY + 2, 0);
|
||||||
|
|
||||||
expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo(
|
expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo(
|
||||||
boundArrow.x + boundArrow.points[1][0] / 2,
|
boundArrow.x + boundArrow.points[1][0] / 2,
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import { pointFrom } from "@excalidraw/math";
|
import { type GlobalPoint, pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
maybeBindLinearElement,
|
maybeBindLinearElement,
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
|
getHoveredElementForBinding,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
|
isElbowArrow,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import { KEYS, arrayToMap, updateActiveTool } from "@excalidraw/common";
|
import { KEYS, updateActiveTool } from "@excalidraw/common";
|
||||||
import { isPathALoop } from "@excalidraw/element/shapes";
|
import { isPathALoop } from "@excalidraw/element/shapes";
|
||||||
|
|
||||||
import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
|
import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
|
||||||
@ -91,10 +93,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),
|
||||||
@ -135,15 +153,9 @@ export const actionFinalize = register({
|
|||||||
!isLoop &&
|
!isLoop &&
|
||||||
multiPointElement.points.length > 1
|
multiPointElement.points.length > 1
|
||||||
) {
|
) {
|
||||||
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
||||||
multiPointElement,
|
|
||||||
-1,
|
|
||||||
arrayToMap(elements),
|
|
||||||
);
|
|
||||||
maybeBindLinearElement(
|
maybeBindLinearElement(
|
||||||
multiPointElement,
|
multiPointElement,
|
||||||
appState,
|
appState,
|
||||||
{ x, y },
|
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
);
|
);
|
||||||
|
@ -73,12 +73,12 @@ describe("flipping re-centers selection", () => {
|
|||||||
API.executeAction(actionFlipHorizontal);
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
|
||||||
const rec1 = h.elements.find((el) => el.id === "rec1")!;
|
const rec1 = h.elements.find((el) => el.id === "rec1")!;
|
||||||
expect(rec1.x).toBeCloseTo(100, 0);
|
expect(Math.floor(rec1.x)).toBeCloseTo(100, 0);
|
||||||
expect(rec1.y).toBeCloseTo(100, 0);
|
expect(Math.floor(rec1.y)).toBeCloseTo(100, 0);
|
||||||
|
|
||||||
const rec2 = h.elements.find((el) => el.id === "rec2")!;
|
const rec2 = h.elements.find((el) => el.id === "rec2")!;
|
||||||
expect(rec2.x).toBeCloseTo(220, 0);
|
expect(Math.floor(rec2.x)).toBeCloseTo(220, 0);
|
||||||
expect(rec2.y).toBeCloseTo(250, 0);
|
expect(Math.floor(rec2.y)).toBeCloseTo(250, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -87,6 +87,16 @@ describe("flipping arrowheads", () => {
|
|||||||
await render(<Excalidraw />);
|
await render(<Excalidraw />);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// UX RATIONALE: If we flip bound arrows by the center axes then there could
|
||||||
|
// be a case where the bindable objects are offset and the arrow would lay
|
||||||
|
// outside both bindable objects binding range, yet remain bound to then,
|
||||||
|
// resulting in a jump on movement.
|
||||||
|
//
|
||||||
|
// We are aware that 2+ point simple arrows behave incorrectly when flipped
|
||||||
|
// this way but it was decided that there is no known use case for this so
|
||||||
|
// left as it is.
|
||||||
|
//
|
||||||
|
// Demo: https://excalidraw.com/#json=isE-S8LqNlD1u-LsS8Ezz,iZZ09PPasp6OWbGtJwOUGQ
|
||||||
it("flipping bound arrow should flip arrowheads only", () => {
|
it("flipping bound arrow should flip arrowheads only", () => {
|
||||||
const rect = API.createElement({
|
const rect = API.createElement({
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
@ -123,6 +133,7 @@ describe("flipping arrowheads", () => {
|
|||||||
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// UX RATIONALE: See above for the reasoning.
|
||||||
it("flipping bound arrow should flip arrowheads only 2", () => {
|
it("flipping bound arrow should flip arrowheads only 2", () => {
|
||||||
const rect = API.createElement({
|
const rect = API.createElement({
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
@ -164,7 +175,9 @@ describe("flipping arrowheads", () => {
|
|||||||
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("flipping unbound arrow shouldn't flip arrowheads", () => {
|
// UX RATIONALE: Unbound arrows are not constrained by other elements and
|
||||||
|
// should behave like any other element when flipped for consisency.
|
||||||
|
it("flipping unbound arrow should mirror on horizontal or vertical axis", () => {
|
||||||
const arrow = API.createElement({
|
const arrow = API.createElement({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
id: "arrow1",
|
id: "arrow1",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
import {
|
import {
|
||||||
bindOrUnbindLinearElements,
|
bindOrUnbindLinearElement,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
|
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
|
||||||
@ -12,15 +12,15 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
|||||||
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
|
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isElbowArrow,
|
isBindableElement,
|
||||||
isLinearElement,
|
isBindingElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/frame";
|
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/frame";
|
||||||
import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
|
import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
@ -160,52 +160,54 @@ const flipElements = (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
bindOrUnbindLinearElements(
|
const selectedBindables = selectedElements.filter(
|
||||||
selectedElements.filter(isLinearElement),
|
(e): e is ExcalidrawBindableElement => isBindableElement(e),
|
||||||
elementsMap,
|
|
||||||
app.scene.getNonDeletedElements(),
|
|
||||||
app.scene,
|
|
||||||
isBindingEnabled(appState),
|
|
||||||
[],
|
|
||||||
appState.zoom,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// flipping arrow elements (and potentially other) makes the selection group
|
|
||||||
// "move" across the canvas because of how arrows can bump against the "wall"
|
|
||||||
// of the selection, so we need to center the group back to the original
|
|
||||||
// position so that repeated flips don't accumulate the offset
|
|
||||||
|
|
||||||
const { elbowArrows, otherElements } = selectedElements.reduce(
|
|
||||||
(
|
|
||||||
acc: {
|
|
||||||
elbowArrows: ExcalidrawElbowArrowElement[];
|
|
||||||
otherElements: ExcalidrawElement[];
|
|
||||||
},
|
|
||||||
element,
|
|
||||||
) =>
|
|
||||||
isElbowArrow(element)
|
|
||||||
? { ...acc, elbowArrows: acc.elbowArrows.concat(element) }
|
|
||||||
: { ...acc, otherElements: acc.otherElements.concat(element) },
|
|
||||||
{ elbowArrows: [], otherElements: [] },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { midX: newMidX, midY: newMidY } =
|
const { midX: newMidX, midY: newMidY } =
|
||||||
getCommonBoundingBox(selectedElements);
|
getCommonBoundingBox(selectedElements);
|
||||||
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
||||||
otherElements.forEach((element) =>
|
|
||||||
|
selectedElements.forEach((element) => {
|
||||||
|
fixBindings(element, selectedBindables, app, elementsMap);
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
x: element.x + diffX,
|
x: element.x + diffX,
|
||||||
y: element.y + diffY,
|
y: element.y + diffY,
|
||||||
}),
|
});
|
||||||
);
|
});
|
||||||
elbowArrows.forEach((element) =>
|
|
||||||
mutateElement(element, {
|
|
||||||
x: element.x + diffX,
|
|
||||||
y: element.y + diffY,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
return selectedElements;
|
return selectedElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// BEHAVIOR: If you flip a binding element along with its bound elements,
|
||||||
|
// the binding should be preserved. If your selected elements doesn't contain
|
||||||
|
// the bound element(s), then remove the binding. Also do not "magically"
|
||||||
|
// re-bind a binable just because the arrow endpoint is flipped into the
|
||||||
|
// binding range. Rationale being the consistency with the fact that arrows
|
||||||
|
// don't bind when the arrow is moved into the binding range by its shaft.
|
||||||
|
const fixBindings = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
selectedBindables: ExcalidrawBindableElement[],
|
||||||
|
app: AppClassProperties,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
) => {
|
||||||
|
if (isBindingElement(element)) {
|
||||||
|
let start = null;
|
||||||
|
let end = null;
|
||||||
|
|
||||||
|
if (isBindingEnabled(app.state)) {
|
||||||
|
start = element.startBinding
|
||||||
|
? selectedBindables.find(
|
||||||
|
(e) => element.startBinding!.elementId === e.id,
|
||||||
|
) ?? null
|
||||||
|
: null;
|
||||||
|
end = element.endBinding
|
||||||
|
? selectedBindables.find(
|
||||||
|
(e) => element.endBinding!.elementId === e.id,
|
||||||
|
) ?? null
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindOrUnbindLinearElement(element, start, end, elementsMap, app.scene);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,11 +113,11 @@ import {
|
|||||||
fixBindingsAfterDeletion,
|
fixBindingsAfterDeletion,
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
isLinearElementSimpleAndAlreadyBound,
|
|
||||||
maybeBindLinearElement,
|
maybeBindLinearElement,
|
||||||
shouldEnableBindingForPointerEvent,
|
shouldEnableBindingForPointerEvent,
|
||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
getSuggestedBindingsForArrows,
|
getSuggestedBindingsForArrows,
|
||||||
|
getOutlineAvoidingPoint,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
@ -170,7 +170,6 @@ import {
|
|||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getLockedLinearCursorAlignSize,
|
|
||||||
getNormalizedDimensions,
|
getNormalizedDimensions,
|
||||||
isElementCompletelyInViewport,
|
isElementCompletelyInViewport,
|
||||||
isElementInViewport,
|
isElementInViewport,
|
||||||
@ -302,10 +301,9 @@ 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,
|
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
@ -329,7 +327,7 @@ import type {
|
|||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
actionAddToLibrary,
|
actionAddToLibrary,
|
||||||
@ -464,6 +462,14 @@ import { isMaybeMermaidDefinition } from "../mermaid";
|
|||||||
|
|
||||||
import { LassoTrail } from "../lasso";
|
import { LassoTrail } from "../lasso";
|
||||||
|
|
||||||
|
import {
|
||||||
|
handleCanvasPointerMoveForLinearElement,
|
||||||
|
handleDoubleClickForLinearElement,
|
||||||
|
maybeSuggestBindingsForLinearElementAtCoords,
|
||||||
|
onPointerMoveFromPointerDownOnLinearElement,
|
||||||
|
onPointerUpFromPointerDownOnLinearElementHandler,
|
||||||
|
} from "../linear";
|
||||||
|
|
||||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
||||||
@ -2768,7 +2774,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.updateEmbeddables();
|
this.updateEmbeddables();
|
||||||
const elements = this.scene.getElementsIncludingDeleted();
|
const elements = this.scene.getElementsIncludingDeleted();
|
||||||
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
||||||
const nonDeletedElementsMap = this.scene.getNonDeletedElementsMap();
|
|
||||||
|
|
||||||
if (!this.state.showWelcomeScreen && !elements.length) {
|
if (!this.state.showWelcomeScreen && !elements.length) {
|
||||||
this.setState({ showWelcomeScreen: true });
|
this.setState({ showWelcomeScreen: true });
|
||||||
@ -2925,13 +2930,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
maybeBindLinearElement(
|
maybeBindLinearElement(
|
||||||
multiElement,
|
multiElement,
|
||||||
this.state,
|
this.state,
|
||||||
tupleToCoors(
|
|
||||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
||||||
multiElement,
|
|
||||||
-1,
|
|
||||||
nonDeletedElementsMap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
);
|
);
|
||||||
@ -4381,7 +4379,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const arrowIdsToRemove = new Set<string>();
|
const arrowIdsToRemove = new Set<string>();
|
||||||
|
|
||||||
selectedElements
|
selectedElements
|
||||||
.filter(isElbowArrow)
|
.filter(isArrowElement)
|
||||||
.filter((arrow) => {
|
.filter((arrow) => {
|
||||||
const startElementNotInSelection =
|
const startElementNotInSelection =
|
||||||
arrow.startBinding &&
|
arrow.startBinding &&
|
||||||
@ -5453,77 +5451,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||||
if (
|
if (
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
handleDoubleClickForLinearElement(
|
||||||
(!this.state.editingLinearElement ||
|
this,
|
||||||
this.state.editingLinearElement.elementId !==
|
this.store,
|
||||||
selectedElements[0].id) &&
|
selectedElements[0],
|
||||||
!isElbowArrow(selectedElements[0])
|
event,
|
||||||
) {
|
sceneX,
|
||||||
this.store.shouldCaptureIncrement();
|
sceneY,
|
||||||
this.setState({
|
|
||||||
editingLinearElement: new LinearElementEditor(selectedElements[0]),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else if (
|
|
||||||
this.state.selectedLinearElement &&
|
|
||||||
isElbowArrow(selectedElements[0])
|
|
||||||
) {
|
|
||||||
const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
|
||||||
this.state.selectedLinearElement,
|
|
||||||
{ x: sceneX, y: sceneY },
|
|
||||||
this.state,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
const midPoint = hitCoords
|
|
||||||
? LinearElementEditor.getSegmentMidPointIndex(
|
|
||||||
this.state.selectedLinearElement,
|
|
||||||
this.state,
|
|
||||||
hitCoords,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
)
|
)
|
||||||
: -1;
|
) {
|
||||||
|
|
||||||
if (midPoint && midPoint > -1) {
|
|
||||||
this.store.shouldCaptureIncrement();
|
|
||||||
LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
|
|
||||||
|
|
||||||
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
|
||||||
{
|
|
||||||
...this.state.selectedLinearElement,
|
|
||||||
segmentMidPointHoveredCoords: null,
|
|
||||||
},
|
|
||||||
{ x: sceneX, y: sceneY },
|
|
||||||
this.state,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
const nextIndex = nextCoords
|
|
||||||
? LinearElementEditor.getSegmentMidPointIndex(
|
|
||||||
this.state.selectedLinearElement,
|
|
||||||
this.state,
|
|
||||||
nextCoords,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
selectedLinearElement: {
|
|
||||||
...this.state.selectedLinearElement,
|
|
||||||
pointerDownState: {
|
|
||||||
...this.state.selectedLinearElement.pointerDownState,
|
|
||||||
segmentMidpoint: {
|
|
||||||
index: nextIndex,
|
|
||||||
value: hitCoords,
|
|
||||||
added: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
segmentMidPointHoveredCoords: nextCoords,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
|
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
|
||||||
this.startImageCropping(selectedElements[0]);
|
this.startImageCropping(selectedElements[0]);
|
||||||
@ -5902,9 +5841,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// and point
|
// and point
|
||||||
const { newElement } = this.state;
|
const { newElement } = this.state;
|
||||||
if (isBindingElement(newElement, false)) {
|
if (isBindingElement(newElement, false)) {
|
||||||
this.maybeSuggestBindingsForLinearElementAtCoords(
|
maybeSuggestBindingsForLinearElementAtCoords(
|
||||||
newElement,
|
newElement,
|
||||||
[scenePointer],
|
[scenePointer],
|
||||||
|
this,
|
||||||
this.state.startBoundElement,
|
this.state.startBoundElement,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -5914,107 +5854,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (this.state.multiElement) {
|
if (this.state.multiElement) {
|
||||||
const { multiElement } = this.state;
|
const { multiElement } = this.state;
|
||||||
const { x: rx, y: ry } = multiElement;
|
handleCanvasPointerMoveForLinearElement(
|
||||||
|
|
||||||
const { points, lastCommittedPoint } = multiElement;
|
|
||||||
const lastPoint = points[points.length - 1];
|
|
||||||
|
|
||||||
setCursorForShape(this.interactiveCanvas, this.state);
|
|
||||||
|
|
||||||
if (lastPoint === lastCommittedPoint) {
|
|
||||||
// if we haven't yet created a temp point and we're beyond commit-zone
|
|
||||||
// threshold, add a point
|
|
||||||
if (
|
|
||||||
pointDistance(
|
|
||||||
pointFrom(scenePointerX - rx, scenePointerY - ry),
|
|
||||||
lastPoint,
|
|
||||||
) >= LINE_CONFIRM_THRESHOLD
|
|
||||||
) {
|
|
||||||
mutateElement(
|
|
||||||
multiElement,
|
multiElement,
|
||||||
{
|
this,
|
||||||
points: [
|
|
||||||
...points,
|
|
||||||
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
|
||||||
// in this branch, we're inside the commit zone, and no uncommitted
|
|
||||||
// point exists. Thus do nothing (don't add/remove points).
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
points.length > 2 &&
|
|
||||||
lastCommittedPoint &&
|
|
||||||
pointDistance(
|
|
||||||
pointFrom(scenePointerX - rx, scenePointerY - ry),
|
|
||||||
lastCommittedPoint,
|
|
||||||
) < LINE_CONFIRM_THRESHOLD
|
|
||||||
) {
|
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
|
||||||
mutateElement(
|
|
||||||
multiElement,
|
|
||||||
{
|
|
||||||
points: points.slice(0, -1),
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const [gridX, gridY] = getGridPoint(
|
|
||||||
scenePointerX,
|
scenePointerX,
|
||||||
scenePointerY,
|
scenePointerY,
|
||||||
event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement)
|
event,
|
||||||
? null
|
this.triggerRender,
|
||||||
: this.getEffectiveGridSize(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [lastCommittedX, lastCommittedY] =
|
|
||||||
multiElement?.lastCommittedPoint ?? [0, 0];
|
|
||||||
|
|
||||||
let dxFromLastCommitted = gridX - rx - lastCommittedX;
|
|
||||||
let dyFromLastCommitted = gridY - ry - lastCommittedY;
|
|
||||||
|
|
||||||
if (shouldRotateWithDiscreteAngle(event)) {
|
|
||||||
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
|
|
||||||
getLockedLinearCursorAlignSize(
|
|
||||||
// actual coordinate of the last committed point
|
|
||||||
lastCommittedX + rx,
|
|
||||||
lastCommittedY + ry,
|
|
||||||
// cursor-grid coordinate
|
|
||||||
gridX,
|
|
||||||
gridY,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPathALoop(points, this.state.zoom.value)) {
|
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
|
||||||
}
|
|
||||||
// update last uncommitted point
|
|
||||||
mutateElement(
|
|
||||||
multiElement,
|
|
||||||
{
|
|
||||||
points: [
|
|
||||||
...points.slice(0, -1),
|
|
||||||
pointFrom<LocalPoint>(
|
|
||||||
lastCommittedX + dxFromLastCommitted,
|
|
||||||
lastCommittedY + dyFromLastCommitted,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
{
|
|
||||||
isDragging: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// in this path, we're mutating multiElement to reflect
|
|
||||||
// how it will be after adding pointer position as the next point
|
|
||||||
// trigger update here so that new element canvas renders again to reflect this
|
|
||||||
this.triggerRender(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7751,10 +7599,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 +7626,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 +7670,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 +7696,24 @@ 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 [x, y] = getOutlineAvoidingPoint(
|
||||||
: newLinearElement({
|
arrow,
|
||||||
|
pointFrom<GlobalPoint>(gridX, gridY),
|
||||||
|
0,
|
||||||
|
this.scene,
|
||||||
|
this.state.zoom,
|
||||||
|
);
|
||||||
|
|
||||||
|
element = {
|
||||||
|
...arrow,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
element = newLinearElement({
|
||||||
type: elementType,
|
type: elementType,
|
||||||
x: gridX,
|
x: gridX,
|
||||||
y: gridY,
|
y: gridY,
|
||||||
@ -7853,6 +7731,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 +8046,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
|
||||||
@ -8267,13 +8140,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerCoords.x,
|
pointerCoords.x,
|
||||||
pointerCoords.y,
|
pointerCoords.y,
|
||||||
(element, pointsSceneCoords) => {
|
(element, pointsSceneCoords) => {
|
||||||
this.maybeSuggestBindingsForLinearElementAtCoords(
|
maybeSuggestBindingsForLinearElementAtCoords(
|
||||||
element,
|
element,
|
||||||
pointsSceneCoords,
|
pointsSceneCoords,
|
||||||
|
this,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
linearElementEditor,
|
linearElementEditor,
|
||||||
this.scene,
|
|
||||||
);
|
);
|
||||||
if (newLinearElementEditor) {
|
if (newLinearElementEditor) {
|
||||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
@ -8658,54 +8531,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (isLinearElement(newElement)) {
|
} else if (isLinearElement(newElement)) {
|
||||||
pointerDownState.drag.hasOccurred = true;
|
onPointerMoveFromPointerDownOnLinearElement(
|
||||||
const points = newElement.points;
|
|
||||||
let dx = gridX - newElement.x;
|
|
||||||
let dy = gridY - newElement.y;
|
|
||||||
|
|
||||||
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
|
|
||||||
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
|
|
||||||
newElement.x,
|
|
||||||
newElement.y,
|
|
||||||
pointerCoords.x,
|
|
||||||
pointerCoords.y,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (points.length === 1) {
|
|
||||||
mutateElement(
|
|
||||||
newElement,
|
newElement,
|
||||||
{
|
this,
|
||||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
pointerDownState,
|
||||||
},
|
pointerCoords,
|
||||||
false,
|
event,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
} else if (
|
|
||||||
points.length === 2 ||
|
|
||||||
(points.length > 1 && isElbowArrow(newElement))
|
|
||||||
) {
|
|
||||||
mutateElement(
|
|
||||||
newElement,
|
|
||||||
{
|
|
||||||
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
{ isDragging: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
newElement,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isBindingElement(newElement, false)) {
|
|
||||||
// When creating a linear element by dragging
|
|
||||||
this.maybeSuggestBindingsForLinearElementAtCoords(
|
|
||||||
newElement,
|
|
||||||
[pointerCoords],
|
|
||||||
this.state.startBoundElement,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||||
@ -8956,21 +8789,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.selectedLinearElement,
|
this.state.selectedLinearElement,
|
||||||
this.state,
|
this.state,
|
||||||
this.scene,
|
this.scene,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { startBindingElement, endBindingElement } =
|
|
||||||
linearElementEditor;
|
|
||||||
const element = this.scene.getElement(linearElementEditor.elementId);
|
|
||||||
if (isBindingElement(element)) {
|
|
||||||
bindOrUnbindLinearElement(
|
|
||||||
element,
|
|
||||||
startBindingElement,
|
|
||||||
endBindingElement,
|
|
||||||
elementsMap,
|
|
||||||
this.scene,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linearElementEditor !== this.state.selectedLinearElement) {
|
if (linearElementEditor !== this.state.selectedLinearElement) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedLinearElement: {
|
selectedLinearElement: {
|
||||||
@ -9071,66 +8892,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isLinearElement(newElement)) {
|
if (isLinearElement(newElement)) {
|
||||||
if (newElement!.points.length > 1) {
|
onPointerUpFromPointerDownOnLinearElementHandler(
|
||||||
this.store.shouldCaptureIncrement();
|
newElement,
|
||||||
}
|
multiElement,
|
||||||
const pointerCoords = viewportCoordsToSceneCoords(
|
this,
|
||||||
|
this.store,
|
||||||
|
pointerDownState,
|
||||||
childEvent,
|
childEvent,
|
||||||
this.state,
|
activeTool,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
|
|
||||||
mutateElement(newElement, {
|
|
||||||
points: [
|
|
||||||
...newElement.points,
|
|
||||||
pointFrom<LocalPoint>(
|
|
||||||
pointerCoords.x - newElement.x,
|
|
||||||
pointerCoords.y - newElement.y,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
this.setState({
|
|
||||||
multiElement: newElement,
|
|
||||||
newElement,
|
|
||||||
});
|
|
||||||
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
|
|
||||||
if (
|
|
||||||
isBindingEnabled(this.state) &&
|
|
||||||
isBindingElement(newElement, false)
|
|
||||||
) {
|
|
||||||
maybeBindLinearElement(
|
|
||||||
newElement,
|
|
||||||
this.state,
|
|
||||||
pointerCoords,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
|
||||||
if (!activeTool.locked) {
|
|
||||||
resetCursor(this.interactiveCanvas);
|
|
||||||
this.setState((prevState) => ({
|
|
||||||
newElement: null,
|
|
||||||
activeTool: updateActiveTool(this.state, {
|
|
||||||
type: "selection",
|
|
||||||
}),
|
|
||||||
selectedElementIds: makeNextSelectedElementIds(
|
|
||||||
{
|
|
||||||
...prevState.selectedElementIds,
|
|
||||||
[newElement.id]: true,
|
|
||||||
},
|
|
||||||
prevState,
|
|
||||||
),
|
|
||||||
selectedLinearElement: new LinearElementEditor(newElement),
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
this.setState((prevState) => ({
|
|
||||||
newElement: null,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
// so that the scene gets rendered again to display the newly drawn linear as well
|
|
||||||
this.scene.triggerUpdate();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10196,49 +9966,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private maybeSuggestBindingsForLinearElementAtCoords = (
|
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
/** scene coords */
|
|
||||||
pointerCoords: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}[],
|
|
||||||
// During line creation the start binding hasn't been written yet
|
|
||||||
// into `linearElement`
|
|
||||||
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
|
|
||||||
): void => {
|
|
||||||
if (!pointerCoords.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const suggestedBindings = pointerCoords.reduce(
|
|
||||||
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
|
|
||||||
const hoveredBindableElement = getHoveredElementForBinding(
|
|
||||||
coords,
|
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
this.state.zoom,
|
|
||||||
isElbowArrow(linearElement),
|
|
||||||
isElbowArrow(linearElement),
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
hoveredBindableElement != null &&
|
|
||||||
!isLinearElementSimpleAndAlreadyBound(
|
|
||||||
linearElement,
|
|
||||||
oppositeBindingBoundElement?.id,
|
|
||||||
hoveredBindableElement,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
acc.push(hoveredBindableElement);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setState({ suggestedBindings });
|
|
||||||
};
|
|
||||||
|
|
||||||
private clearSelection(hitElement: ExcalidrawElement | null): void {
|
private clearSelection(hitElement: ExcalidrawElement | null): void {
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
||||||
@ -10725,12 +10452,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({
|
||||||
|
@ -5,6 +5,10 @@ import { mutateElement } from "@excalidraw/element/mutateElement";
|
|||||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||||
import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
|
import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
import { getSuggestedBindingsForArrows } from "@excalidraw/element/binding";
|
||||||
|
|
||||||
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { Degrees } from "@excalidraw/math";
|
import type { Degrees } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
@ -14,9 +18,11 @@ import { angleIcon } from "../icons";
|
|||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type {
|
||||||
|
DragFinishedCallbackType,
|
||||||
|
DragInputCallbackType,
|
||||||
|
} from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
|
||||||
|
|
||||||
interface AngleProps {
|
interface AngleProps {
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
@ -33,9 +39,10 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
scene,
|
scene,
|
||||||
|
setAppState,
|
||||||
|
originalAppState,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
if (origElement && !isElbowArrow(origElement)) {
|
if (origElement && !isElbowArrow(origElement)) {
|
||||||
const latestElement = elementsMap.get(origElement.id);
|
const latestElement = elementsMap.get(origElement.id);
|
||||||
@ -48,7 +55,8 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||||||
mutateElement(latestElement, {
|
mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
updateBindings(latestElement, elementsMap, elements, scene);
|
|
||||||
|
updateBindings(latestElement, elementsMap, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
@ -74,15 +82,32 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||||||
mutateElement(latestElement, {
|
mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
updateBindings(latestElement, elementsMap, elements, scene);
|
|
||||||
|
updateBindings(latestElement, elementsMap, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle });
|
mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAppState({
|
||||||
|
suggestedBindings: getSuggestedBindingsForArrows(
|
||||||
|
[latestElement],
|
||||||
|
elementsMap,
|
||||||
|
originalAppState.zoom,
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFinished: DragFinishedCallbackType<AngleProps["property"]> = ({
|
||||||
|
setAppState,
|
||||||
|
}) => {
|
||||||
|
setAppState({
|
||||||
|
suggestedBindings: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const Angle = ({ element, scene, appState, property }: AngleProps) => {
|
const Angle = ({ element, scene, appState, property }: AngleProps) => {
|
||||||
return (
|
return (
|
||||||
<DragInput
|
<DragInput
|
||||||
@ -91,6 +116,7 @@ const Angle = ({ element, scene, appState, property }: AngleProps) => {
|
|||||||
value={Math.round((radiansToDegrees(element.angle) % 360) * 100) / 100}
|
value={Math.round((radiansToDegrees(element.angle) % 360) * 100) / 100}
|
||||||
elements={[element]}
|
elements={[element]}
|
||||||
dragInputCallback={handleDegreeChange}
|
dragInputCallback={handleDegreeChange}
|
||||||
|
dragFinishedCallback={handleFinished}
|
||||||
editable={isPropertyEditable(element, "angle")}
|
editable={isPropertyEditable(element, "angle")}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
@ -12,7 +12,7 @@ import { isImageElement } from "@excalidraw/element/typeChecks";
|
|||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
@ -118,6 +118,9 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateBindings(element, elementsMap, scene);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +153,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateBindings(element, elementsMap, scene);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,6 +189,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
updateBindings(origElement, elementsMap, scene);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const changeInWidth = property === "width" ? accumulatedChange : 0;
|
const changeInWidth = property === "width" ? accumulatedChange : 0;
|
||||||
@ -230,6 +237,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
shouldMaintainAspectRatio: keepAspectRatio,
|
shouldMaintainAspectRatio: keepAspectRatio,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
updateBindings(origElement, elementsMap, scene);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
|||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { CaptureUpdateAction } from "../../store";
|
import { CaptureUpdateAction } from "../../store";
|
||||||
import { useApp } from "../App";
|
import { useApp, useExcalidrawSetAppState } from "../App";
|
||||||
import { InlineIcon } from "../InlineIcon";
|
import { InlineIcon } from "../InlineIcon";
|
||||||
|
|
||||||
import { SMALLEST_DELTA } from "./utils";
|
import { SMALLEST_DELTA } from "./utils";
|
||||||
@ -34,6 +34,21 @@ export type DragInputCallbackType<
|
|||||||
property: P;
|
property: P;
|
||||||
originalAppState: AppState;
|
originalAppState: AppState;
|
||||||
setInputValue: (value: number) => void;
|
setInputValue: (value: number) => void;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
export type DragFinishedCallbackType<
|
||||||
|
P extends StatsInputProperty,
|
||||||
|
E = ExcalidrawElement,
|
||||||
|
> = (props: {
|
||||||
|
originalElements: readonly E[];
|
||||||
|
originalElementsMap: ElementsMap;
|
||||||
|
scene: Scene;
|
||||||
|
property: P;
|
||||||
|
originalAppState: AppState;
|
||||||
|
accumulatedChange: number;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
setInputValue: (value: number) => void;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
interface StatsDragInputProps<
|
interface StatsDragInputProps<
|
||||||
@ -47,6 +62,7 @@ interface StatsDragInputProps<
|
|||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
shouldKeepAspectRatio?: boolean;
|
shouldKeepAspectRatio?: boolean;
|
||||||
dragInputCallback: DragInputCallbackType<T, E>;
|
dragInputCallback: DragInputCallbackType<T, E>;
|
||||||
|
dragFinishedCallback?: DragFinishedCallbackType<T, E>;
|
||||||
property: T;
|
property: T;
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@ -61,6 +77,7 @@ const StatsDragInput = <
|
|||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
dragInputCallback,
|
dragInputCallback,
|
||||||
|
dragFinishedCallback,
|
||||||
value,
|
value,
|
||||||
elements,
|
elements,
|
||||||
editable = true,
|
editable = true,
|
||||||
@ -71,6 +88,7 @@ const StatsDragInput = <
|
|||||||
sensitivity = 1,
|
sensitivity = 1,
|
||||||
}: StatsDragInputProps<T, E>) => {
|
}: StatsDragInputProps<T, E>) => {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const labelRef = useRef<HTMLDivElement>(null);
|
const labelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -123,6 +141,7 @@ const StatsDragInput = <
|
|||||||
// reason: idempotent to avoid unnecessary
|
// reason: idempotent to avoid unnecessary
|
||||||
if (isNaN(original) || Math.abs(rounded - original) >= SMALLEST_DELTA) {
|
if (isNaN(original) || Math.abs(rounded - original) >= SMALLEST_DELTA) {
|
||||||
stateRef.current.lastUpdatedValue = updatedValue;
|
stateRef.current.lastUpdatedValue = updatedValue;
|
||||||
|
const originalElementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
dragInputCallback({
|
dragInputCallback({
|
||||||
accumulatedChange: 0,
|
accumulatedChange: 0,
|
||||||
instantChange: 0,
|
instantChange: 0,
|
||||||
@ -135,6 +154,17 @@ const StatsDragInput = <
|
|||||||
property,
|
property,
|
||||||
originalAppState: appState,
|
originalAppState: appState,
|
||||||
setInputValue: (value) => setInputValue(String(value)),
|
setInputValue: (value) => setInputValue(String(value)),
|
||||||
|
setAppState,
|
||||||
|
});
|
||||||
|
dragFinishedCallback?.({
|
||||||
|
originalElements: elements,
|
||||||
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
|
originalAppState: appState,
|
||||||
|
accumulatedChange: rounded,
|
||||||
|
property,
|
||||||
|
setAppState,
|
||||||
|
setInputValue: (value) => setInputValue(String(value)),
|
||||||
});
|
});
|
||||||
app.syncActionResult({
|
app.syncActionResult({
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
@ -262,6 +292,7 @@ const StatsDragInput = <
|
|||||||
scene,
|
scene,
|
||||||
originalAppState,
|
originalAppState,
|
||||||
setInputValue: (value) => setInputValue(String(value)),
|
setInputValue: (value) => setInputValue(String(value)),
|
||||||
|
setAppState,
|
||||||
});
|
});
|
||||||
|
|
||||||
stepChange = 0;
|
stepChange = 0;
|
||||||
@ -282,6 +313,19 @@ const StatsDragInput = <
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (originalElements !== null && originalElementsMap !== null) {
|
||||||
|
dragFinishedCallback?.({
|
||||||
|
originalElements,
|
||||||
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
|
originalAppState,
|
||||||
|
property,
|
||||||
|
accumulatedChange,
|
||||||
|
setAppState,
|
||||||
|
setInputValue: (value) => setInputValue(String(value)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
app.syncActionResult({
|
app.syncActionResult({
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,13 @@ import type {
|
|||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
import {
|
||||||
|
getAtomicUnits,
|
||||||
|
getStepSizedValue,
|
||||||
|
isPropertyEditable,
|
||||||
|
updateBindings,
|
||||||
|
updateSelectionBindings,
|
||||||
|
} from "./utils";
|
||||||
import { getElementsInAtomicUnit } from "./utils";
|
import { getElementsInAtomicUnit } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
@ -87,9 +93,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(
|
||||||
@ -120,6 +124,7 @@ const resizeGroup = (
|
|||||||
originalElements: ExcalidrawElement[],
|
originalElements: ExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
// keep aspect ratio for groups
|
// keep aspect ratio for groups
|
||||||
if (property === "width") {
|
if (property === "width") {
|
||||||
@ -145,6 +150,8 @@ const resizeGroup = (
|
|||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSelectionBindings(originalElements, elementsMap, scene);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDimensionChange: DragInputCallbackType<
|
const handleDimensionChange: DragInputCallbackType<
|
||||||
@ -196,6 +203,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
originalElements,
|
originalElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const [el] = elementsInUnit;
|
const [el] = elementsInUnit;
|
||||||
@ -244,6 +252,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
shouldInformMutation: false,
|
shouldInformMutation: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
updateBindings(latestElement, elementsMap, scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -303,6 +313,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
originalElements,
|
originalElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const [el] = elementsInUnit;
|
const [el] = elementsInUnit;
|
||||||
|
@ -8,12 +8,16 @@ import { getCommonBounds } from "@excalidraw/element/bounds";
|
|||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
import {
|
||||||
|
getAtomicUnits,
|
||||||
|
getStepSizedValue,
|
||||||
|
isPropertyEditable,
|
||||||
|
updateSelectionBindings,
|
||||||
|
} from "./utils";
|
||||||
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
@ -66,12 +70,12 @@ const moveElements = (
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
|
||||||
scene,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSelectionBindings(elements, elementsMap, scene);
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveGroupTo = (
|
const moveGroupTo = (
|
||||||
@ -79,7 +83,6 @@ const moveGroupTo = (
|
|||||||
nextY: number,
|
nextY: number,
|
||||||
originalElements: ExcalidrawElement[],
|
originalElements: ExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
@ -113,13 +116,13 @@ const moveGroupTo = (
|
|||||||
topLeftY + offsetY,
|
topLeftY + offsetY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
|
||||||
scene,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSelectionBindings(originalElements, elementsMap, scene);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePositionChange: DragInputCallbackType<
|
const handlePositionChange: DragInputCallbackType<
|
||||||
@ -135,7 +138,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
originalAppState,
|
originalAppState,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
for (const atomicUnit of getAtomicUnits(
|
for (const atomicUnit of getAtomicUnits(
|
||||||
@ -160,7 +162,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
elementsInUnit.map((el) => el.original),
|
elementsInUnit.map((el) => el.original),
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
@ -189,8 +190,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
|
||||||
scene,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -10,9 +10,12 @@ import { isImageElement } from "@excalidraw/element/typeChecks";
|
|||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getStepSizedValue, moveElement } from "./utils";
|
import { getStepSizedValue, moveElement, updateBindings } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type {
|
||||||
|
DragFinishedCallbackType,
|
||||||
|
DragInputCallbackType,
|
||||||
|
} from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
@ -36,9 +39,9 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
property,
|
property,
|
||||||
scene,
|
scene,
|
||||||
originalAppState,
|
originalAppState,
|
||||||
|
setAppState,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
const [cx, cy] = [
|
const [cx, cy] = [
|
||||||
origElement.x + origElement.width / 2,
|
origElement.x + origElement.width / 2,
|
||||||
@ -105,6 +108,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateBindings(element, elementsMap, scene);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +128,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateBindings(element, elementsMap, scene);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,10 +141,11 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
|
||||||
scene,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
updateBindings(origElement, elementsMap, scene);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,19 +175,21 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
|
||||||
scene,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
updateBindings(origElement, elementsMap, scene);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Position = ({
|
const handleFinished: DragFinishedCallbackType<"x" | "y"> = ({
|
||||||
property,
|
setAppState,
|
||||||
element,
|
}) => {
|
||||||
elementsMap,
|
setAppState({
|
||||||
scene,
|
suggestedBindings: [],
|
||||||
appState,
|
});
|
||||||
}: PositionProps) => {
|
};
|
||||||
|
|
||||||
|
const Position = ({ property, element, scene, appState }: PositionProps) => {
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
pointFrom(element.x, element.y),
|
pointFrom(element.x, element.y),
|
||||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
||||||
@ -207,6 +217,7 @@ const Position = ({
|
|||||||
label={property === "x" ? "X" : "Y"}
|
label={property === "x" ? "X" : "Y"}
|
||||||
elements={[element]}
|
elements={[element]}
|
||||||
dragInputCallback={handlePositionChange}
|
dragInputCallback={handlePositionChange}
|
||||||
|
dragFinishedCallback={handleFinished}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
value={value}
|
value={value}
|
||||||
property={property}
|
property={property}
|
||||||
|
@ -128,7 +128,7 @@ describe("binding with linear elements", () => {
|
|||||||
restoreOriginalGetBoundingClientRect();
|
restoreOriginalGetBoundingClientRect();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remain bound to linear element on small position change", async () => {
|
it("should not remain bound to linear element even on small position change", async () => {
|
||||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
const linear = h.elements[1] as ExcalidrawLinearElement;
|
||||||
const inputX = UI.queryStatsProperty("X")?.querySelector(
|
const inputX = UI.queryStatsProperty("X")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
@ -137,10 +137,10 @@ describe("binding with linear elements", () => {
|
|||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
expect(inputX).not.toBeNull();
|
expect(inputX).not.toBeNull();
|
||||||
UI.updateInput(inputX, String("204"));
|
UI.updateInput(inputX, String("204"));
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remain bound to linear element on small angle change", async () => {
|
it("should not remain bound to linear element on any angle change", async () => {
|
||||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
const linear = h.elements[1] as ExcalidrawLinearElement;
|
||||||
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
@ -148,7 +148,7 @@ describe("binding with linear elements", () => {
|
|||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
UI.updateInput(inputAngle, String("1"));
|
UI.updateInput(inputAngle, String("1"));
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should unbind linear element on large position change", async () => {
|
it("should unbind linear element on large position change", async () => {
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
|
||||||
bindOrUnbindLinearElements,
|
|
||||||
updateBoundElements,
|
|
||||||
} from "@excalidraw/element/binding";
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||||
import {
|
import {
|
||||||
|
isBindableElement,
|
||||||
|
isBindingElement,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
@ -18,6 +15,11 @@ import {
|
|||||||
isInGroup,
|
isInGroup,
|
||||||
} from "@excalidraw/element/groups";
|
} from "@excalidraw/element/groups";
|
||||||
|
|
||||||
|
import {
|
||||||
|
bindOrUnbindLinearElement,
|
||||||
|
updateBoundElements,
|
||||||
|
} from "@excalidraw/element/binding";
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -27,7 +29,8 @@ import type {
|
|||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||||
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
export type StatsInputProperty =
|
export type StatsInputProperty =
|
||||||
@ -120,8 +123,6 @@ export const moveElement = (
|
|||||||
newTopLeftY: number,
|
newTopLeftY: number,
|
||||||
originalElement: ExcalidrawElement,
|
originalElement: ExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scene: Scene,
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
) => {
|
) => {
|
||||||
@ -156,7 +157,10 @@ export const moveElement = (
|
|||||||
},
|
},
|
||||||
shouldInformMutation,
|
shouldInformMutation,
|
||||||
);
|
);
|
||||||
updateBindings(latestElement, elementsMap, elements, scene);
|
|
||||||
|
if (isBindableElement(latestElement)) {
|
||||||
|
updateBoundElements(latestElement, elementsMap);
|
||||||
|
}
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(
|
const boundTextElement = getBoundTextElement(
|
||||||
originalElement,
|
originalElement,
|
||||||
@ -200,25 +204,34 @@ export const getAtomicUnits = (
|
|||||||
export const updateBindings = (
|
export const updateBindings = (
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
options?: {
|
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
|
||||||
newSize?: { width: number; height: number };
|
|
||||||
zoom?: AppState["zoom"];
|
|
||||||
},
|
|
||||||
) => {
|
) => {
|
||||||
if (isLinearElement(latestElement)) {
|
if (isBindingElement(latestElement)) {
|
||||||
bindOrUnbindLinearElements(
|
if (latestElement.startBinding || latestElement.endBinding) {
|
||||||
[latestElement],
|
bindOrUnbindLinearElement(latestElement, null, null, elementsMap, scene);
|
||||||
elementsMap,
|
}
|
||||||
elements,
|
} else if (isBindableElement(latestElement)) {
|
||||||
scene,
|
updateBoundElements(latestElement, elementsMap);
|
||||||
true,
|
}
|
||||||
[],
|
};
|
||||||
options?.zoom,
|
|
||||||
);
|
export const updateSelectionBindings = (
|
||||||
} else {
|
elements: readonly ExcalidrawElement[],
|
||||||
updateBoundElements(latestElement, elementsMap, options);
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
scene: Scene,
|
||||||
|
) => {
|
||||||
|
for (const element of elements) {
|
||||||
|
// Only preserve bindings if the bound element is in the selection
|
||||||
|
if (isBindingElement(element)) {
|
||||||
|
if (elements.find((el) => el.id !== element.startBinding?.elementId)) {
|
||||||
|
bindOrUnbindLinearElement(element, null, "keep", elementsMap, scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.find((el) => el.id !== element.endBinding?.elementId)) {
|
||||||
|
bindOrUnbindLinearElement(element, "keep", null, elementsMap, scene);
|
||||||
|
}
|
||||||
|
} else if (isBindableElement(element)) {
|
||||||
|
updateBoundElements(element, elementsMap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -89,12 +89,12 @@ 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,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 35,
|
"height": 33.53813187180941,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -108,8 +108,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
394,
|
377.5017739818493,
|
||||||
34,
|
32.53813187180941,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -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",
|
||||||
@ -128,7 +128,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 395,
|
"width": 378.5017739818493,
|
||||||
"x": 247.5,
|
"x": 247.5,
|
||||||
"y": 420.5,
|
"y": 420.5,
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
@ -164,7 +164,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
399,
|
397.82801810964474,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -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",
|
||||||
@ -184,7 +184,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 400,
|
"width": 398.82801810964474,
|
||||||
"x": 227.5,
|
"x": 227.5,
|
||||||
"y": 450,
|
"y": 450,
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
@ -354,7 +354,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99,
|
299,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -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",
|
||||||
@ -374,7 +374,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 300,
|
||||||
"x": 255.5,
|
"x": 255.5,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
@ -456,7 +456,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99,
|
89,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -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",
|
||||||
@ -476,7 +476,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 90,
|
||||||
"x": 255.5,
|
"x": 255.5,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
@ -592,7 +592,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 355,
|
"x": 350,
|
||||||
"y": 189,
|
"y": 189,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -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,
|
||||||
@ -632,7 +632,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
99,
|
89,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -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",
|
||||||
@ -652,7 +652,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 100,
|
"width": 90,
|
||||||
"x": 255.5,
|
"x": 255.5,
|
||||||
"y": 239,
|
"y": 239,
|
||||||
}
|
}
|
||||||
@ -786,7 +786,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "top",
|
"verticalAlign": "top",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 355,
|
"x": 350,
|
||||||
"y": 226.5,
|
"y": 226.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -1475,12 +1475,12 @@ 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,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 0,
|
"height": 7.105427357601002e-15,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a4",
|
"index": "a4",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -1494,8 +1494,8 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
271.985,
|
272.28487500000006,
|
||||||
0,
|
-0.9999999999999929,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -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",
|
||||||
@ -1516,9 +1516,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"width": 272.985,
|
"width": 273.28487500000006,
|
||||||
"x": 111.762,
|
"x": 111.762,
|
||||||
"y": 57,
|
"y": 57.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -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",
|
||||||
|
@ -20,10 +20,6 @@ import {
|
|||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
import { normalizeFixedPoint } from "@excalidraw/element/binding";
|
import { normalizeFixedPoint } from "@excalidraw/element/binding";
|
||||||
import {
|
|
||||||
updateElbowArrowPoints,
|
|
||||||
validateElbowPoints,
|
|
||||||
} from "@excalidraw/element/elbowArrow";
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
import { bumpVersion } from "@excalidraw/element/mutateElement";
|
import { bumpVersion } from "@excalidraw/element/mutateElement";
|
||||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||||
@ -57,7 +53,6 @@ import type {
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
PointBinding,
|
PointBinding,
|
||||||
StrokeRoundness,
|
StrokeRoundness,
|
||||||
@ -585,73 +580,7 @@ export const restoreElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE (mtolmacs): Temporary fix for extremely large arrows
|
return restoredElements;
|
||||||
// Need to iterate again so we have attached text nodes in elementsMap
|
|
||||||
return restoredElements.map((element) => {
|
|
||||||
if (
|
|
||||||
isElbowArrow(element) &&
|
|
||||||
element.startBinding == null &&
|
|
||||||
element.endBinding == null &&
|
|
||||||
!validateElbowPoints(element.points)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...element,
|
|
||||||
...updateElbowArrowPoints(
|
|
||||||
element,
|
|
||||||
restoredElementsMap as NonDeletedSceneElementsMap,
|
|
||||||
{
|
|
||||||
points: [
|
|
||||||
pointFrom<LocalPoint>(0, 0),
|
|
||||||
element.points[element.points.length - 1],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
index: element.index,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isElbowArrow(element) &&
|
|
||||||
element.startBinding &&
|
|
||||||
element.endBinding &&
|
|
||||||
element.startBinding.elementId === element.endBinding.elementId &&
|
|
||||||
element.points.length > 1 &&
|
|
||||||
element.points.some(
|
|
||||||
([rx, ry]) => Math.abs(rx) > 1e6 || Math.abs(ry) > 1e6,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
console.error("Fixing self-bound elbow arrow", element.id);
|
|
||||||
const boundElement = restoredElementsMap.get(
|
|
||||||
element.startBinding.elementId,
|
|
||||||
);
|
|
||||||
if (!boundElement) {
|
|
||||||
console.error(
|
|
||||||
"Bound element not found",
|
|
||||||
element.startBinding.elementId,
|
|
||||||
);
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...element,
|
|
||||||
x: boundElement.x + boundElement.width / 2,
|
|
||||||
y: boundElement.y - 5,
|
|
||||||
width: boundElement.width,
|
|
||||||
height: boundElement.height,
|
|
||||||
points: [
|
|
||||||
pointFrom<LocalPoint>(0, 0),
|
|
||||||
pointFrom<LocalPoint>(0, -10),
|
|
||||||
pointFrom<LocalPoint>(boundElement.width / 2 + 5, -10),
|
|
||||||
pointFrom<LocalPoint>(
|
|
||||||
boundElement.width / 2 + 5,
|
|
||||||
boundElement.height / 2 + 5,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const coalesceAppStateValue = <
|
const coalesceAppStateValue = <
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element/binding";
|
||||||
|
|
||||||
import type { ExcalidrawArrowElement } from "@excalidraw/element/types";
|
import type { ExcalidrawArrowElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { convertToExcalidrawElements } from "./transform";
|
import { convertToExcalidrawElements } from "./transform";
|
||||||
@ -433,7 +435,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,
|
||||||
@ -462,7 +464,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(ellipse).toMatchObject({
|
expect(ellipse).toMatchObject({
|
||||||
x: 355,
|
x: 350,
|
||||||
y: 189,
|
y: 189,
|
||||||
type: "ellipse",
|
type: "ellipse",
|
||||||
boundElements: [
|
boundElements: [
|
||||||
@ -518,7 +520,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,
|
||||||
@ -547,7 +549,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(text3).toMatchObject({
|
expect(text3).toMatchObject({
|
||||||
x: 355,
|
x: 350,
|
||||||
y: 226.5,
|
y: 226.5,
|
||||||
type: "text",
|
type: "text",
|
||||||
boundElements: [
|
boundElements: [
|
||||||
@ -781,7 +783,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([
|
||||||
{
|
{
|
||||||
|
457
packages/excalidraw/linear.ts
Normal file
457
packages/excalidraw/linear.ts
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
import {
|
||||||
|
CURSOR_TYPE,
|
||||||
|
getGridPoint,
|
||||||
|
KEYS,
|
||||||
|
LINE_CONFIRM_THRESHOLD,
|
||||||
|
shouldRotateWithDiscreteAngle,
|
||||||
|
updateActiveTool,
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { getLockedLinearCursorAlignSize } from "@excalidraw/element/sizeHelpers";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isBindingElement,
|
||||||
|
isElbowArrow,
|
||||||
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
import {
|
||||||
|
getHoveredElementForBinding,
|
||||||
|
getOutlineAvoidingPoint,
|
||||||
|
isBindingEnabled,
|
||||||
|
isLinearElementSimpleAndAlreadyBound,
|
||||||
|
maybeBindLinearElement,
|
||||||
|
} from "@excalidraw/element/binding";
|
||||||
|
|
||||||
|
import { pointDistance, pointFrom } from "@excalidraw/math";
|
||||||
|
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||||
|
|
||||||
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
|
import { isPathALoop } from "@excalidraw/element/shapes";
|
||||||
|
|
||||||
|
import { makeNextSelectedElementIds } from "@excalidraw/element/selection";
|
||||||
|
|
||||||
|
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
NonDeleted,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import { resetCursor, setCursor, setCursorForShape } from "./cursor";
|
||||||
|
|
||||||
|
import type App from "./components/App";
|
||||||
|
|
||||||
|
import type { ActiveTool, PointerDownState } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called when the user drags the pointer to create a new linear element.
|
||||||
|
*/
|
||||||
|
export function onPointerMoveFromPointerDownOnLinearElement(
|
||||||
|
newElement: ExcalidrawLinearElement,
|
||||||
|
app: App,
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
pointerCoords: { x: number; y: number },
|
||||||
|
event: PointerEvent,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
) {
|
||||||
|
pointerDownState.drag.hasOccurred = true;
|
||||||
|
const points = newElement.points;
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
pointerCoords.x,
|
||||||
|
pointerCoords.y,
|
||||||
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
|
);
|
||||||
|
let dx = gridX - newElement.x;
|
||||||
|
let dy = gridY - newElement.y;
|
||||||
|
|
||||||
|
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
|
||||||
|
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
|
||||||
|
newElement.x,
|
||||||
|
newElement.y,
|
||||||
|
pointerCoords.x,
|
||||||
|
pointerCoords.y,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points.length === 1) {
|
||||||
|
let x = newElement.x + dx;
|
||||||
|
let y = newElement.y + dy;
|
||||||
|
if (isArrowElement(newElement)) {
|
||||||
|
[x, y] = getOutlineAvoidingPoint(
|
||||||
|
newElement,
|
||||||
|
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
|
||||||
|
newElement.points.length - 1,
|
||||||
|
app.scene,
|
||||||
|
app.state.zoom,
|
||||||
|
pointFrom<GlobalPoint>(newElement.x + dx, newElement.y + dy),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(
|
||||||
|
newElement,
|
||||||
|
{
|
||||||
|
points: [
|
||||||
|
...points,
|
||||||
|
pointFrom<LocalPoint>(x - newElement.x, y - newElement.y),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
points.length === 2 ||
|
||||||
|
(points.length > 1 && isElbowArrow(newElement))
|
||||||
|
) {
|
||||||
|
const targets = [];
|
||||||
|
|
||||||
|
if (isArrowElement(newElement)) {
|
||||||
|
const [endX, endY] = getOutlineAvoidingPoint(
|
||||||
|
newElement,
|
||||||
|
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
|
||||||
|
points.length - 1,
|
||||||
|
app.scene,
|
||||||
|
app.state.zoom,
|
||||||
|
pointFrom<GlobalPoint>(newElement.x + dx, newElement.y + dy),
|
||||||
|
);
|
||||||
|
|
||||||
|
targets.push({
|
||||||
|
index: points.length - 1,
|
||||||
|
isDragging: true,
|
||||||
|
point: pointFrom<LocalPoint>(endX - newElement.x, endY - newElement.y),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
targets.push({
|
||||||
|
index: points.length - 1,
|
||||||
|
isDragging: true,
|
||||||
|
point: pointFrom<LocalPoint>(dx, dy),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearElementEditor.movePoints(newElement, targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.setState({
|
||||||
|
newElement,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isBindingElement(newElement, false)) {
|
||||||
|
// When creating a linear element by dragging
|
||||||
|
maybeSuggestBindingsForLinearElementAtCoords(
|
||||||
|
newElement,
|
||||||
|
[pointerCoords],
|
||||||
|
app,
|
||||||
|
app.state.startBoundElement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function handleCanvasPointerMoveForLinearElement(
|
||||||
|
multiElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
app: App,
|
||||||
|
scenePointerX: number,
|
||||||
|
scenePointerY: number,
|
||||||
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
|
triggerRender: (forceUpdate?: boolean) => void,
|
||||||
|
) {
|
||||||
|
const { x: rx, y: ry } = multiElement;
|
||||||
|
|
||||||
|
const { points, lastCommittedPoint } = multiElement;
|
||||||
|
const lastPoint = points[points.length - 1];
|
||||||
|
|
||||||
|
setCursorForShape(app.interactiveCanvas, app.state);
|
||||||
|
|
||||||
|
if (lastPoint === lastCommittedPoint) {
|
||||||
|
// if we haven't yet created a temp point and we're beyond commit-zone
|
||||||
|
// threshold, add a point
|
||||||
|
if (
|
||||||
|
pointDistance(
|
||||||
|
pointFrom(scenePointerX - rx, scenePointerY - ry),
|
||||||
|
lastPoint,
|
||||||
|
) >= LINE_CONFIRM_THRESHOLD
|
||||||
|
) {
|
||||||
|
mutateElement(
|
||||||
|
multiElement,
|
||||||
|
{
|
||||||
|
points: [
|
||||||
|
...points,
|
||||||
|
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
|
// in this branch, we're inside the commit zone, and no uncommitted
|
||||||
|
// point exists. Thus do nothing (don't add/remove points).
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
points.length > 2 &&
|
||||||
|
lastCommittedPoint &&
|
||||||
|
pointDistance(
|
||||||
|
pointFrom(scenePointerX - rx, scenePointerY - ry),
|
||||||
|
lastCommittedPoint,
|
||||||
|
) < LINE_CONFIRM_THRESHOLD
|
||||||
|
) {
|
||||||
|
setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
|
mutateElement(
|
||||||
|
multiElement,
|
||||||
|
{
|
||||||
|
points: points.slice(0, -1),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [lastCommittedX, lastCommittedY] =
|
||||||
|
multiElement?.lastCommittedPoint ?? [0, 0];
|
||||||
|
|
||||||
|
let dxFromLastCommitted = gridX - rx - lastCommittedX;
|
||||||
|
let dyFromLastCommitted = gridY - ry - lastCommittedY;
|
||||||
|
|
||||||
|
if (shouldRotateWithDiscreteAngle(event)) {
|
||||||
|
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
|
||||||
|
getLockedLinearCursorAlignSize(
|
||||||
|
// actual coordinate of the last committed point
|
||||||
|
lastCommittedX + rx,
|
||||||
|
lastCommittedY + ry,
|
||||||
|
// cursor-grid coordinate
|
||||||
|
gridX,
|
||||||
|
gridY,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPathALoop(points, app.state.zoom.value)) {
|
||||||
|
setCursor(app.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = multiElement.x + lastCommittedX + dxFromLastCommitted;
|
||||||
|
let y = multiElement.y + lastCommittedY + dyFromLastCommitted;
|
||||||
|
|
||||||
|
if (isArrowElement(multiElement)) {
|
||||||
|
[x, y] = getOutlineAvoidingPoint(
|
||||||
|
multiElement,
|
||||||
|
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||||
|
multiElement.points.length - 1,
|
||||||
|
app.scene,
|
||||||
|
app.state.zoom,
|
||||||
|
pointFrom<GlobalPoint>(x, y),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update last uncommitted point
|
||||||
|
LinearElementEditor.movePoints(multiElement, [
|
||||||
|
{
|
||||||
|
index: points.length - 1,
|
||||||
|
point: pointFrom<LocalPoint>(x - multiElement.x, y - multiElement.y),
|
||||||
|
isDragging: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// in this path, we're mutating multiElement to reflect
|
||||||
|
// how it will be after adding pointer position as the next point
|
||||||
|
// trigger update here so that new element canvas renders again to reflect this
|
||||||
|
triggerRender(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onPointerUpFromPointerDownOnLinearElementHandler(
|
||||||
|
newElement: ExcalidrawLinearElement,
|
||||||
|
multiElement: NonDeleted<ExcalidrawLinearElement> | null,
|
||||||
|
app: App,
|
||||||
|
store: App["store"],
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
childEvent: PointerEvent,
|
||||||
|
activeTool: {
|
||||||
|
lastActiveTool: ActiveTool | null;
|
||||||
|
locked: boolean;
|
||||||
|
fromSelection: boolean;
|
||||||
|
} & ActiveTool,
|
||||||
|
) {
|
||||||
|
if (newElement!.points.length > 1) {
|
||||||
|
store.shouldCaptureIncrement();
|
||||||
|
}
|
||||||
|
const pointerCoords = viewportCoordsToSceneCoords(childEvent, app.state);
|
||||||
|
|
||||||
|
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
|
||||||
|
mutateElement(newElement, {
|
||||||
|
points: [
|
||||||
|
...newElement.points,
|
||||||
|
pointFrom<LocalPoint>(
|
||||||
|
pointerCoords.x - newElement.x,
|
||||||
|
pointerCoords.y - newElement.y,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
app.setState({
|
||||||
|
multiElement: newElement,
|
||||||
|
newElement,
|
||||||
|
});
|
||||||
|
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
|
||||||
|
if (isBindingEnabled(app.state) && isBindingElement(newElement, false)) {
|
||||||
|
maybeBindLinearElement(
|
||||||
|
newElement,
|
||||||
|
app.state,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
app.scene.getNonDeletedElements(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
app.setState({ suggestedBindings: [], startBoundElement: null });
|
||||||
|
if (!activeTool.locked) {
|
||||||
|
resetCursor(app.interactiveCanvas);
|
||||||
|
app.setState((prevState) => ({
|
||||||
|
newElement: null,
|
||||||
|
activeTool: updateActiveTool(app.state, {
|
||||||
|
type: "selection",
|
||||||
|
}),
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
{
|
||||||
|
...prevState.selectedElementIds,
|
||||||
|
[newElement.id]: true,
|
||||||
|
},
|
||||||
|
prevState,
|
||||||
|
),
|
||||||
|
selectedLinearElement: new LinearElementEditor(newElement),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
app.setState((prevState) => ({
|
||||||
|
newElement: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// so that the scene gets rendered again to display the newly drawn linear as well
|
||||||
|
app.scene.triggerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles double click on a linear element to edit it or delete a segment
|
||||||
|
*/
|
||||||
|
export function handleDoubleClickForLinearElement(
|
||||||
|
app: App,
|
||||||
|
store: App["store"],
|
||||||
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
event: React.MouseEvent<HTMLCanvasElement>,
|
||||||
|
sceneX: number,
|
||||||
|
sceneY: number,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
(!app.state.editingLinearElement ||
|
||||||
|
app.state.editingLinearElement.elementId !== selectedElement.id) &&
|
||||||
|
!isElbowArrow(selectedElement)
|
||||||
|
) {
|
||||||
|
store.shouldCaptureIncrement();
|
||||||
|
app.setState({
|
||||||
|
editingLinearElement: new LinearElementEditor(selectedElement),
|
||||||
|
});
|
||||||
|
} else if (app.state.selectedLinearElement && isElbowArrow(selectedElement)) {
|
||||||
|
const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||||
|
app.state.selectedLinearElement,
|
||||||
|
{ x: sceneX, y: sceneY },
|
||||||
|
app.state,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
const midPoint = hitCoords
|
||||||
|
? LinearElementEditor.getSegmentMidPointIndex(
|
||||||
|
app.state.selectedLinearElement,
|
||||||
|
app.state,
|
||||||
|
hitCoords,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
)
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
if (midPoint && midPoint > -1) {
|
||||||
|
store.shouldCaptureIncrement();
|
||||||
|
LinearElementEditor.deleteFixedSegment(selectedElement, midPoint);
|
||||||
|
|
||||||
|
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||||
|
{
|
||||||
|
...app.state.selectedLinearElement,
|
||||||
|
segmentMidPointHoveredCoords: null,
|
||||||
|
},
|
||||||
|
{ x: sceneX, y: sceneY },
|
||||||
|
app.state,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
const nextIndex = nextCoords
|
||||||
|
? LinearElementEditor.getSegmentMidPointIndex(
|
||||||
|
app.state.selectedLinearElement,
|
||||||
|
app.state,
|
||||||
|
nextCoords,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
app.setState({
|
||||||
|
selectedLinearElement: {
|
||||||
|
...app.state.selectedLinearElement,
|
||||||
|
pointerDownState: {
|
||||||
|
...app.state.selectedLinearElement.pointerDownState,
|
||||||
|
segmentMidpoint: {
|
||||||
|
index: nextIndex,
|
||||||
|
value: hitCoords,
|
||||||
|
added: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
segmentMidPointHoveredCoords: nextCoords,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function maybeSuggestBindingsForLinearElementAtCoords(
|
||||||
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
/** scene coords */
|
||||||
|
pointerCoords: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}[],
|
||||||
|
app: App,
|
||||||
|
// During line creation the start binding hasn't been written yet
|
||||||
|
// into `linearElement`
|
||||||
|
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
|
||||||
|
) {
|
||||||
|
if (!pointerCoords.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestedBindings = pointerCoords.reduce(
|
||||||
|
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
|
||||||
|
const hoveredBindableElement = getHoveredElementForBinding(
|
||||||
|
coords,
|
||||||
|
app.scene.getNonDeletedElements(),
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
app.state.zoom,
|
||||||
|
isElbowArrow(linearElement),
|
||||||
|
isElbowArrow(linearElement),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
hoveredBindableElement != null &&
|
||||||
|
!isLinearElementSimpleAndAlreadyBound(
|
||||||
|
linearElement,
|
||||||
|
oppositeBindingBoundElement?.id,
|
||||||
|
hoveredBindableElement,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
acc.push(hoveredBindableElement);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
app.setState({ suggestedBindings });
|
||||||
|
}
|
@ -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.23572",
|
||||||
"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",
|
"96.42891",
|
||||||
"102.35417",
|
"99.23572",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"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": "96.42891",
|
||||||
"x": "0.70711",
|
"x": "3.53553",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -295,47 +295,47 @@ History {
|
|||||||
"deleted": {
|
"deleted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id171",
|
"elementId": "id171",
|
||||||
"focus": "0.00990",
|
"focus": "0.01065",
|
||||||
"gap": 1,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
"height": "0.98586",
|
"height": "1.00000",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"92.92893",
|
||||||
"-0.98586",
|
"-1.00000",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id170",
|
"elementId": "id170",
|
||||||
"focus": "0.02970",
|
"focus": "0.03194",
|
||||||
"gap": 1,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id171",
|
"elementId": "id171",
|
||||||
"focus": "-0.02000",
|
"focus": "-0.02251",
|
||||||
"gap": 1,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
"height": "0.00000",
|
"height": "0.08238",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"92.92893",
|
||||||
"0.00000",
|
"0.08238",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id170",
|
"elementId": "id170",
|
||||||
"focus": "0.02000",
|
"focus": "0.01897",
|
||||||
"gap": 1,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -390,43 +390,47 @@ History {
|
|||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "102.35417",
|
"height": "99.23572",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"101.77517",
|
"96.42891",
|
||||||
"102.35417",
|
"99.23572",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
|
"width": "96.42891",
|
||||||
|
"x": "3.53553",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id171",
|
"elementId": "id171",
|
||||||
"focus": "0.00990",
|
"focus": "0.01065",
|
||||||
"gap": 1,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
"height": "0.98586",
|
"height": "1.00000",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"92.92893",
|
||||||
"-0.98586",
|
"-1.00000",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id170",
|
"elementId": "id170",
|
||||||
"focus": "0.02970",
|
"focus": "0.03194",
|
||||||
"gap": 1,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
"y": "0.99364",
|
"width": "92.92893",
|
||||||
|
"x": "3.53553",
|
||||||
|
"y": "1.03339",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id175" => Delta {
|
"id175" => Delta {
|
||||||
@ -566,7 +570,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"96.46447",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -580,8 +584,8 @@ History {
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 100,
|
"width": "96.46447",
|
||||||
"x": 0,
|
"x": "3.53553",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@ -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,10 +858,11 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0,
|
"0.00000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
"width": "0.00000",
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"points": [
|
"points": [
|
||||||
@ -866,10 +871,11 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"89.39340",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
"width": "89.39340",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -921,17 +927,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 +947,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 +1084,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"96.46447",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -1088,8 +1098,8 @@ History {
|
|||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 100,
|
"width": "96.46447",
|
||||||
"x": 0,
|
"x": "3.53553",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@ -1241,7 +1251,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 +1265,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 +1289,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 +1623,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 +1637,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 +1661,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 +1781,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 +1794,8 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"92.92893",
|
||||||
"11.27227",
|
"12.86717",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -1806,8 +1816,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 +2331,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.23631",
|
||||||
"id": "id186",
|
"id": "id186",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2340,8 +2350,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"502.78936",
|
"496.83418",
|
||||||
"-374.05754",
|
"-369.23631",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -2352,7 +2362,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 +2370,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.83418",
|
||||||
"x": "-0.83465",
|
"x": "2.19080",
|
||||||
"y": "-36.58211",
|
"y": "-38.78706",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2481,7 +2491,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 +2509,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -2511,14 +2521,14 @@ 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": "92.92893",
|
||||||
"x": 0,
|
"x": "3.53553",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@ -7490,7 +7500,7 @@ History {
|
|||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of elements 1`] = `1`;
|
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `10`;
|
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `11`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] appState 1`] = `
|
exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
@ -10561,7 +10571,7 @@ History {
|
|||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of elements 1`] = `1`;
|
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `15`;
|
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `16`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] appState 1`] = `
|
exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
@ -15161,7 +15171,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 +15190,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -15192,7 +15202,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 +15210,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 +15252,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -15255,7 +15265,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -15532,7 +15542,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 +15560,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -15562,14 +15572,14 @@ 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": "92.92893",
|
||||||
"x": 0,
|
"x": "3.53553",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@ -15859,7 +15869,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 +15888,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -15890,7 +15900,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 +15908,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 +16162,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 +16180,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -16182,14 +16192,14 @@ 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": "92.92893",
|
||||||
"x": 0,
|
"x": "3.53553",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@ -16479,7 +16489,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 +16508,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -16510,7 +16520,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 +16528,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 +16782,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 +16800,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -16802,14 +16812,14 @@ 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": "92.92893",
|
||||||
"x": 0,
|
"x": "3.53553",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@ -17097,7 +17107,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 +17126,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -17128,7 +17138,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 +17146,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 +17203,14 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id68",
|
"elementId": "id68",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@ -17210,7 +17220,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -17460,7 +17470,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 +17488,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -17490,14 +17500,14 @@ 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": "92.92893",
|
||||||
"x": 0,
|
"x": "3.53553",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@ -17811,7 +17821,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 +17840,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -17842,7 +17852,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 +17860,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 +17923,7 @@ History {
|
|||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id77",
|
"elementId": "id77",
|
||||||
"focus": -0,
|
"focus": -0,
|
||||||
"gap": 1,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
@ -17921,14 +17931,14 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id75",
|
"elementId": "id75",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@ -17939,7 +17949,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -18189,7 +18199,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 +18217,7 @@ History {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
100,
|
"92.92893",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -18219,14 +18229,14 @@ 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": "92.92893",
|
||||||
"x": 0,
|
"x": "3.53553",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
@ -20188,4 +20198,4 @@ History {
|
|||||||
|
|
||||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of elements 1`] = `1`;
|
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `21`;
|
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `22`;
|
||||||
|
@ -190,13 +190,13 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id1",
|
"elementId": "id1",
|
||||||
"focus": "-0.46667",
|
"focus": "-0.40764",
|
||||||
"gap": 10,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "87.29887",
|
"height": "82.18136",
|
||||||
"id": "id2",
|
"id": "id2",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -210,8 +210,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"86.85786",
|
"93.92893",
|
||||||
"87.29887",
|
"82.18136",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -222,8 +222,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"focus": "-0.60000",
|
"focus": "-0.49801",
|
||||||
"gap": 10,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -232,8 +232,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"versionNonce": 1051383431,
|
"versionNonce": 1051383431,
|
||||||
"width": "86.85786",
|
"width": "93.92893",
|
||||||
"x": "107.07107",
|
"x": "103.53553",
|
||||||
"y": "47.07107",
|
"y": "50.01536",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -50,7 +50,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 1505387817,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -106,7 +106,7 @@ exports[`multi point mode in linear elements > line 3`] = `
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 1505387817,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
@ -6835,7 +6835,7 @@ History {
|
|||||||
|
|
||||||
exports[`regression tests > draw every type of shape > [end of test] number of elements 1`] = `0`;
|
exports[`regression tests > draw every type of shape > [end of test] number of elements 1`] = `0`;
|
||||||
|
|
||||||
exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `33`;
|
exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `35`;
|
||||||
|
|
||||||
exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] appState 1`] = `
|
exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
@ -14566,7 +14566,7 @@ History {
|
|||||||
|
|
||||||
exports[`regression tests > undo/redo drawing an element > [end of test] number of elements 1`] = `0`;
|
exports[`regression tests > undo/redo drawing an element > [end of test] number of elements 1`] = `0`;
|
||||||
|
|
||||||
exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `20`;
|
exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `21`;
|
||||||
|
|
||||||
exports[`regression tests > updates fontSize & fontFamily appState > [end of test] appState 1`] = `
|
exports[`regression tests > updates fontSize & fontFamily appState > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
|
@ -467,6 +467,7 @@ export class UI {
|
|||||||
height: initialHeight = initialWidth,
|
height: initialHeight = initialWidth,
|
||||||
angle = 0,
|
angle = 0,
|
||||||
points: initialPoints,
|
points: initialPoints,
|
||||||
|
elbowed = false,
|
||||||
}: {
|
}: {
|
||||||
position?: number;
|
position?: number;
|
||||||
x?: number;
|
x?: number;
|
||||||
@ -476,6 +477,7 @@ export class UI {
|
|||||||
height?: number;
|
height?: number;
|
||||||
angle?: number;
|
angle?: number;
|
||||||
points?: T extends "line" | "arrow" | "freedraw" ? LocalPoint[] : never;
|
points?: T extends "line" | "arrow" | "freedraw" ? LocalPoint[] : never;
|
||||||
|
elbowed?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
): Element<T> & {
|
): Element<T> & {
|
||||||
/** Returns the actual, current element from the elements array, instead
|
/** Returns the actual, current element from the elements array, instead
|
||||||
@ -494,6 +496,17 @@ export class UI {
|
|||||||
if (type === "text") {
|
if (type === "text") {
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.click(x, y);
|
mouse.click(x, y);
|
||||||
|
} else if (type === "arrow" && points.length === 2 && elbowed) {
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(x + points[0][0], y + points[0][1]);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(
|
||||||
|
x + points[points.length - 1][0],
|
||||||
|
y + points[points.length - 1][1],
|
||||||
|
);
|
||||||
|
mouse.click();
|
||||||
|
Keyboard.keyPress(KEYS.ESCAPE);
|
||||||
} else if ((type === "line" || type === "arrow") && points.length > 2) {
|
} else if ((type === "line" || type === "arrow") && points.length > 2) {
|
||||||
points.forEach((point) => {
|
points.forEach((point) => {
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
waitFor,
|
waitFor,
|
||||||
} from "@testing-library/react";
|
} from "@testing-library/react";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element/binding";
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
@ -4779,12 +4780,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,
|
||||||
}),
|
}),
|
||||||
|
@ -1247,7 +1247,7 @@ describe("Test Linear Elements", () => {
|
|||||||
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
|
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
expect(arrow.width).toBe(400);
|
expect(arrow.width).toBeCloseTo(408, 0);
|
||||||
expect(rect.x).toBe(400);
|
expect(rect.x).toBe(400);
|
||||||
expect(rect.y).toBe(0);
|
expect(rect.y).toBe(0);
|
||||||
expect(
|
expect(
|
||||||
@ -1266,7 +1266,7 @@ describe("Test Linear Elements", () => {
|
|||||||
mouse.downAt(rect.x, rect.y);
|
mouse.downAt(rect.x, rect.y);
|
||||||
mouse.moveTo(200, 0);
|
mouse.moveTo(200, 0);
|
||||||
mouse.upAt(200, 0);
|
mouse.upAt(200, 0);
|
||||||
expect(arrow.width).toBeCloseTo(204, 0);
|
expect(arrow.width).toBeCloseTo(207, 0);
|
||||||
expect(rect.x).toBe(200);
|
expect(rect.x).toBe(200);
|
||||||
expect(rect.y).toBe(0);
|
expect(rect.y).toBe(0);
|
||||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||||
|
@ -109,8 +109,10 @@ describe("move element", () => {
|
|||||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||||
expect([rectB.x, rectB.y]).toEqual([200, 0]);
|
expect([rectB.x, rectB.y]).toEqual([200, 0]);
|
||||||
expect([arrow.x, arrow.y]).toEqual([110, 50]);
|
expect([Math.round(arrow.x), Math.round(arrow.y)]).toEqual([104, 50]);
|
||||||
expect([arrow.width, arrow.height]).toEqual([80, 80]);
|
expect([Math.round(arrow.width), Math.round(arrow.height)]).toEqual([
|
||||||
|
93, 81,
|
||||||
|
]);
|
||||||
|
|
||||||
renderInteractiveScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
renderStaticScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
@ -128,8 +130,11 @@ describe("move element", () => {
|
|||||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||||
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
||||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[107.07, 47.07]]);
|
|
||||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[86.86, 87.3]]);
|
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[103.53, 50.01]]);
|
||||||
|
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([
|
||||||
|
[93.9289, 82.1813],
|
||||||
|
]);
|
||||||
|
|
||||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
@ -118,8 +118,8 @@ describe("multi point mode in linear elements", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
@ -161,8 +161,8 @@ describe("multi point mode in linear elements", () => {
|
|||||||
fireEvent.keyDown(document, {
|
fireEvent.keyDown(document, {
|
||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
@ -3,6 +3,8 @@ import { expect } from "vitest";
|
|||||||
|
|
||||||
import { reseed } from "@excalidraw/common";
|
import { reseed } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import "@excalidraw/utils/test-utils";
|
||||||
|
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
|
|
||||||
import { UI } from "./helpers/ui";
|
import { UI } from "./helpers/ui";
|
||||||
@ -35,7 +37,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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -71,14 +73,16 @@ test("unselected bound arrows update when rotating their target elements", async
|
|||||||
expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id);
|
expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id);
|
||||||
expect(ellipseArrow.x).toEqual(0);
|
expect(ellipseArrow.x).toEqual(0);
|
||||||
expect(ellipseArrow.y).toEqual(0);
|
expect(ellipseArrow.y).toEqual(0);
|
||||||
expect(ellipseArrow.points[0]).toEqual([0, 0]);
|
|
||||||
expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1);
|
expect(ellipseArrow.points).toCloselyEqualPoints([
|
||||||
expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1);
|
[0, 0],
|
||||||
|
[90.1827, 98.5896],
|
||||||
|
]);
|
||||||
|
|
||||||
expect(textArrow.endBinding?.elementId).toEqual(text.id);
|
expect(textArrow.endBinding?.elementId).toEqual(text.id);
|
||||||
expect(textArrow.x).toEqual(360);
|
expect(textArrow.x).toEqual(360);
|
||||||
expect(textArrow.y).toEqual(300);
|
expect(textArrow.y).toEqual(300);
|
||||||
expect(textArrow.points[0]).toEqual([0, 0]);
|
expect(textArrow.points[0]).toEqual([0, 0]);
|
||||||
expect(textArrow.points[1][0]).toBeCloseTo(-94, 0);
|
expect(textArrow.points[1][0]).toBeCloseTo(-95, 0);
|
||||||
expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0);
|
expect(textArrow.points[1][1]).toBeCloseTo(-129.1, 0);
|
||||||
});
|
});
|
||||||
|
@ -157,22 +157,13 @@ export function curveIntersectLineSegment<
|
|||||||
return bezierEquation(c, t);
|
return bezierEquation(c, t);
|
||||||
};
|
};
|
||||||
|
|
||||||
let solution = calculate(initial_guesses[0]);
|
const solutions = [
|
||||||
if (solution) {
|
calculate(initial_guesses[0]),
|
||||||
return [solution];
|
calculate(initial_guesses[1]),
|
||||||
}
|
calculate(initial_guesses[2]),
|
||||||
|
].filter((x, i, a): x is Point => x !== null && a.indexOf(x) === i);
|
||||||
|
|
||||||
solution = calculate(initial_guesses[1]);
|
return solutions;
|
||||||
if (solution) {
|
|
||||||
return [solution];
|
|
||||||
}
|
|
||||||
|
|
||||||
solution = calculate(initial_guesses[2]);
|
|
||||||
if (solution) {
|
|
||||||
return [solution];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,9 +91,10 @@ export function isPoint(p: unknown): p is LocalPoint | GlobalPoint {
|
|||||||
export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
|
export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
|
||||||
a: Point,
|
a: Point,
|
||||||
b: Point,
|
b: Point,
|
||||||
|
precision: number = PRECISION,
|
||||||
): boolean {
|
): boolean {
|
||||||
const abs = Math.abs;
|
const abs = Math.abs;
|
||||||
return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION;
|
return abs(a[0] - b[0]) < precision && abs(a[1] - b[1]) < precision;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,7 +6,7 @@ export const clamp = (value: number, min: number, max: number) => {
|
|||||||
|
|
||||||
export const round = (
|
export const round = (
|
||||||
value: number,
|
value: number,
|
||||||
precision: number,
|
precision: number = (Math.log(1 / PRECISION) * Math.LOG10E + 1) | 0,
|
||||||
func: "round" | "floor" | "ceil" = "round",
|
func: "round" | "floor" | "ceil" = "round",
|
||||||
) => {
|
) => {
|
||||||
const multiplier = Math.pow(10, precision);
|
const multiplier = Math.pow(10, precision);
|
||||||
|
@ -195,11 +195,6 @@ export const getEllipseShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCurvePathOps = (shape: Drawable): Op[] => {
|
export const getCurvePathOps = (shape: Drawable): Op[] => {
|
||||||
// NOTE (mtolmacs): Temporary fix for extremely large elements
|
|
||||||
if (!shape) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const set of shape.sets) {
|
for (const set of shape.sets) {
|
||||||
if (set.type === "path") {
|
if (set.type === "path") {
|
||||||
return set.ops;
|
return set.ops;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user