mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
Compare commits
53 commits
159d116c8b
...
b9320ed2a8
Author | SHA1 | Date | |
---|---|---|---|
|
b9320ed2a8 | ||
|
a5a74be45d | ||
|
3f9c6299a0 | ||
|
3068787ac4 | ||
|
c2b78346c1 | ||
|
44df764a88 | ||
|
cc01e16e52 | ||
|
6b9fa5bcc5 | ||
|
06b3750a2f | ||
|
c3924a8f8c | ||
|
eecabccf8d | ||
|
ccda36a0e3 | ||
|
37653484a1 | ||
|
b2e19055bf | ||
|
3d40221dc1 | ||
|
5cc5c626df | ||
|
9a599cfc05 | ||
|
88d4c4fe8d | ||
|
4efa6f69e5 | ||
|
a79eb06939 | ||
|
bcbd418154 | ||
|
db9e501d35 | ||
|
ce10087edc | ||
|
76a782bd52 | ||
|
d6d4d00f60 | ||
|
4ea534a732 | ||
|
e90350b7d1 | ||
|
ea5ad1412c | ||
|
c8ade51b53 | ||
|
6e520fdbb9 | ||
|
fdd7420e65 | ||
|
f4abdc751e | ||
|
946d3ddf87 | ||
|
fbde68c849 | ||
|
4ee99de2fb | ||
|
4d1e2c2bbb | ||
|
1a87aa8e55 | ||
|
528e6aa2df | ||
|
8c9666b8ab | ||
|
8ac508af11 | ||
|
cbe6705c98 | ||
|
1819661828 | ||
|
373b940e75 | ||
|
2f02d72741 | ||
|
a54322a34f | ||
|
5c1fc2f4fb | ||
|
63d53fc242 | ||
|
e1812c4c91 | ||
|
e459ea0cc7 | ||
|
f354285d69 | ||
|
03b91deb4a | ||
|
dca9fbe306 | ||
|
f363fcabd8 |
46 changed files with 1776 additions and 1410 deletions
|
@ -18,7 +18,7 @@ import {
|
|||
} from "@excalidraw/math";
|
||||
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";
|
||||
|
||||
|
|
|
@ -9,3 +9,4 @@ export * from "./promise-pool";
|
|||
export * from "./random";
|
||||
export * from "./url";
|
||||
export * from "./utils";
|
||||
export * from "./visualdebug";
|
||||
|
|
|
@ -437,26 +437,12 @@ export const _generateElementShape = (
|
|||
: [pointFrom<LocalPoint>(0, 0)];
|
||||
|
||||
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 = [
|
||||
generator.path(
|
||||
generateElbowArrowShape(points, 16),
|
||||
generateRoughOptions(element, true),
|
||||
),
|
||||
];
|
||||
}
|
||||
shape = [
|
||||
generator.path(
|
||||
generateElbowArrowShape(points, 16),
|
||||
generateRoughOptions(element, true),
|
||||
),
|
||||
];
|
||||
} else if (!element.roundness) {
|
||||
// curve is always the first element
|
||||
// this simplifies finding the curve for an element
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
PRECISION,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { isPointOnShape } from "@excalidraw/utils/collision";
|
||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
|
@ -81,11 +81,10 @@ import type {
|
|||
NonDeletedSceneElementsMap,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawArrowElement,
|
||||
OrderedExcalidrawElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
FixedPoint,
|
||||
SceneElementsMap,
|
||||
FixedPointBinding,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
|
||||
export type SuggestedBinding =
|
||||
|
@ -108,6 +107,7 @@ export const isBindingEnabled = (appState: AppState): boolean => {
|
|||
return appState.isBindingEnabled;
|
||||
};
|
||||
|
||||
export const INSIDE_BINDING_BAND_PERCENT = 0.1;
|
||||
export const FIXED_BINDING_DISTANCE = 5;
|
||||
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
||||
export const BINDING_HIGHLIGHT_OFFSET = 4;
|
||||
|
@ -223,43 +223,33 @@ const bindOrUnbindLinearElementEdge = (
|
|||
}
|
||||
};
|
||||
|
||||
const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
edge: "start" | "end",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
): NonDeleted<ExcalidrawElement> | null => {
|
||||
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
||||
const elementId =
|
||||
edge === "start"
|
||||
? linearElement.startBinding?.elementId
|
||||
: linearElement.endBinding?.elementId;
|
||||
if (elementId) {
|
||||
const element = elementsMap.get(elementId);
|
||||
if (
|
||||
isBindableElement(element) &&
|
||||
bindingBorderTest(element, coors, elementsMap, zoom)
|
||||
) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
||||
export const getOriginalBindingsIfStillCloseToArrowEnds = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
||||
["start", "end"].map((edge) =>
|
||||
getOriginalBindingIfStillCloseOfLinearElementEdge(
|
||||
["start", "end"].map((edge) => {
|
||||
const coors = getLinearElementEdgeCoors(
|
||||
linearElement,
|
||||
edge as "start" | "end",
|
||||
elementsMap,
|
||||
zoom,
|
||||
),
|
||||
);
|
||||
);
|
||||
const elementId =
|
||||
edge === "start"
|
||||
? linearElement.startBinding?.elementId
|
||||
: linearElement.endBinding?.elementId;
|
||||
if (elementId) {
|
||||
const element = elementsMap.get(elementId);
|
||||
if (
|
||||
isBindableElement(element) &&
|
||||
bindingBorderTest(element, coors, elementsMap, zoom)
|
||||
) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const getBindingStrategyForDraggingArrowEndpoints = (
|
||||
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
|
@ -275,7 +265,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
||||
const start = startDragged
|
||||
? isBindingEnabled
|
||||
? getElligibleElementForBindingElement(
|
||||
? getEligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
|
@ -285,7 +275,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||
: null // If binding is disabled and start is dragged, break all binds
|
||||
: !isElbowArrow(selectedElement)
|
||||
? // We have to update the focus and gap of the binding, so let's rebind
|
||||
getElligibleElementForBindingElement(
|
||||
getEligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
|
@ -295,7 +285,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||
: "keep";
|
||||
const end = endDragged
|
||||
? isBindingEnabled
|
||||
? getElligibleElementForBindingElement(
|
||||
? getEligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
|
@ -305,7 +295,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||
: null // If binding is disabled and end is dragged, break all binds
|
||||
: !isElbowArrow(selectedElement)
|
||||
? // We have to update the focus and gap of the binding, so let's rebind
|
||||
getElligibleElementForBindingElement(
|
||||
getEligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
|
@ -336,7 +326,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||
);
|
||||
const start = startIsClose
|
||||
? isBindingEnabled
|
||||
? getElligibleElementForBindingElement(
|
||||
? getEligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
|
@ -347,7 +337,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||
: null;
|
||||
const end = endIsClose
|
||||
? isBindingEnabled
|
||||
? getElligibleElementForBindingElement(
|
||||
? getEligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
|
@ -428,10 +418,47 @@ export const getSuggestedBindingsForArrows = (
|
|||
export const maybeBindLinearElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
pointerCoords: { x: number; y: number },
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
): 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) {
|
||||
bindLinearElement(
|
||||
linearElement,
|
||||
|
@ -441,15 +468,6 @@ export const maybeBindLinearElement = (
|
|||
);
|
||||
}
|
||||
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
pointerCoords,
|
||||
elements,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
isElbowArrow(linearElement),
|
||||
isElbowArrow(linearElement),
|
||||
);
|
||||
|
||||
if (hoveredElement !== null) {
|
||||
if (
|
||||
!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 = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
|
@ -493,17 +491,25 @@ export const bindLinearElement = (
|
|||
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 = {
|
||||
elementId: hoveredElement.id,
|
||||
...normalizePointBinding(
|
||||
calculateFocusAndGap(
|
||||
linearElement,
|
||||
hoveredElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
),
|
||||
hoveredElement,
|
||||
),
|
||||
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
};
|
||||
|
||||
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, {
|
||||
points,
|
||||
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
||||
});
|
||||
|
||||
|
@ -706,33 +730,6 @@ const getAllElementsAtPositionForBinding = (
|
|||
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
|
||||
// linear elements.
|
||||
// Because scaling involves moving the focus points as well, it is
|
||||
|
@ -743,11 +740,9 @@ export const updateBoundElements = (
|
|||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||
},
|
||||
) => {
|
||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||
const { simultaneouslyUpdated } = options ?? {};
|
||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||
simultaneouslyUpdated,
|
||||
);
|
||||
|
@ -781,22 +776,13 @@ export const updateBoundElements = (
|
|||
endBounds = getElementBounds(endBindingElement, elementsMap);
|
||||
}
|
||||
|
||||
const bindings = {
|
||||
startBinding: maybeCalculateNewGapWhenScaling(
|
||||
changedElement,
|
||||
element.startBinding,
|
||||
newSize,
|
||||
),
|
||||
endBinding: maybeCalculateNewGapWhenScaling(
|
||||
changedElement,
|
||||
element.endBinding,
|
||||
newSize,
|
||||
),
|
||||
};
|
||||
|
||||
// `linearElement` is being moved/scaled already, just update the binding
|
||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||
mutateElement(element, bindings, true);
|
||||
mutateElement(
|
||||
element,
|
||||
{ startBinding: element.startBinding, endBinding: element.endBinding },
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -818,7 +804,9 @@ export const updateBoundElements = (
|
|||
const point = updateBoundPoint(
|
||||
element,
|
||||
bindingProp,
|
||||
bindings[bindingProp],
|
||||
bindingProp === "startBinding"
|
||||
? element.startBinding
|
||||
: element.endBinding,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
|
@ -848,10 +836,10 @@ export const updateBoundElements = (
|
|||
updates,
|
||||
{
|
||||
...(changedElement.id === element.startBinding?.elementId
|
||||
? { startBinding: bindings.startBinding }
|
||||
? { startBinding: element.startBinding }
|
||||
: {}),
|
||||
...(changedElement.id === element.endBinding?.elementId
|
||||
? { endBinding: bindings.endBinding }
|
||||
? { endBinding: element.endBinding }
|
||||
: {}),
|
||||
},
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
|
@ -885,7 +873,6 @@ export const getHeadingForElbowArrowSnap = (
|
|||
otherPoint: Readonly<GlobalPoint>,
|
||||
bindableElement: ExcalidrawBindableElement | undefined | null,
|
||||
aabb: Bounds | undefined | null,
|
||||
elementsMap: ElementsMap,
|
||||
origPoint: GlobalPoint,
|
||||
zoom?: AppState["zoom"],
|
||||
): Heading => {
|
||||
|
@ -895,12 +882,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||
return otherPointHeading;
|
||||
}
|
||||
|
||||
const distance = getDistanceForBinding(
|
||||
origPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
zoom,
|
||||
);
|
||||
const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
|
||||
|
||||
if (!distance) {
|
||||
return vectorToHeading(
|
||||
|
@ -920,7 +902,6 @@ export const getHeadingForElbowArrowSnap = (
|
|||
const getDistanceForBinding = (
|
||||
point: Readonly<GlobalPoint>,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
) => {
|
||||
const distance = distanceToBindableElement(bindableElement, point);
|
||||
|
@ -935,40 +916,54 @@ const getDistanceForBinding = (
|
|||
};
|
||||
|
||||
export const bindPointToSnapToElementOutline = (
|
||||
arrow: ExcalidrawElbowArrowElement,
|
||||
linearElement: ExcalidrawLinearElement,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
): GlobalPoint => {
|
||||
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 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 adjacentPointIdx = startOrEnd === "start" ? 1 : arrow.points.length - 2;
|
||||
const adjacentPoint = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
arrow.x + arrow.points[adjacentPointIdx][0],
|
||||
arrow.y + arrow.points[adjacentPointIdx][1],
|
||||
),
|
||||
center,
|
||||
arrow.angle ?? 0,
|
||||
|
||||
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>(
|
||||
linearElement.x + linearElement.points[adjacentPointIdx][0],
|
||||
linearElement.y + linearElement.points[adjacentPointIdx][1],
|
||||
),
|
||||
center,
|
||||
linearElement.angle ?? 0,
|
||||
);
|
||||
|
||||
let intersection: GlobalPoint | null = null;
|
||||
if (elbowed) {
|
||||
const isHorizontal = headingIsHorizontal(
|
||||
headingForPointFromElement(bindableElement, aabb, globalP),
|
||||
headingForPointFromElement(bindableElement, aabb, p),
|
||||
);
|
||||
const otherPoint = pointFrom<GlobalPoint>(
|
||||
isHorizontal ? center[0] : edgePoint[0],
|
||||
|
@ -1016,6 +1011,14 @@ export const bindPointToSnapToElementOutline = (
|
|||
return edgePoint;
|
||||
}
|
||||
|
||||
const shape = getElementShape(bindableElement, elementsMap);
|
||||
const pointInShape = isPointInShape(edgePoint, shape);
|
||||
const otherPointInShape = isPointInShape(otherPoint, shape);
|
||||
|
||||
if (pointInShape && otherPointInShape) {
|
||||
return edgePoint;
|
||||
}
|
||||
|
||||
if (elbowed) {
|
||||
const scalar =
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -1040,6 +1065,10 @@ export const avoidRectangularCorner = (
|
|||
element: ExcalidrawBindableElement,
|
||||
p: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
if (!isRectanguloidElement(element)) {
|
||||
return p;
|
||||
}
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
|
@ -1200,6 +1229,45 @@ export const snapToMid = (
|
|||
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 = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "startBinding" | "endBinding",
|
||||
|
@ -1263,66 +1331,58 @@ const updateBoundPoint = (
|
|||
|
||||
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) {
|
||||
const edgePointAbsolute =
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
linearElement,
|
||||
edgePointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const interceptorLength =
|
||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||
pointDistance(adjacentPoint, center) +
|
||||
Math.max(bindableElement.width, bindableElement.height) * 2;
|
||||
const intersections = [
|
||||
...intersectElementWithLineSegment(
|
||||
bindableElement,
|
||||
lineSegment<GlobalPoint>(
|
||||
adjacentPoint,
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)),
|
||||
interceptorLength,
|
||||
),
|
||||
adjacentPoint,
|
||||
),
|
||||
),
|
||||
FIXED_BINDING_DISTANCE,
|
||||
).sort(
|
||||
(g, h) =>
|
||||
pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint),
|
||||
),
|
||||
// Fallback when arrow doesn't point to the shape
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)),
|
||||
pointDistance(adjacentPoint, edgePointAbsolute),
|
||||
),
|
||||
adjacentPoint,
|
||||
),
|
||||
];
|
||||
|
||||
if (intersections.length > 1) {
|
||||
// The adjacent point is outside the shape (+ gap)
|
||||
newEdgePoint = intersections[0];
|
||||
} else if (intersections.length === 1) {
|
||||
// The adjacent point is inside the shape (+ gap)
|
||||
newEdgePoint = focusPointAbsolute;
|
||||
} else {
|
||||
const edgePointAbsolute =
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
linearElement,
|
||||
edgePointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const interceptorLength =
|
||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||
pointDistance(adjacentPoint, center) +
|
||||
Math.max(bindableElement.width, bindableElement.height) * 2;
|
||||
const intersections = [
|
||||
...intersectElementWithLineSegment(
|
||||
bindableElement,
|
||||
lineSegment<GlobalPoint>(
|
||||
adjacentPoint,
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(
|
||||
vectorFromPoint(focusPointAbsolute, adjacentPoint),
|
||||
),
|
||||
interceptorLength,
|
||||
),
|
||||
adjacentPoint,
|
||||
),
|
||||
),
|
||||
binding.gap,
|
||||
).sort(
|
||||
(g, h) =>
|
||||
pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint),
|
||||
),
|
||||
// Fallback when arrow doesn't point to the shape
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)),
|
||||
pointDistance(adjacentPoint, edgePointAbsolute),
|
||||
),
|
||||
adjacentPoint,
|
||||
),
|
||||
];
|
||||
|
||||
if (intersections.length > 1) {
|
||||
// The adjacent point is outside the shape (+ gap)
|
||||
newEdgePoint = intersections[0];
|
||||
} else if (intersections.length === 1) {
|
||||
// The adjacent point is inside the shape (+ gap)
|
||||
newEdgePoint = focusPointAbsolute;
|
||||
} else {
|
||||
// Shouldn't happend, but just in case
|
||||
newEdgePoint = edgePointAbsolute;
|
||||
}
|
||||
// Shouldn't happend, but just in case
|
||||
newEdgePoint = edgePointAbsolute;
|
||||
}
|
||||
|
||||
return LinearElementEditor.pointFromAbsoluteCoords(
|
||||
|
@ -1333,7 +1393,7 @@ const updateBoundPoint = (
|
|||
};
|
||||
|
||||
export const calculateFixedPointForElbowArrowBinding = (
|
||||
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
||||
linearElement: NonDeleted<ExcalidrawArrowElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
|
@ -1348,6 +1408,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||
linearElement,
|
||||
hoveredElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
);
|
||||
const globalMidPoint = pointFrom(
|
||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||
|
@ -1369,29 +1430,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||
};
|
||||
};
|
||||
|
||||
const maybeCalculateNewGapWhenScaling = (
|
||||
changedElement: ExcalidrawBindableElement,
|
||||
currentBinding: PointBinding | null | undefined,
|
||||
newSize: { width: number; height: number } | undefined,
|
||||
): PointBinding | null | undefined => {
|
||||
if (currentBinding == null || newSize == null) {
|
||||
return currentBinding;
|
||||
}
|
||||
const { width: newWidth, height: newHeight } = newSize;
|
||||
const { width, height } = changedElement;
|
||||
const newGap = Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
maxBindingGap(changedElement, newWidth, newHeight),
|
||||
currentBinding.gap *
|
||||
(newWidth < newHeight ? newWidth / width : newHeight / height),
|
||||
),
|
||||
);
|
||||
|
||||
return { ...currentBinding, gap: newGap };
|
||||
};
|
||||
|
||||
const getElligibleElementForBindingElement = (
|
||||
const getEligibleElementForBindingElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
tupleToCoors,
|
||||
getSizeFromPoints,
|
||||
isDevEnv,
|
||||
isTestEnv,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
@ -50,7 +51,6 @@ import { isBindableElement } from "./typeChecks";
|
|||
import {
|
||||
type ExcalidrawElbowArrowElement,
|
||||
type NonDeletedSceneElementsMap,
|
||||
type SceneElementsMap,
|
||||
} from "./types";
|
||||
|
||||
import { aabbForElement, pointInsideBounds } from "./shapes";
|
||||
|
@ -63,7 +63,6 @@ import type {
|
|||
ExcalidrawBindableElement,
|
||||
FixedPointBinding,
|
||||
FixedSegment,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
// NOTE (mtolmacs): This is a temporary check to ensure that the incoming elbow
|
||||
// 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) {
|
||||
if (isDevEnv() || isTestEnv()) {
|
||||
invariant(
|
||||
!updates.points || updates.points.length >= 2,
|
||||
"Updated point array length must match the arrow point length, contain " +
|
||||
|
@ -1221,19 +1174,31 @@ const getElbowArrowData = (
|
|||
if (options?.isDragging) {
|
||||
const elements = Array.from(elementsMap.values());
|
||||
hoveredStartElement =
|
||||
getHoveredElement(
|
||||
origStartGlobalPoint,
|
||||
elementsMap,
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(origStartGlobalPoint),
|
||||
elements,
|
||||
elementsMap,
|
||||
options?.zoom,
|
||||
true,
|
||||
true,
|
||||
) || null;
|
||||
hoveredEndElement =
|
||||
getHoveredElement(
|
||||
origEndGlobalPoint,
|
||||
elementsMap,
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(origEndGlobalPoint),
|
||||
elements,
|
||||
elementsMap,
|
||||
options?.zoom,
|
||||
true,
|
||||
true,
|
||||
) || 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 {
|
||||
hoveredStartElement = arrow.startBinding
|
||||
? getBindableElementForId(arrow.startBinding.elementId, elementsMap) ||
|
||||
|
@ -1254,6 +1219,7 @@ const getElbowArrowData = (
|
|||
"start",
|
||||
arrow.startBinding?.fixedPoint,
|
||||
origStartGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredStartElement,
|
||||
options?.isDragging,
|
||||
);
|
||||
|
@ -1267,20 +1233,19 @@ const getElbowArrowData = (
|
|||
"end",
|
||||
arrow.endBinding?.fixedPoint,
|
||||
origEndGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredEndElement,
|
||||
options?.isDragging,
|
||||
);
|
||||
const startHeading = getBindPointHeading(
|
||||
startGlobalPoint,
|
||||
endGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredStartElement,
|
||||
origStartGlobalPoint,
|
||||
);
|
||||
const endHeading = getBindPointHeading(
|
||||
endGlobalPoint,
|
||||
startGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredEndElement,
|
||||
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]) =>
|
||||
pointFrom<LocalPoint>(clamp(x, -1e6, 1e6), clamp(y, -1e6, 1e6)),
|
||||
);
|
||||
|
@ -2212,6 +2154,7 @@ const getGlobalPoint = (
|
|||
startOrEnd: "start" | "end",
|
||||
fixedPointRatio: [number, number] | undefined | null,
|
||||
initialPoint: GlobalPoint,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
element?: ExcalidrawBindableElement | null,
|
||||
isDragging?: boolean,
|
||||
): GlobalPoint => {
|
||||
|
@ -2221,6 +2164,7 @@ const getGlobalPoint = (
|
|||
arrow,
|
||||
element,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
return snapToMid(element, snapPoint);
|
||||
|
@ -2240,7 +2184,7 @@ const getGlobalPoint = (
|
|||
distanceToBindableElement(element, fixedGlobalPoint) -
|
||||
FIXED_BINDING_DISTANCE,
|
||||
) > 0.01
|
||||
? bindPointToSnapToElementOutline(arrow, element, startOrEnd)
|
||||
? bindPointToSnapToElementOutline(arrow, element, startOrEnd, elementsMap)
|
||||
: fixedGlobalPoint;
|
||||
}
|
||||
|
||||
|
@ -2250,7 +2194,6 @@ const getGlobalPoint = (
|
|||
const getBindPointHeading = (
|
||||
p: GlobalPoint,
|
||||
otherPoint: GlobalPoint,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||
origPoint: GlobalPoint,
|
||||
): Heading =>
|
||||
|
@ -2268,26 +2211,9 @@ const getBindPointHeading = (
|
|||
number,
|
||||
],
|
||||
),
|
||||
elementsMap,
|
||||
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 =>
|
||||
a[0] === b[0] && a[1] === b[1];
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ import type { Mutable } from "@excalidraw/common/utility-types";
|
|||
import {
|
||||
bindOrUnbindLinearElement,
|
||||
getHoveredElementForBinding,
|
||||
getOutlineAvoidingPoint,
|
||||
isBindingEnabled,
|
||||
} from "./binding";
|
||||
import {
|
||||
|
@ -56,6 +57,7 @@ import { headingIsHorizontal, vectorToHeading } from "./heading";
|
|||
import { bumpVersion, mutateElement } from "./mutateElement";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBindingElement,
|
||||
isElbowArrow,
|
||||
isFixedPointBinding,
|
||||
|
@ -252,27 +254,28 @@ export class LinearElementEditor {
|
|||
pointSceneCoords: { x: number; y: number }[],
|
||||
) => void,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
scene: Scene,
|
||||
): LinearElementEditor | null {
|
||||
if (!linearElementEditor) {
|
||||
return null;
|
||||
}
|
||||
const { elementId } = linearElementEditor;
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elbowed = isElbowArrow(element);
|
||||
|
||||
if (
|
||||
isElbowArrow(element) &&
|
||||
elbowed &&
|
||||
!linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
|
||||
linearElementEditor.pointerDownState.lastClickedPoint !== 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedPointsIndices = isElbowArrow(element)
|
||||
const selectedPointsIndices = elbowed
|
||||
? [
|
||||
!!linearElementEditor.selectedPointsIndices?.includes(0)
|
||||
? 0
|
||||
|
@ -282,7 +285,7 @@ export class LinearElementEditor {
|
|||
: undefined,
|
||||
].filter((idx): idx is number => idx !== undefined)
|
||||
: linearElementEditor.selectedPointsIndices;
|
||||
const lastClickedPoint = isElbowArrow(element)
|
||||
const lastClickedPoint = elbowed
|
||||
? linearElementEditor.pointerDownState.lastClickedPoint > 0
|
||||
? element.points.length - 1
|
||||
: 0
|
||||
|
@ -334,7 +337,7 @@ export class LinearElementEditor {
|
|||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
selectedPointsIndices.map((pointIndex) => {
|
||||
const newPointPosition: LocalPoint =
|
||||
let newPointPosition: LocalPoint =
|
||||
pointIndex === lastClickedPoint
|
||||
? LinearElementEditor.createPointAt(
|
||||
element,
|
||||
|
@ -347,6 +350,46 @@ export class LinearElementEditor {
|
|||
element.points[pointIndex][0] + deltaX,
|
||||
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 {
|
||||
index: pointIndex,
|
||||
point: newPointPosition,
|
||||
|
@ -426,6 +469,7 @@ export class LinearElementEditor {
|
|||
editingLinearElement: LinearElementEditor,
|
||||
appState: AppState,
|
||||
scene: Scene,
|
||||
shouldBind?: boolean,
|
||||
): LinearElementEditor {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
|
@ -488,6 +532,19 @@ export class LinearElementEditor {
|
|||
}
|
||||
}
|
||||
|
||||
if (shouldBind) {
|
||||
const element = scene.getElement(editingLinearElement.elementId);
|
||||
if (isBindingElement(element) && isBindingEnabled(appState)) {
|
||||
bindOrUnbindLinearElement(
|
||||
element,
|
||||
bindings.startBindingElement || "keep",
|
||||
bindings.endBindingElement || "keep",
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...editingLinearElement,
|
||||
...bindings,
|
||||
|
|
|
@ -98,28 +98,6 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||
...rest
|
||||
}: 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
|
||||
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
||||
id: rest.id || randomId(),
|
||||
|
|
|
@ -969,10 +969,7 @@ export const resizeSingleElement = (
|
|||
|
||||
mutateElement(latestElement, updates, shouldInformMutation);
|
||||
|
||||
updateBoundElements(latestElement, elementsMap as SceneElementsMap, {
|
||||
// TODO: confirm with MARK if this actually makes sense
|
||||
newSize: { width: nextWidth, height: nextHeight },
|
||||
});
|
||||
updateBoundElements(latestElement, elementsMap as SceneElementsMap);
|
||||
|
||||
if (boundTextElement && boundTextFont != null) {
|
||||
mutateElement(boundTextElement, {
|
||||
|
@ -1525,7 +1522,7 @@ export const resizeMultipleElements = (
|
|||
element,
|
||||
update: { boundTextFontSize, ...update },
|
||||
} of elementsAndUpdates) {
|
||||
const { width, height, angle } = update;
|
||||
const { angle } = update;
|
||||
|
||||
mutateElement(element, update, false, {
|
||||
// needed for the fixed binding point udpate to take effect
|
||||
|
@ -1534,7 +1531,6 @@ export const resizeMultipleElements = (
|
|||
|
||||
updateBoundElements(element, elementsMap as SceneElementsMap, {
|
||||
simultaneouslyUpdated: elementsToUpdate,
|
||||
newSize: { width, height },
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
|
|
@ -130,7 +130,7 @@ export const isLinearElementType = (
|
|||
export const isBindingElement = (
|
||||
element?: ExcalidrawElement | null,
|
||||
includeLocked = true,
|
||||
): element is ExcalidrawLinearElement => {
|
||||
): element is ExcalidrawArrowElement => {
|
||||
return (
|
||||
element != null &&
|
||||
(!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 { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
|
||||
|
||||
import "@excalidraw/utils/test-utils";
|
||||
|
||||
import { getTransformHandles } from "../src/transformHandles";
|
||||
|
||||
const { h } = window;
|
||||
|
@ -18,7 +20,9 @@ const mouse = new Pointer("mouse");
|
|||
|
||||
describe("element binding", () => {
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
mouse.reset();
|
||||
});
|
||||
|
||||
it("should create valid binding if duplicate start/end points", async () => {
|
||||
|
@ -89,46 +93,55 @@ describe("element binding", () => {
|
|||
});
|
||||
});
|
||||
|
||||
//@TODO fix the test with rotation
|
||||
it.skip("rotation of arrow should rebind both ends", () => {
|
||||
const rectLeft = UI.createElement("rectangle", {
|
||||
x: 0,
|
||||
width: 200,
|
||||
height: 500,
|
||||
});
|
||||
const rectRight = UI.createElement("rectangle", {
|
||||
x: 400,
|
||||
width: 200,
|
||||
height: 500,
|
||||
});
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: 210,
|
||||
y: 250,
|
||||
width: 180,
|
||||
height: 1,
|
||||
});
|
||||
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
||||
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
||||
// UX RATIONALE: We are not aware of any use-case where the user would want to
|
||||
// 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", {
|
||||
x: 0,
|
||||
width: 200,
|
||||
height: 500,
|
||||
});
|
||||
const rectRight = UI.createElement("rectangle", {
|
||||
x: 400,
|
||||
width: 200,
|
||||
height: 500,
|
||||
});
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: 210,
|
||||
y: 250,
|
||||
width: 180,
|
||||
height: 1,
|
||||
});
|
||||
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
||||
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
||||
|
||||
const rotation = getTransformHandles(
|
||||
arrow,
|
||||
h.state.zoom,
|
||||
arrayToMap(h.elements),
|
||||
"mouse",
|
||||
).rotation!;
|
||||
const rotationHandleX = rotation[0] + rotation[2] / 2;
|
||||
const rotationHandleY = rotation[1] + rotation[3] / 2;
|
||||
mouse.down(rotationHandleX, rotationHandleY);
|
||||
mouse.move(300, 400);
|
||||
mouse.up();
|
||||
expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
|
||||
expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
|
||||
expect(arrow.startBinding?.elementId).toBe(rectRight.id);
|
||||
expect(arrow.endBinding?.elementId).toBe(rectLeft.id);
|
||||
});
|
||||
const rotation = getTransformHandles(
|
||||
arrow,
|
||||
h.state.zoom,
|
||||
arrayToMap(h.elements),
|
||||
"mouse",
|
||||
).rotation!;
|
||||
const rotationHandleX = rotation[0] + rotation[2] / 2;
|
||||
const rotationHandleY = rotation[1] + rotation[3] / 2;
|
||||
mouse.down(rotationHandleX, rotationHandleY);
|
||||
mouse.move(300, 400);
|
||||
mouse.up();
|
||||
expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
|
||||
expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
|
||||
expect(arrow.startBinding).toBe(null);
|
||||
expect(arrow.endBinding).toBe(null);
|
||||
},
|
||||
);
|
||||
|
||||
// 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 by clicking on element A should end up selecting A",
|
||||
async () => {
|
||||
|
@ -142,7 +155,10 @@ describe("element binding", () => {
|
|||
mouse.up(0, 80);
|
||||
|
||||
// Edit arrow with multi-point
|
||||
mouse.doubleClick();
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
mouse.doubleClick();
|
||||
});
|
||||
|
||||
// move arrow head
|
||||
mouse.down();
|
||||
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
|
||||
mouse.reset();
|
||||
expect(h.state.editingLinearElement).not.toBe(null);
|
||||
mouse.down(0, 0);
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
expect(h.state.editingLinearElement).toBe(null);
|
||||
expect(API.getSelectedElement().type).toBe("rectangle");
|
||||
mouse.up();
|
||||
mouse.click();
|
||||
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", {
|
||||
x: 75,
|
||||
y: 0,
|
||||
|
@ -187,13 +199,19 @@ describe("element binding", () => {
|
|||
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
||||
expect(API.getSelectedElement().type).toBe("arrow");
|
||||
|
||||
// Sever connection
|
||||
expect(API.getSelectedElement().type).toBe("arrow");
|
||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
||||
expect(arrow.endBinding).toBe(null);
|
||||
Keyboard.keyPress(KEYS.ARROW_RIGHT);
|
||||
expect(arrow.endBinding).toBe(null);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
||||
expect(arrow.x).toBe(0);
|
||||
expect(arrow.y).toBe(0);
|
||||
});
|
||||
|
||||
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 { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
||||
import {
|
||||
actionDuplicateSelection,
|
||||
actionSelectAll,
|
||||
} from "@excalidraw/excalidraw/actions";
|
||||
|
||||
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 {
|
||||
act,
|
||||
|
@ -28,7 +31,10 @@ import type { LocalPoint } from "@excalidraw/math";
|
|||
import { mutateElement } from "../src/mutateElement";
|
||||
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
||||
|
||||
import type { ExcalidrawLinearElement } from "../src/types";
|
||||
import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawLinearElement,
|
||||
} from "../src/types";
|
||||
|
||||
const { h } = window;
|
||||
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", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw />);
|
||||
|
|
|
@ -3,14 +3,11 @@ import { pointFrom } from "@excalidraw/math";
|
|||
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
||||
|
||||
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 { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
GlobalTestState,
|
||||
queryByTestId,
|
||||
|
@ -301,114 +298,4 @@ describe("elbow arrow ui", () => {
|
|||
[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,
|
||||
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||
|
||||
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element/binding";
|
||||
|
||||
import type { LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import { isLinearElement } from "../src/typeChecks";
|
||||
|
@ -195,7 +197,7 @@ describe("generic element", () => {
|
|||
UI.resize(rectangle, "w", [50, 0]);
|
||||
|
||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0);
|
||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(81, 0);
|
||||
});
|
||||
|
||||
it("resizes with a label", async () => {
|
||||
|
@ -826,8 +828,9 @@ describe("image element", () => {
|
|||
UI.resize(image, "nw", [50, 20]);
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
|
@ -1003,14 +1006,14 @@ describe("multiple selection", () => {
|
|||
size: 100,
|
||||
});
|
||||
const leftBoundArrow = UI.createElement("arrow", {
|
||||
x: -110,
|
||||
x: -100 - FIXED_BINDING_DISTANCE,
|
||||
y: 50,
|
||||
width: 100,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
const rightBoundArrow = UI.createElement("arrow", {
|
||||
x: 210,
|
||||
x: 200 + FIXED_BINDING_DISTANCE,
|
||||
y: 50,
|
||||
width: -100,
|
||||
height: 0,
|
||||
|
@ -1031,27 +1034,29 @@ describe("multiple selection", () => {
|
|||
shift: true,
|
||||
});
|
||||
|
||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||
expect(leftBoundArrow.x).toBeCloseTo(-100 - FIXED_BINDING_DISTANCE);
|
||||
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.angle).toEqual(0);
|
||||
expect(leftBoundArrow.startBinding).toBeNull();
|
||||
expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10);
|
||||
expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(5);
|
||||
expect(leftBoundArrow.endBinding?.elementId).toBe(
|
||||
leftArrowBinding.elementId,
|
||||
);
|
||||
expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
||||
|
||||
expect(rightBoundArrow.x).toBeCloseTo(210);
|
||||
expect(rightBoundArrow.x).toBeCloseTo(210 - FIXED_BINDING_DISTANCE);
|
||||
expect(rightBoundArrow.y).toBeCloseTo(
|
||||
(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.angle).toEqual(0);
|
||||
expect(rightBoundArrow.startBinding).toBeNull();
|
||||
expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
|
||||
expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(FIXED_BINDING_DISTANCE);
|
||||
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
||||
rightArrowBinding.elementId,
|
||||
);
|
||||
|
@ -1338,8 +1343,8 @@ describe("multiple selection", () => {
|
|||
|
||||
expect(boundArrow.x).toBeCloseTo(380 * scaleX);
|
||||
expect(boundArrow.y).toBeCloseTo(240 * scaleY);
|
||||
expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX);
|
||||
expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY);
|
||||
expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX - 2, 0);
|
||||
expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY + 2, 0);
|
||||
|
||||
expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo(
|
||||
boundArrow.x + boundArrow.points[1][0] / 2,
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import { pointFrom } from "@excalidraw/math";
|
||||
import { type GlobalPoint, pointFrom } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
maybeBindLinearElement,
|
||||
bindOrUnbindLinearElement,
|
||||
getHoveredElementForBinding,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
isBindingElement,
|
||||
isElbowArrow,
|
||||
isLinearElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { KEYS, arrayToMap, updateActiveTool } from "@excalidraw/common";
|
||||
import { KEYS, updateActiveTool } from "@excalidraw/common";
|
||||
import { isPathALoop } from "@excalidraw/element/shapes";
|
||||
|
||||
import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
|
||||
|
@ -91,10 +93,26 @@ export const actionFinalize = register({
|
|||
multiPointElement.type !== "freedraw" &&
|
||||
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 (
|
||||
!lastCommittedPoint ||
|
||||
points[points.length - 1] !== lastCommittedPoint
|
||||
!hoveredElementForBinding &&
|
||||
(!lastCommittedPoint ||
|
||||
points[points.length - 1] !== lastCommittedPoint)
|
||||
) {
|
||||
mutateElement(multiPointElement, {
|
||||
points: multiPointElement.points.slice(0, -1),
|
||||
|
@ -135,15 +153,9 @@ export const actionFinalize = register({
|
|||
!isLoop &&
|
||||
multiPointElement.points.length > 1
|
||||
) {
|
||||
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
multiPointElement,
|
||||
-1,
|
||||
arrayToMap(elements),
|
||||
);
|
||||
maybeBindLinearElement(
|
||||
multiPointElement,
|
||||
appState,
|
||||
{ x, y },
|
||||
elementsMap,
|
||||
elements,
|
||||
);
|
||||
|
|
|
@ -73,12 +73,12 @@ describe("flipping re-centers selection", () => {
|
|||
API.executeAction(actionFlipHorizontal);
|
||||
|
||||
const rec1 = h.elements.find((el) => el.id === "rec1")!;
|
||||
expect(rec1.x).toBeCloseTo(100, 0);
|
||||
expect(rec1.y).toBeCloseTo(100, 0);
|
||||
expect(Math.floor(rec1.x)).toBeCloseTo(100, 0);
|
||||
expect(Math.floor(rec1.y)).toBeCloseTo(100, 0);
|
||||
|
||||
const rec2 = h.elements.find((el) => el.id === "rec2")!;
|
||||
expect(rec2.x).toBeCloseTo(220, 0);
|
||||
expect(rec2.y).toBeCloseTo(250, 0);
|
||||
expect(Math.floor(rec2.x)).toBeCloseTo(220, 0);
|
||||
expect(Math.floor(rec2.y)).toBeCloseTo(250, 0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -87,6 +87,16 @@ describe("flipping arrowheads", () => {
|
|||
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", () => {
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
|
@ -123,6 +133,7 @@ describe("flipping arrowheads", () => {
|
|||
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||
});
|
||||
|
||||
// UX RATIONALE: See above for the reasoning.
|
||||
it("flipping bound arrow should flip arrowheads only 2", () => {
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
|
@ -164,7 +175,9 @@ describe("flipping arrowheads", () => {
|
|||
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({
|
||||
type: "arrow",
|
||||
id: "arrow1",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
import {
|
||||
bindOrUnbindLinearElements,
|
||||
bindOrUnbindLinearElement,
|
||||
isBindingEnabled,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
|
||||
|
@ -12,15 +12,15 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
|||
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
|
||||
import {
|
||||
isArrowElement,
|
||||
isElbowArrow,
|
||||
isLinearElement,
|
||||
isBindableElement,
|
||||
isBindingElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/frame";
|
||||
import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
|
||||
|
||||
import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
NonDeleted,
|
||||
NonDeletedSceneElementsMap,
|
||||
|
@ -160,52 +160,54 @@ const flipElements = (
|
|||
},
|
||||
);
|
||||
|
||||
bindOrUnbindLinearElements(
|
||||
selectedElements.filter(isLinearElement),
|
||||
elementsMap,
|
||||
app.scene.getNonDeletedElements(),
|
||||
app.scene,
|
||||
isBindingEnabled(appState),
|
||||
[],
|
||||
appState.zoom,
|
||||
const selectedBindables = selectedElements.filter(
|
||||
(e): e is ExcalidrawBindableElement => isBindableElement(e),
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 } =
|
||||
getCommonBoundingBox(selectedElements);
|
||||
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
||||
otherElements.forEach((element) =>
|
||||
|
||||
selectedElements.forEach((element) => {
|
||||
fixBindings(element, selectedBindables, app, elementsMap);
|
||||
|
||||
mutateElement(element, {
|
||||
x: element.x + diffX,
|
||||
y: element.y + diffY,
|
||||
}),
|
||||
);
|
||||
elbowArrows.forEach((element) =>
|
||||
mutateElement(element, {
|
||||
x: element.x + diffX,
|
||||
y: element.y + diffY,
|
||||
}),
|
||||
);
|
||||
// ---------------------------------------------------------------------------
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
startHoveredElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
)
|
||||
: startGlobalPoint;
|
||||
const finalEndPoint = endHoveredElement
|
||||
|
@ -1662,6 +1663,7 @@ export const actionChangeArrowType = register({
|
|||
newElement,
|
||||
endHoveredElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
)
|
||||
: endGlobalPoint;
|
||||
|
||||
|
|
|
@ -1508,9 +1508,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||
) {
|
||||
for (const element of changed.values()) {
|
||||
if (!element.isDeleted && isBindableElement(element)) {
|
||||
updateBoundElements(element, elements, {
|
||||
changedElements: changed,
|
||||
});
|
||||
updateBoundElements(element, elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,11 +113,11 @@ import {
|
|||
fixBindingsAfterDeletion,
|
||||
getHoveredElementForBinding,
|
||||
isBindingEnabled,
|
||||
isLinearElementSimpleAndAlreadyBound,
|
||||
maybeBindLinearElement,
|
||||
shouldEnableBindingForPointerEvent,
|
||||
updateBoundElements,
|
||||
getSuggestedBindingsForArrows,
|
||||
getOutlineAvoidingPoint,
|
||||
} from "@excalidraw/element/binding";
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
@ -170,7 +170,6 @@ import {
|
|||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import {
|
||||
getLockedLinearCursorAlignSize,
|
||||
getNormalizedDimensions,
|
||||
isElementCompletelyInViewport,
|
||||
isElementInViewport,
|
||||
|
@ -302,10 +301,9 @@ import {
|
|||
|
||||
import { isNonDeletedElement } from "@excalidraw/element";
|
||||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawGenericElement,
|
||||
|
@ -329,7 +327,7 @@ import type {
|
|||
ExcalidrawArrowElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||
|
||||
import {
|
||||
actionAddToLibrary,
|
||||
|
@ -464,6 +462,14 @@ import { isMaybeMermaidDefinition } from "../mermaid";
|
|||
|
||||
import { LassoTrail } from "../lasso";
|
||||
|
||||
import {
|
||||
handleCanvasPointerMoveForLinearElement,
|
||||
handleDoubleClickForLinearElement,
|
||||
maybeSuggestBindingsForLinearElementAtCoords,
|
||||
onPointerMoveFromPointerDownOnLinearElement,
|
||||
onPointerUpFromPointerDownOnLinearElementHandler,
|
||||
} from "../linear";
|
||||
|
||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
||||
|
@ -2768,7 +2774,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.updateEmbeddables();
|
||||
const elements = this.scene.getElementsIncludingDeleted();
|
||||
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
||||
const nonDeletedElementsMap = this.scene.getNonDeletedElementsMap();
|
||||
|
||||
if (!this.state.showWelcomeScreen && !elements.length) {
|
||||
this.setState({ showWelcomeScreen: true });
|
||||
|
@ -2925,13 +2930,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
maybeBindLinearElement(
|
||||
multiElement,
|
||||
this.state,
|
||||
tupleToCoors(
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
multiElement,
|
||||
-1,
|
||||
nonDeletedElementsMap,
|
||||
),
|
||||
),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
);
|
||||
|
@ -4381,7 +4379,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const arrowIdsToRemove = new Set<string>();
|
||||
|
||||
selectedElements
|
||||
.filter(isElbowArrow)
|
||||
.filter(isArrowElement)
|
||||
.filter((arrow) => {
|
||||
const startElementNotInSelection =
|
||||
arrow.startBinding &&
|
||||
|
@ -5453,75 +5451,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||
if (
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
(!this.state.editingLinearElement ||
|
||||
this.state.editingLinearElement.elementId !==
|
||||
selectedElements[0].id) &&
|
||||
!isElbowArrow(selectedElements[0])
|
||||
handleDoubleClickForLinearElement(
|
||||
this,
|
||||
this.store,
|
||||
selectedElements[0],
|
||||
event,
|
||||
sceneX,
|
||||
sceneY,
|
||||
)
|
||||
) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5902,9 +5841,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
// and point
|
||||
const { newElement } = this.state;
|
||||
if (isBindingElement(newElement, false)) {
|
||||
this.maybeSuggestBindingsForLinearElementAtCoords(
|
||||
maybeSuggestBindingsForLinearElementAtCoords(
|
||||
newElement,
|
||||
[scenePointer],
|
||||
this,
|
||||
this.state.startBoundElement,
|
||||
);
|
||||
} else {
|
||||
|
@ -5914,106 +5854,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
if (this.state.multiElement) {
|
||||
const { multiElement } = this.state;
|
||||
const { x: rx, y: ry } = multiElement;
|
||||
|
||||
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,
|
||||
{
|
||||
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,
|
||||
scenePointerY,
|
||||
event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement)
|
||||
? null
|
||||
: 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);
|
||||
}
|
||||
handleCanvasPointerMoveForLinearElement(
|
||||
multiElement,
|
||||
this,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
event,
|
||||
this.triggerRender,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -7751,18 +7599,34 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
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
|
||||
if (
|
||||
multiElement.points.length > 1 &&
|
||||
lastCommittedPoint &&
|
||||
pointDistance(
|
||||
pointFrom(
|
||||
pointerDownState.origin.x - rx,
|
||||
pointerDownState.origin.y - ry,
|
||||
),
|
||||
lastCommittedPoint,
|
||||
) < LINE_CONFIRM_THRESHOLD
|
||||
hoveredElementForBinding ||
|
||||
(multiElement.points.length > 1 &&
|
||||
lastCommittedPoint &&
|
||||
pointDistance(
|
||||
pointFrom(
|
||||
pointerDownState.origin.x - rx,
|
||||
pointerDownState.origin.y - ry,
|
||||
),
|
||||
lastCommittedPoint,
|
||||
) < LINE_CONFIRM_THRESHOLD)
|
||||
) {
|
||||
this.actionManager.executeAction(actionFinalize);
|
||||
return;
|
||||
|
@ -7806,53 +7670,68 @@ class App extends React.Component<AppProps, AppState> {
|
|||
? [currentItemStartArrowhead, currentItemEndArrowhead]
|
||||
: [null, null];
|
||||
|
||||
const element =
|
||||
elementType === "arrow"
|
||||
? newArrowElement({
|
||||
type: elementType,
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
roundness:
|
||||
this.state.currentItemArrowType === ARROW_TYPE.round
|
||||
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
|
||||
: // note, roundness doesn't have any effect for elbow arrows,
|
||||
// but it's best to set it to null as well
|
||||
null,
|
||||
startArrowhead,
|
||||
endArrowhead,
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
|
||||
fixedSegments:
|
||||
this.state.currentItemArrowType === ARROW_TYPE.elbow
|
||||
? []
|
||||
: null,
|
||||
})
|
||||
: newLinearElement({
|
||||
type: elementType,
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
roundness:
|
||||
this.state.currentItemRoundness === "round"
|
||||
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
|
||||
: null,
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
let element: NonDeleted<ExcalidrawLinearElement>;
|
||||
if (elementType === "arrow") {
|
||||
const arrow: Mutable<NonDeleted<ExcalidrawArrowElement>> =
|
||||
newArrowElement({
|
||||
type: "arrow",
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
roundness:
|
||||
this.state.currentItemArrowType === ARROW_TYPE.round
|
||||
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
|
||||
: // note, roundness doesn't have any effect for elbow arrows,
|
||||
// but it's best to set it to null as well
|
||||
null,
|
||||
startArrowhead,
|
||||
endArrowhead,
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
|
||||
fixedSegments:
|
||||
this.state.currentItemArrowType === ARROW_TYPE.elbow ? [] : null,
|
||||
});
|
||||
|
||||
const [x, y] = getOutlineAvoidingPoint(
|
||||
arrow,
|
||||
pointFrom<GlobalPoint>(gridX, gridY),
|
||||
0,
|
||||
this.scene,
|
||||
this.state.zoom,
|
||||
);
|
||||
|
||||
element = {
|
||||
...arrow,
|
||||
x,
|
||||
y,
|
||||
};
|
||||
} else {
|
||||
element = newLinearElement({
|
||||
type: elementType,
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
roundness:
|
||||
this.state.currentItemRoundness === "round"
|
||||
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
|
||||
: null,
|
||||
locked: false,
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
}
|
||||
this.setState((prevState) => {
|
||||
const nextSelectedElementIds = {
|
||||
...prevState.selectedElementIds,
|
||||
|
@ -8167,12 +8046,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
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
|
||||
// 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
|
||||
|
@ -8267,13 +8140,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
(element, pointsSceneCoords) => {
|
||||
this.maybeSuggestBindingsForLinearElementAtCoords(
|
||||
maybeSuggestBindingsForLinearElementAtCoords(
|
||||
element,
|
||||
pointsSceneCoords,
|
||||
this,
|
||||
);
|
||||
},
|
||||
linearElementEditor,
|
||||
this.scene,
|
||||
);
|
||||
if (newLinearElementEditor) {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
|
@ -8658,54 +8531,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
}
|
||||
} else if (isLinearElement(newElement)) {
|
||||
pointerDownState.drag.hasOccurred = true;
|
||||
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,
|
||||
{
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
},
|
||||
false,
|
||||
);
|
||||
} 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({
|
||||
onPointerMoveFromPointerDownOnLinearElement(
|
||||
newElement,
|
||||
});
|
||||
|
||||
if (isBindingElement(newElement, false)) {
|
||||
// When creating a linear element by dragging
|
||||
this.maybeSuggestBindingsForLinearElementAtCoords(
|
||||
newElement,
|
||||
[pointerCoords],
|
||||
this.state.startBoundElement,
|
||||
);
|
||||
}
|
||||
this,
|
||||
pointerDownState,
|
||||
pointerCoords,
|
||||
event,
|
||||
elementsMap,
|
||||
);
|
||||
} else {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||
|
@ -8956,21 +8789,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state.selectedLinearElement,
|
||||
this.state,
|
||||
this.scene,
|
||||
true,
|
||||
);
|
||||
|
||||
const { startBindingElement, endBindingElement } =
|
||||
linearElementEditor;
|
||||
const element = this.scene.getElement(linearElementEditor.elementId);
|
||||
if (isBindingElement(element)) {
|
||||
bindOrUnbindLinearElement(
|
||||
element,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
elementsMap,
|
||||
this.scene,
|
||||
);
|
||||
}
|
||||
|
||||
if (linearElementEditor !== this.state.selectedLinearElement) {
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
|
@ -9071,66 +8892,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
if (isLinearElement(newElement)) {
|
||||
if (newElement!.points.length > 1) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
}
|
||||
const pointerCoords = viewportCoordsToSceneCoords(
|
||||
onPointerUpFromPointerDownOnLinearElementHandler(
|
||||
newElement,
|
||||
multiElement,
|
||||
this,
|
||||
this.store,
|
||||
pointerDownState,
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -10196,49 +9966,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
};
|
||||
|
||||
private maybeSuggestBindingsForLinearElementAtCoords = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
/** scene coords */
|
||||
pointerCoords: {
|
||||
x: number;
|
||||
y: number;
|
||||
}[],
|
||||
// During line creation the start binding hasn't been written yet
|
||||
// into `linearElement`
|
||||
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
|
||||
): void => {
|
||||
if (!pointerCoords.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const suggestedBindings = pointerCoords.reduce(
|
||||
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
|
||||
const hoveredBindableElement = getHoveredElementForBinding(
|
||||
coords,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state.zoom,
|
||||
isElbowArrow(linearElement),
|
||||
isElbowArrow(linearElement),
|
||||
);
|
||||
if (
|
||||
hoveredBindableElement != null &&
|
||||
!isLinearElementSimpleAndAlreadyBound(
|
||||
linearElement,
|
||||
oppositeBindingBoundElement?.id,
|
||||
hoveredBindableElement,
|
||||
)
|
||||
) {
|
||||
acc.push(hoveredBindableElement);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
this.setState({ suggestedBindings });
|
||||
};
|
||||
|
||||
private clearSelection(hitElement: ExcalidrawElement | null): void {
|
||||
this.setState((prevState) => ({
|
||||
selectedElementIds: makeNextSelectedElementIds({}, prevState),
|
||||
|
@ -10725,12 +10452,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
updateBoundElements(
|
||||
croppingElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
{
|
||||
newSize: {
|
||||
width: croppingElement.width,
|
||||
height: croppingElement.height,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.setState({
|
||||
|
|
|
@ -5,6 +5,10 @@ import { mutateElement } from "@excalidraw/element/mutateElement";
|
|||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
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 { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
@ -14,9 +18,11 @@ import { angleIcon } from "../icons";
|
|||
import DragInput from "./DragInput";
|
||||
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 { AppState } from "../../types";
|
||||
|
||||
interface AngleProps {
|
||||
element: ExcalidrawElement;
|
||||
|
@ -33,9 +39,10 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
shouldChangeByStepSize,
|
||||
nextValue,
|
||||
scene,
|
||||
setAppState,
|
||||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
if (origElement && !isElbowArrow(origElement)) {
|
||||
const latestElement = elementsMap.get(origElement.id);
|
||||
|
@ -48,7 +55,8 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
|
||||
updateBindings(latestElement, elementsMap, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
|
@ -74,15 +82,32 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
|
||||
updateBindings(latestElement, elementsMap, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
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) => {
|
||||
return (
|
||||
<DragInput
|
||||
|
@ -91,6 +116,7 @@ const Angle = ({ element, scene, appState, property }: AngleProps) => {
|
|||
value={Math.round((radiansToDegrees(element.angle) % 360) * 100) / 100}
|
||||
elements={[element]}
|
||||
dragInputCallback={handleDegreeChange}
|
||||
dragFinishedCallback={handleFinished}
|
||||
editable={isPropertyEditable(element, "angle")}
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { isImageElement } from "@excalidraw/element/typeChecks";
|
|||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
|
@ -118,6 +118,9 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||
});
|
||||
|
||||
updateBindings(element, elementsMap, scene);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -150,6 +153,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||
});
|
||||
|
||||
updateBindings(element, elementsMap, scene);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -184,6 +189,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
},
|
||||
);
|
||||
|
||||
updateBindings(origElement, elementsMap, scene);
|
||||
|
||||
return;
|
||||
}
|
||||
const changeInWidth = property === "width" ? accumulatedChange : 0;
|
||||
|
@ -230,6 +237,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
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 { CaptureUpdateAction } from "../../store";
|
||||
import { useApp } from "../App";
|
||||
import { useApp, useExcalidrawSetAppState } from "../App";
|
||||
import { InlineIcon } from "../InlineIcon";
|
||||
|
||||
import { SMALLEST_DELTA } from "./utils";
|
||||
|
@ -34,6 +34,21 @@ export type DragInputCallbackType<
|
|||
property: P;
|
||||
originalAppState: AppState;
|
||||
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;
|
||||
|
||||
interface StatsDragInputProps<
|
||||
|
@ -47,6 +62,7 @@ interface StatsDragInputProps<
|
|||
editable?: boolean;
|
||||
shouldKeepAspectRatio?: boolean;
|
||||
dragInputCallback: DragInputCallbackType<T, E>;
|
||||
dragFinishedCallback?: DragFinishedCallbackType<T, E>;
|
||||
property: T;
|
||||
scene: Scene;
|
||||
appState: AppState;
|
||||
|
@ -61,6 +77,7 @@ const StatsDragInput = <
|
|||
label,
|
||||
icon,
|
||||
dragInputCallback,
|
||||
dragFinishedCallback,
|
||||
value,
|
||||
elements,
|
||||
editable = true,
|
||||
|
@ -71,6 +88,7 @@ const StatsDragInput = <
|
|||
sensitivity = 1,
|
||||
}: StatsDragInputProps<T, E>) => {
|
||||
const app = useApp();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const labelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -123,6 +141,7 @@ const StatsDragInput = <
|
|||
// reason: idempotent to avoid unnecessary
|
||||
if (isNaN(original) || Math.abs(rounded - original) >= SMALLEST_DELTA) {
|
||||
stateRef.current.lastUpdatedValue = updatedValue;
|
||||
const originalElementsMap = app.scene.getNonDeletedElementsMap();
|
||||
dragInputCallback({
|
||||
accumulatedChange: 0,
|
||||
instantChange: 0,
|
||||
|
@ -135,6 +154,17 @@ const StatsDragInput = <
|
|||
property,
|
||||
originalAppState: appState,
|
||||
setInputValue: (value) => setInputValue(String(value)),
|
||||
setAppState,
|
||||
});
|
||||
dragFinishedCallback?.({
|
||||
originalElements: elements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
originalAppState: appState,
|
||||
accumulatedChange: rounded,
|
||||
property,
|
||||
setAppState,
|
||||
setInputValue: (value) => setInputValue(String(value)),
|
||||
});
|
||||
app.syncActionResult({
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
|
@ -262,6 +292,7 @@ const StatsDragInput = <
|
|||
scene,
|
||||
originalAppState,
|
||||
setInputValue: (value) => setInputValue(String(value)),
|
||||
setAppState,
|
||||
});
|
||||
|
||||
stepChange = 0;
|
||||
|
@ -282,6 +313,19 @@ const StatsDragInput = <
|
|||
false,
|
||||
);
|
||||
|
||||
if (originalElements !== null && originalElementsMap !== null) {
|
||||
dragFinishedCallback?.({
|
||||
originalElements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
originalAppState,
|
||||
property,
|
||||
accumulatedChange,
|
||||
setAppState,
|
||||
setInputValue: (value) => setInputValue(String(value)),
|
||||
});
|
||||
}
|
||||
|
||||
app.syncActionResult({
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
});
|
||||
|
|
|
@ -24,7 +24,13 @@ import type {
|
|||
} from "@excalidraw/element/types";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
import {
|
||||
getAtomicUnits,
|
||||
getStepSizedValue,
|
||||
isPropertyEditable,
|
||||
updateBindings,
|
||||
updateSelectionBindings,
|
||||
} from "./utils";
|
||||
import { getElementsInAtomicUnit } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
|
@ -87,9 +93,7 @@ const resizeElementInGroup = (
|
|||
);
|
||||
if (boundTextElement) {
|
||||
const newFontSize = boundTextElement.fontSize * scale;
|
||||
updateBoundElements(latestElement, elementsMap, {
|
||||
newSize: { width: updates.width, height: updates.height },
|
||||
});
|
||||
updateBoundElements(latestElement, elementsMap);
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||
mutateElement(
|
||||
|
@ -120,6 +124,7 @@ const resizeGroup = (
|
|||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
// keep aspect ratio for groups
|
||||
if (property === "width") {
|
||||
|
@ -145,6 +150,8 @@ const resizeGroup = (
|
|||
originalElementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
updateSelectionBindings(originalElements, elementsMap, scene);
|
||||
};
|
||||
|
||||
const handleDimensionChange: DragInputCallbackType<
|
||||
|
@ -196,6 +203,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
|
@ -244,6 +252,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
shouldInformMutation: false,
|
||||
},
|
||||
);
|
||||
|
||||
updateBindings(latestElement, elementsMap, scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -303,6 +313,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
|
|
|
@ -8,12 +8,16 @@ import { getCommonBounds } from "@excalidraw/element/bounds";
|
|||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
import {
|
||||
getAtomicUnits,
|
||||
getStepSizedValue,
|
||||
isPropertyEditable,
|
||||
updateSelectionBindings,
|
||||
} from "./utils";
|
||||
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
|
@ -66,12 +70,12 @@ const moveElements = (
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
updateSelectionBindings(elements, elementsMap, scene);
|
||||
};
|
||||
|
||||
const moveGroupTo = (
|
||||
|
@ -79,7 +83,6 @@ const moveGroupTo = (
|
|||
nextY: number,
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
|
@ -113,13 +116,13 @@ const moveGroupTo = (
|
|||
topLeftY + offsetY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectionBindings(originalElements, elementsMap, scene);
|
||||
};
|
||||
|
||||
const handlePositionChange: DragInputCallbackType<
|
||||
|
@ -135,7 +138,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
|
||||
if (nextValue !== undefined) {
|
||||
for (const atomicUnit of getAtomicUnits(
|
||||
|
@ -160,7 +162,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||
newTopLeftY,
|
||||
elementsInUnit.map((el) => el.original),
|
||||
elementsMap,
|
||||
elements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
|
@ -189,8 +190,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
);
|
||||
|
|
|
@ -10,9 +10,12 @@ import { isImageElement } from "@excalidraw/element/typeChecks";
|
|||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
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 { AppState } from "../../types";
|
||||
|
||||
|
@ -36,9 +39,9 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
property,
|
||||
scene,
|
||||
originalAppState,
|
||||
setAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
const [cx, cy] = [
|
||||
origElement.x + origElement.width / 2,
|
||||
|
@ -105,6 +108,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
crop: nextCrop,
|
||||
});
|
||||
|
||||
updateBindings(element, elementsMap, scene);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -123,6 +128,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
crop: nextCrop,
|
||||
});
|
||||
|
||||
updateBindings(element, elementsMap, scene);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -134,10 +141,11 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
|
||||
updateBindings(origElement, elementsMap, scene);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -167,19 +175,21 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
|
||||
updateBindings(origElement, elementsMap, scene);
|
||||
};
|
||||
|
||||
const Position = ({
|
||||
property,
|
||||
element,
|
||||
elementsMap,
|
||||
scene,
|
||||
appState,
|
||||
}: PositionProps) => {
|
||||
const handleFinished: DragFinishedCallbackType<"x" | "y"> = ({
|
||||
setAppState,
|
||||
}) => {
|
||||
setAppState({
|
||||
suggestedBindings: [],
|
||||
});
|
||||
};
|
||||
|
||||
const Position = ({ property, element, scene, appState }: PositionProps) => {
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(element.x, element.y),
|
||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
||||
|
@ -207,6 +217,7 @@ const Position = ({
|
|||
label={property === "x" ? "X" : "Y"}
|
||||
elements={[element]}
|
||||
dragInputCallback={handlePositionChange}
|
||||
dragFinishedCallback={handleFinished}
|
||||
scene={scene}
|
||||
value={value}
|
||||
property={property}
|
||||
|
|
|
@ -128,7 +128,7 @@ describe("binding with linear elements", () => {
|
|||
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 inputX = UI.queryStatsProperty("X")?.querySelector(
|
||||
".drag-input",
|
||||
|
@ -137,10 +137,10 @@ describe("binding with linear elements", () => {
|
|||
expect(linear.startBinding).not.toBe(null);
|
||||
expect(inputX).not.toBeNull();
|
||||
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 inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
||||
".drag-input",
|
||||
|
@ -148,7 +148,7 @@ describe("binding with linear elements", () => {
|
|||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
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 () => {
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
bindOrUnbindLinearElements,
|
||||
updateBoundElements,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
import {
|
||||
isBindableElement,
|
||||
isBindingElement,
|
||||
isFrameLikeElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
|
@ -18,6 +15,11 @@ import {
|
|||
isInGroup,
|
||||
} from "@excalidraw/element/groups";
|
||||
|
||||
import {
|
||||
bindOrUnbindLinearElement,
|
||||
updateBoundElements,
|
||||
} from "@excalidraw/element/binding";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
|
@ -27,7 +29,8 @@ import type {
|
|||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
export type StatsInputProperty =
|
||||
|
@ -120,8 +123,6 @@ export const moveElement = (
|
|||
newTopLeftY: number,
|
||||
originalElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
originalElementsMap: ElementsMap,
|
||||
shouldInformMutation = true,
|
||||
) => {
|
||||
|
@ -156,7 +157,10 @@ export const moveElement = (
|
|||
},
|
||||
shouldInformMutation,
|
||||
);
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
|
||||
if (isBindableElement(latestElement)) {
|
||||
updateBoundElements(latestElement, elementsMap);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
originalElement,
|
||||
|
@ -200,25 +204,34 @@ export const getAtomicUnits = (
|
|||
export const updateBindings = (
|
||||
latestElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
zoom?: AppState["zoom"];
|
||||
},
|
||||
) => {
|
||||
if (isLinearElement(latestElement)) {
|
||||
bindOrUnbindLinearElements(
|
||||
[latestElement],
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
true,
|
||||
[],
|
||||
options?.zoom,
|
||||
);
|
||||
} else {
|
||||
updateBoundElements(latestElement, elementsMap, options);
|
||||
if (isBindingElement(latestElement)) {
|
||||
if (latestElement.startBinding || latestElement.endBinding) {
|
||||
bindOrUnbindLinearElement(latestElement, null, null, elementsMap, scene);
|
||||
}
|
||||
} else if (isBindableElement(latestElement)) {
|
||||
updateBoundElements(latestElement, elementsMap);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSelectionBindings = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
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": {
|
||||
"elementId": "ellipse-1",
|
||||
"focus": -0.007519379844961235,
|
||||
"gap": 11.562288374879595,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 35,
|
||||
"height": 33.53813187180941,
|
||||
"id": Any<String>,
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
|
@ -108,8 +108,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
0,
|
||||
],
|
||||
[
|
||||
394,
|
||||
34,
|
||||
377.5017739818493,
|
||||
32.53813187180941,
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -119,7 +119,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"startBinding": {
|
||||
"elementId": "id49",
|
||||
"focus": -0.0813953488372095,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1864ab",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -128,7 +128,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 395,
|
||||
"width": 378.5017739818493,
|
||||
"x": 247.5,
|
||||
"y": 420.5,
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"endBinding": {
|
||||
"elementId": "ellipse-1",
|
||||
"focus": 0.10666666666666667,
|
||||
"gap": 3.8343264684446097,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -164,7 +164,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
0,
|
||||
],
|
||||
[
|
||||
399,
|
||||
397.82801810964474,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -175,7 +175,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"startBinding": {
|
||||
"elementId": "diamond-1",
|
||||
"focus": 0,
|
||||
"gap": 4.545343408287929,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#e67700",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -184,7 +184,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 400,
|
||||
"width": 398.82801810964474,
|
||||
"x": 227.5,
|
||||
"y": 450,
|
||||
}
|
||||
|
@ -335,7 +335,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||
"endBinding": {
|
||||
"elementId": "text-2",
|
||||
"focus": 0,
|
||||
"gap": 14,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -354,7 +354,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||
0,
|
||||
],
|
||||
[
|
||||
99,
|
||||
299,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -365,7 +365,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||
"startBinding": {
|
||||
"elementId": "text-1",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -374,7 +374,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"width": 300,
|
||||
"x": 255.5,
|
||||
"y": 239,
|
||||
}
|
||||
|
@ -437,7 +437,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||
"endBinding": {
|
||||
"elementId": "id42",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -456,7 +456,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||
0,
|
||||
],
|
||||
[
|
||||
99,
|
||||
89,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -467,7 +467,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||
"startBinding": {
|
||||
"elementId": "id41",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -476,7 +476,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"width": 90,
|
||||
"x": 255.5,
|
||||
"y": 239,
|
||||
}
|
||||
|
@ -592,7 +592,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||
"version": 3,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"x": 355,
|
||||
"x": 350,
|
||||
"y": 189,
|
||||
}
|
||||
`;
|
||||
|
@ -613,7 +613,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||
"endBinding": {
|
||||
"elementId": "id46",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -632,7 +632,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||
0,
|
||||
],
|
||||
[
|
||||
99,
|
||||
89,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -643,7 +643,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||
"startBinding": {
|
||||
"elementId": "id45",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -652,7 +652,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 100,
|
||||
"width": 90,
|
||||
"x": 255.5,
|
||||
"y": 239,
|
||||
}
|
||||
|
@ -786,7 +786,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||
"versionNonce": Any<Number>,
|
||||
"verticalAlign": "top",
|
||||
"width": 100,
|
||||
"x": 355,
|
||||
"x": 350,
|
||||
"y": 226.5,
|
||||
}
|
||||
`;
|
||||
|
@ -1475,12 +1475,12 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"endBinding": {
|
||||
"elementId": "Alice",
|
||||
"focus": -0,
|
||||
"gap": 5.299874999999986,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
"height": 7.105427357601002e-15,
|
||||
"id": Any<String>,
|
||||
"index": "a4",
|
||||
"isDeleted": false,
|
||||
|
@ -1494,8 +1494,8 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
0,
|
||||
],
|
||||
[
|
||||
271.985,
|
||||
0,
|
||||
272.28487500000006,
|
||||
-0.9999999999999929,
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -1507,7 +1507,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"startBinding": {
|
||||
"elementId": "Bob",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -1516,9 +1516,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": Any<Number>,
|
||||
"width": 272.985,
|
||||
"width": 273.28487500000006,
|
||||
"x": 111.762,
|
||||
"y": 57,
|
||||
"y": 57.5,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -1538,7 +1538,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"endBinding": {
|
||||
"elementId": "B",
|
||||
"focus": 0,
|
||||
"gap": 14,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -1566,7 +1566,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||
"startBinding": {
|
||||
"elementId": "Bob",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
|
|
@ -20,10 +20,6 @@ import {
|
|||
} from "@excalidraw/common";
|
||||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
import { normalizeFixedPoint } from "@excalidraw/element/binding";
|
||||
import {
|
||||
updateElbowArrowPoints,
|
||||
validateElbowPoints,
|
||||
} from "@excalidraw/element/elbowArrow";
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import { bumpVersion } from "@excalidraw/element/mutateElement";
|
||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||
|
@ -57,7 +53,6 @@ import type {
|
|||
ExcalidrawTextElement,
|
||||
FixedPointBinding,
|
||||
FontFamilyValues,
|
||||
NonDeletedSceneElementsMap,
|
||||
OrderedExcalidrawElement,
|
||||
PointBinding,
|
||||
StrokeRoundness,
|
||||
|
@ -585,73 +580,7 @@ export const restoreElements = (
|
|||
}
|
||||
}
|
||||
|
||||
// NOTE (mtolmacs): Temporary fix for extremely large arrows
|
||||
// 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;
|
||||
});
|
||||
return restoredElements;
|
||||
};
|
||||
|
||||
const coalesceAppStateValue = <
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { pointFrom } from "@excalidraw/math";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element/binding";
|
||||
|
||||
import type { ExcalidrawArrowElement } from "@excalidraw/element/types";
|
||||
|
||||
import { convertToExcalidrawElements } from "./transform";
|
||||
|
@ -433,7 +435,7 @@ describe("Test Transform", () => {
|
|||
startBinding: {
|
||||
elementId: rectangle.id,
|
||||
focus: 0,
|
||||
gap: 1,
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
},
|
||||
endBinding: {
|
||||
elementId: ellipse.id,
|
||||
|
@ -462,7 +464,7 @@ describe("Test Transform", () => {
|
|||
});
|
||||
|
||||
expect(ellipse).toMatchObject({
|
||||
x: 355,
|
||||
x: 350,
|
||||
y: 189,
|
||||
type: "ellipse",
|
||||
boundElements: [
|
||||
|
@ -518,7 +520,7 @@ describe("Test Transform", () => {
|
|||
startBinding: {
|
||||
elementId: text2.id,
|
||||
focus: 0,
|
||||
gap: 1,
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
},
|
||||
endBinding: {
|
||||
elementId: text3.id,
|
||||
|
@ -547,7 +549,7 @@ describe("Test Transform", () => {
|
|||
});
|
||||
|
||||
expect(text3).toMatchObject({
|
||||
x: 355,
|
||||
x: 350,
|
||||
y: 226.5,
|
||||
type: "text",
|
||||
boundElements: [
|
||||
|
@ -781,7 +783,7 @@ describe("Test Transform", () => {
|
|||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||
elementId: "rect-1",
|
||||
focus: -0,
|
||||
gap: 14,
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
});
|
||||
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",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "102.35417",
|
||||
"height": "99.23572",
|
||||
"id": "id172",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
|
@ -212,8 +212,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
0,
|
||||
],
|
||||
[
|
||||
"101.77517",
|
||||
"102.35417",
|
||||
"96.42891",
|
||||
"99.23572",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -228,8 +228,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 40,
|
||||
"width": "101.77517",
|
||||
"x": "0.70711",
|
||||
"width": "96.42891",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -295,47 +295,47 @@ History {
|
|||
"deleted": {
|
||||
"endBinding": {
|
||||
"elementId": "id171",
|
||||
"focus": "0.00990",
|
||||
"gap": 1,
|
||||
"focus": "0.01065",
|
||||
"gap": 5,
|
||||
},
|
||||
"height": "0.98586",
|
||||
"height": "1.00000",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"-0.98586",
|
||||
"92.92893",
|
||||
"-1.00000",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id170",
|
||||
"focus": "0.02970",
|
||||
"gap": 1,
|
||||
"focus": "0.03194",
|
||||
"gap": 5,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id171",
|
||||
"focus": "-0.02000",
|
||||
"gap": 1,
|
||||
"focus": "-0.02251",
|
||||
"gap": 5,
|
||||
},
|
||||
"height": "0.00000",
|
||||
"height": "0.08238",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"0.00000",
|
||||
"92.92893",
|
||||
"0.08238",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id170",
|
||||
"focus": "0.02000",
|
||||
"gap": 1,
|
||||
"focus": "0.01897",
|
||||
"gap": 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -390,43 +390,47 @@ History {
|
|||
"focus": 0,
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "102.35417",
|
||||
"height": "99.23572",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"101.77517",
|
||||
"102.35417",
|
||||
"96.42891",
|
||||
"99.23572",
|
||||
],
|
||||
],
|
||||
"startBinding": null,
|
||||
"width": "96.42891",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id171",
|
||||
"focus": "0.00990",
|
||||
"gap": 1,
|
||||
"focus": "0.01065",
|
||||
"gap": 5,
|
||||
},
|
||||
"height": "0.98586",
|
||||
"height": "1.00000",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"-0.98586",
|
||||
"92.92893",
|
||||
"-1.00000",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id170",
|
||||
"focus": "0.02970",
|
||||
"gap": 1,
|
||||
"focus": "0.03194",
|
||||
"gap": 5,
|
||||
},
|
||||
"y": "0.99364",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": "1.03339",
|
||||
},
|
||||
},
|
||||
"id175" => Delta {
|
||||
|
@ -566,7 +570,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"96.46447",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -580,8 +584,8 @@ History {
|
|||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"width": "96.46447",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -804,7 +808,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"96.46447",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -820,8 +824,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 30,
|
||||
"width": 0,
|
||||
"x": "149.29289",
|
||||
"width": "96.46447",
|
||||
"x": 150,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -854,10 +858,11 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
0,
|
||||
"0.00000",
|
||||
0,
|
||||
],
|
||||
],
|
||||
"width": "0.00000",
|
||||
},
|
||||
"inserted": {
|
||||
"points": [
|
||||
|
@ -866,10 +871,11 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"89.39340",
|
||||
0,
|
||||
],
|
||||
],
|
||||
"width": "89.39340",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -921,17 +927,19 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"96.46447",
|
||||
0,
|
||||
],
|
||||
],
|
||||
"startBinding": null,
|
||||
"width": "96.46447",
|
||||
"x": 150,
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id166",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"points": [
|
||||
[
|
||||
|
@ -939,15 +947,17 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
0,
|
||||
"0.00000",
|
||||
0,
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id165",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"width": "0.00000",
|
||||
"x": "146.46447",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1074,7 +1084,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"96.46447",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -1088,8 +1098,8 @@ History {
|
|||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"width": "96.46447",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -1241,7 +1251,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "1.30038",
|
||||
"height": "1.71911",
|
||||
"id": "id178",
|
||||
"index": "Zz",
|
||||
"isDeleted": false,
|
||||
|
@ -1255,8 +1265,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"1.30038",
|
||||
"92.92893",
|
||||
"1.71911",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -1279,8 +1289,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -1613,7 +1623,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "1.30038",
|
||||
"height": "1.71911",
|
||||
"id": "id181",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
|
@ -1627,8 +1637,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"1.30038",
|
||||
"92.92893",
|
||||
"1.71911",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -1651,8 +1661,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -1771,7 +1781,7 @@ History {
|
|||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "11.27227",
|
||||
"height": "12.86717",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
|
@ -1784,8 +1794,8 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"11.27227",
|
||||
"92.92893",
|
||||
"12.86717",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -1806,8 +1816,8 @@ History {
|
|||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -2321,12 +2331,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"endBinding": {
|
||||
"elementId": "id185",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "374.05754",
|
||||
"height": "369.23631",
|
||||
"id": "id186",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
|
@ -2340,8 +2350,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
0,
|
||||
],
|
||||
[
|
||||
"502.78936",
|
||||
"-374.05754",
|
||||
"496.83418",
|
||||
"-369.23631",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -2352,7 +2362,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"startBinding": {
|
||||
"elementId": "id184",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -2360,9 +2370,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "502.78936",
|
||||
"x": "-0.83465",
|
||||
"y": "-36.58211",
|
||||
"width": "496.83418",
|
||||
"x": "2.19080",
|
||||
"y": "-38.78706",
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -2481,7 +2491,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id185",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -2499,7 +2509,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -2511,14 +2521,14 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id184",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -7490,7 +7500,7 @@ History {
|
|||
|
||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `10`;
|
||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `11`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] appState 1`] = `
|
||||
{
|
||||
|
@ -10561,7 +10571,7 @@ History {
|
|||
|
||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `15`;
|
||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `16`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] appState 1`] = `
|
||||
{
|
||||
|
@ -15161,7 +15171,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"endBinding": {
|
||||
"elementId": "id58",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -15180,7 +15190,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -15192,7 +15202,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"startBinding": {
|
||||
"elementId": "id56",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -15200,8 +15210,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -15242,7 +15252,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -15255,7 +15265,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -15532,7 +15542,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id58",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -15550,7 +15560,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -15562,14 +15572,14 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id56",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -15859,7 +15869,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"endBinding": {
|
||||
"elementId": "id52",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -15878,7 +15888,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -15890,7 +15900,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"startBinding": {
|
||||
"elementId": "id50",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -15898,8 +15908,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -16152,7 +16162,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id52",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -16170,7 +16180,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -16182,14 +16192,14 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id50",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -16479,7 +16489,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"endBinding": {
|
||||
"elementId": "id64",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -16498,7 +16508,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -16510,7 +16520,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"startBinding": {
|
||||
"elementId": "id62",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -16518,8 +16528,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -16772,7 +16782,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id64",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -16790,7 +16800,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -16802,14 +16812,14 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id62",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -17097,7 +17107,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"endBinding": {
|
||||
"elementId": "id70",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -17116,7 +17126,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -17128,7 +17138,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"startBinding": {
|
||||
"elementId": "id68",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -17136,8 +17146,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -17193,14 +17203,14 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id68",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -17210,7 +17220,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -17460,7 +17470,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id70",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -17478,7 +17488,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -17490,14 +17500,14 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id68",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -17811,7 +17821,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"endBinding": {
|
||||
"elementId": "id77",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -17830,7 +17840,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -17842,7 +17852,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"startBinding": {
|
||||
"elementId": "id75",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -17850,8 +17860,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -17913,7 +17923,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id77",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"points": [
|
||||
[
|
||||
|
@ -17921,14 +17931,14 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "id75",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -17939,7 +17949,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -18189,7 +18199,7 @@ History {
|
|||
"endBinding": {
|
||||
"elementId": "id77",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -18207,7 +18217,7 @@ History {
|
|||
0,
|
||||
],
|
||||
[
|
||||
100,
|
||||
"92.92893",
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -18219,14 +18229,14 @@ History {
|
|||
"startBinding": {
|
||||
"elementId": "id75",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"width": "92.92893",
|
||||
"x": "3.53553",
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
|
@ -20188,4 +20198,4 @@ History {
|
|||
|
||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `21`;
|
||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `22`;
|
||||
|
|
|
@ -190,13 +190,13 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "id1",
|
||||
"focus": "-0.46667",
|
||||
"gap": 10,
|
||||
"focus": "-0.40764",
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "87.29887",
|
||||
"height": "82.18136",
|
||||
"id": "id2",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
|
@ -210,8 +210,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||
0,
|
||||
],
|
||||
[
|
||||
"86.85786",
|
||||
"87.29887",
|
||||
"93.92893",
|
||||
"82.18136",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -222,8 +222,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"focus": "-0.60000",
|
||||
"gap": 10,
|
||||
"focus": "-0.49801",
|
||||
"gap": 5,
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
@ -232,8 +232,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||
"updated": 1,
|
||||
"version": 11,
|
||||
"versionNonce": 1051383431,
|
||||
"width": "86.85786",
|
||||
"x": "107.07107",
|
||||
"y": "47.07107",
|
||||
"width": "93.92893",
|
||||
"x": "103.53553",
|
||||
"y": "50.01536",
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -50,7 +50,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 1505387817,
|
||||
"width": 70,
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
|
@ -106,7 +106,7 @@ exports[`multi point mode in linear elements > line 3`] = `
|
|||
"type": "line",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 1505387817,
|
||||
"width": 70,
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
|
|
|
@ -6835,7 +6835,7 @@ History {
|
|||
|
||||
exports[`regression tests > draw every type of shape > [end of test] number of elements 1`] = `0`;
|
||||
|
||||
exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `33`;
|
||||
exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `35`;
|
||||
|
||||
exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] appState 1`] = `
|
||||
{
|
||||
|
@ -14566,7 +14566,7 @@ History {
|
|||
|
||||
exports[`regression tests > undo/redo drawing an element > [end of test] number of elements 1`] = `0`;
|
||||
|
||||
exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `20`;
|
||||
exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `21`;
|
||||
|
||||
exports[`regression tests > updates fontSize & fontFamily appState > [end of test] appState 1`] = `
|
||||
{
|
||||
|
|
|
@ -467,6 +467,7 @@ export class UI {
|
|||
height: initialHeight = initialWidth,
|
||||
angle = 0,
|
||||
points: initialPoints,
|
||||
elbowed = false,
|
||||
}: {
|
||||
position?: number;
|
||||
x?: number;
|
||||
|
@ -476,6 +477,7 @@ export class UI {
|
|||
height?: number;
|
||||
angle?: number;
|
||||
points?: T extends "line" | "arrow" | "freedraw" ? LocalPoint[] : never;
|
||||
elbowed?: boolean;
|
||||
} = {},
|
||||
): Element<T> & {
|
||||
/** Returns the actual, current element from the elements array, instead
|
||||
|
@ -494,6 +496,17 @@ export class UI {
|
|||
if (type === "text") {
|
||||
mouse.reset();
|
||||
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) {
|
||||
points.forEach((point) => {
|
||||
mouse.reset();
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element/binding";
|
||||
import { pointFrom } from "@excalidraw/math";
|
||||
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
|
@ -4779,12 +4780,12 @@ describe("history", () => {
|
|||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
focus: 0,
|
||||
gap: 1,
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
focus: -0,
|
||||
gap: 1,
|
||||
gap: FIXED_BINDING_DISTANCE,
|
||||
}),
|
||||
isDeleted: true,
|
||||
}),
|
||||
|
|
|
@ -1247,7 +1247,7 @@ describe("Test Linear Elements", () => {
|
|||
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
|
||||
|
||||
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.y).toBe(0);
|
||||
expect(
|
||||
|
@ -1266,7 +1266,7 @@ describe("Test Linear Elements", () => {
|
|||
mouse.downAt(rect.x, rect.y);
|
||||
mouse.moveTo(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.y).toBe(0);
|
||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||
|
|
|
@ -109,8 +109,10 @@ describe("move element", () => {
|
|||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||
expect([rectB.x, rectB.y]).toEqual([200, 0]);
|
||||
expect([arrow.x, arrow.y]).toEqual([110, 50]);
|
||||
expect([arrow.width, arrow.height]).toEqual([80, 80]);
|
||||
expect([Math.round(arrow.x), Math.round(arrow.y)]).toEqual([104, 50]);
|
||||
expect([Math.round(arrow.width), Math.round(arrow.height)]).toEqual([
|
||||
93, 81,
|
||||
]);
|
||||
|
||||
renderInteractiveScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
|
@ -128,8 +130,11 @@ describe("move element", () => {
|
|||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||
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());
|
||||
});
|
||||
|
|
|
@ -118,8 +118,8 @@ describe("multi point mode in linear elements", () => {
|
|||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(h.elements.length).toEqual(1);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
|
@ -161,8 +161,8 @@ describe("multi point mode in linear elements", () => {
|
|||
fireEvent.keyDown(document, {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(h.elements.length).toEqual(1);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
|
|
|
@ -3,6 +3,8 @@ import { expect } from "vitest";
|
|||
|
||||
import { reseed } from "@excalidraw/common";
|
||||
|
||||
import "@excalidraw/utils/test-utils";
|
||||
|
||||
import { Excalidraw } from "../index";
|
||||
|
||||
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.x).toBeCloseTo(-80);
|
||||
expect(arrow.y).toBeCloseTo(50);
|
||||
expect(arrow.width).toBeCloseTo(116.7, 1);
|
||||
expect(arrow.width).toBeCloseTo(119.6, 1);
|
||||
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.x).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[1][1]).toBeCloseTo(125.79, 1);
|
||||
|
||||
expect(ellipseArrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[90.1827, 98.5896],
|
||||
]);
|
||||
|
||||
expect(textArrow.endBinding?.elementId).toEqual(text.id);
|
||||
expect(textArrow.x).toEqual(360);
|
||||
expect(textArrow.y).toEqual(300);
|
||||
expect(textArrow.points[0]).toEqual([0, 0]);
|
||||
expect(textArrow.points[1][0]).toBeCloseTo(-94, 0);
|
||||
expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0);
|
||||
expect(textArrow.points[1][0]).toBeCloseTo(-95, 0);
|
||||
expect(textArrow.points[1][1]).toBeCloseTo(-129.1, 0);
|
||||
});
|
||||
|
|
|
@ -157,22 +157,13 @@ export function curveIntersectLineSegment<
|
|||
return bezierEquation(c, t);
|
||||
};
|
||||
|
||||
let solution = calculate(initial_guesses[0]);
|
||||
if (solution) {
|
||||
return [solution];
|
||||
}
|
||||
const solutions = [
|
||||
calculate(initial_guesses[0]),
|
||||
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]);
|
||||
if (solution) {
|
||||
return [solution];
|
||||
}
|
||||
|
||||
solution = calculate(initial_guesses[2]);
|
||||
if (solution) {
|
||||
return [solution];
|
||||
}
|
||||
|
||||
return [];
|
||||
return solutions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -91,9 +91,10 @@ export function isPoint(p: unknown): p is LocalPoint | GlobalPoint {
|
|||
export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
precision: number = PRECISION,
|
||||
): boolean {
|
||||
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 = (
|
||||
value: number,
|
||||
precision: number,
|
||||
precision: number = (Math.log(1 / PRECISION) * Math.LOG10E + 1) | 0,
|
||||
func: "round" | "floor" | "ceil" = "round",
|
||||
) => {
|
||||
const multiplier = Math.pow(10, precision);
|
||||
|
|
|
@ -195,11 +195,6 @@ export const getEllipseShape = <Point extends GlobalPoint | LocalPoint>(
|
|||
};
|
||||
|
||||
export const getCurvePathOps = (shape: Drawable): Op[] => {
|
||||
// NOTE (mtolmacs): Temporary fix for extremely large elements
|
||||
if (!shape) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const set of shape.sets) {
|
||||
if (set.type === "path") {
|
||||
return set.ops;
|
||||
|
|
Loading…
Add table
Reference in a new issue