Compare commits
51 Commits
master
...
mtolmacs/a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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,
|
||||||
|
@ -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;
|
||||||
@ -9071,66 +8904,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 +9978,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 +10464,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": {
|
||||||
@ -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": {
|
||||||
|
@ -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",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -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());
|
||||||
});
|
});
|
||||||
|
@ -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