mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: Elbow arrow segment fixing & positioning (#8952)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
8551823da9
commit
91ebf8b0ea
33 changed files with 3282 additions and 1716 deletions
|
@ -623,11 +623,9 @@ export const updateBoundElements = (
|
|||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||
zoom?: AppState["zoom"];
|
||||
},
|
||||
) => {
|
||||
const { newSize, simultaneouslyUpdated, changedElements, zoom } =
|
||||
options ?? {};
|
||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||
simultaneouslyUpdated,
|
||||
);
|
||||
|
@ -661,7 +659,7 @@ export const updateBoundElements = (
|
|||
|
||||
// `linearElement` is being moved/scaled already, just update the binding
|
||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||
mutateElement(element, bindings);
|
||||
mutateElement(element, bindings, true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -703,23 +701,14 @@ export const updateBoundElements = (
|
|||
}> => update !== null,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
updates,
|
||||
elementsMap,
|
||||
{
|
||||
...(changedElement.id === element.startBinding?.elementId
|
||||
? { startBinding: bindings.startBinding }
|
||||
: {}),
|
||||
...(changedElement.id === element.endBinding?.elementId
|
||||
? { endBinding: bindings.endBinding }
|
||||
: {}),
|
||||
},
|
||||
{
|
||||
changedElements,
|
||||
zoom,
|
||||
},
|
||||
);
|
||||
LinearElementEditor.movePoints(element, updates, {
|
||||
...(changedElement.id === element.startBinding?.elementId
|
||||
? { startBinding: bindings.startBinding }
|
||||
: {}),
|
||||
...(changedElement.id === element.endBinding?.elementId
|
||||
? { endBinding: bindings.endBinding }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
if (boundText && !boundText.isDeleted) {
|
||||
|
@ -778,9 +767,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||
);
|
||||
}
|
||||
|
||||
const pointHeading = headingForPointFromElement(bindableElement, aabb, p);
|
||||
|
||||
return pointHeading;
|
||||
return headingForPointFromElement(bindableElement, aabb, p);
|
||||
};
|
||||
|
||||
const getDistanceForBinding = (
|
||||
|
@ -2283,7 +2270,7 @@ export const getGlobalFixedPointForBindableElement = (
|
|||
);
|
||||
};
|
||||
|
||||
const getGlobalFixedPoints = (
|
||||
export const getGlobalFixedPoints = (
|
||||
arrow: ExcalidrawElbowArrowElement,
|
||||
elementsMap: ElementsMap,
|
||||
): [GlobalPoint, GlobalPoint] => {
|
||||
|
|
|
@ -42,9 +42,20 @@ export const dragSelectedElements = (
|
|||
return;
|
||||
}
|
||||
|
||||
const selectedElements = _selectedElements.filter(
|
||||
(el) => !(isElbowArrow(el) && el.startBinding && el.endBinding),
|
||||
);
|
||||
const selectedElements = _selectedElements.filter((element) => {
|
||||
if (isElbowArrow(element) && element.startBinding && element.endBinding) {
|
||||
const startElement = _selectedElements.find(
|
||||
(el) => el.id === element.startBinding?.elementId,
|
||||
);
|
||||
const endElement = _selectedElements.find(
|
||||
(el) => el.id === element.endBinding?.elementId,
|
||||
);
|
||||
|
||||
return startElement && endElement;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// we do not want a frame and its elements to be selected at the same time
|
||||
// but when it happens (due to some bug), we want to avoid updating element
|
||||
|
@ -78,10 +89,8 @@ export const dragSelectedElements = (
|
|||
|
||||
elementsToUpdate.forEach((element) => {
|
||||
updateElementCoords(pointerDownState, element, adjustedOffset);
|
||||
if (
|
||||
if (!isArrowElement(element)) {
|
||||
// skip arrow labels since we calculate its position during render
|
||||
!isArrowElement(element)
|
||||
) {
|
||||
const textElement = getBoundTextElement(
|
||||
element,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
|
@ -89,10 +98,10 @@ export const dragSelectedElements = (
|
|||
if (textElement) {
|
||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||
}
|
||||
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||
});
|
||||
}
|
||||
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -9,20 +9,121 @@ import {
|
|||
render,
|
||||
} from "../tests/test-utils";
|
||||
import { bindLinearElement } from "./binding";
|
||||
import { Excalidraw } from "../index";
|
||||
import { mutateElbowArrow } from "./routing";
|
||||
import { Excalidraw, mutateElement } from "../index";
|
||||
import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
import { ARROW_TYPE } from "../constants";
|
||||
import type { LocalPoint } from "../../math";
|
||||
import { pointFrom } from "../../math";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
describe("elbow arrow segment move", () => {
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
});
|
||||
|
||||
it("can move the second segment of a fully connected elbow arrow", () => {
|
||||
UI.createElement("rectangle", {
|
||||
x: -100,
|
||||
y: -50,
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
UI.createElement("rectangle", {
|
||||
x: 200,
|
||||
y: 150,
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
|
||||
UI.clickTool("arrow");
|
||||
UI.clickOnTestId("elbow-arrow");
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(0, 0);
|
||||
mouse.click();
|
||||
mouse.moveTo(200, 200);
|
||||
mouse.click();
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(100, 100);
|
||||
mouse.down();
|
||||
mouse.moveTo(115, 100);
|
||||
mouse.up();
|
||||
|
||||
const arrow = h.scene.getSelectedElements(
|
||||
h.state,
|
||||
)[0] as ExcalidrawElbowArrowElement;
|
||||
|
||||
expect(h.state.selectedElementIds).toEqual({ [arrow.id]: true });
|
||||
expect(arrow.fixedSegments?.length).toBe(1);
|
||||
|
||||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[110, 0],
|
||||
[110, 200],
|
||||
[190, 200],
|
||||
]);
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(105, 74.275);
|
||||
mouse.doubleClick();
|
||||
|
||||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[110, 0],
|
||||
[110, 200],
|
||||
[190, 200],
|
||||
]);
|
||||
});
|
||||
|
||||
it("can move the second segment of an unconnected elbow arrow", () => {
|
||||
UI.clickTool("arrow");
|
||||
UI.clickOnTestId("elbow-arrow");
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(0, 0);
|
||||
mouse.click();
|
||||
mouse.moveTo(250, 200);
|
||||
mouse.click();
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(125, 100);
|
||||
mouse.down();
|
||||
mouse.moveTo(130, 100);
|
||||
mouse.up();
|
||||
|
||||
const arrow = h.scene.getSelectedElements(
|
||||
h.state,
|
||||
)[0] as ExcalidrawArrowElement;
|
||||
|
||||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[130, 0],
|
||||
[130, 200],
|
||||
[250, 200],
|
||||
]);
|
||||
|
||||
mouse.reset();
|
||||
mouse.moveTo(130, 100);
|
||||
mouse.doubleClick();
|
||||
|
||||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[125, 0],
|
||||
[125, 200],
|
||||
[250, 200],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("elbow arrow routing", () => {
|
||||
it("can properly generate orthogonal arrow points", () => {
|
||||
const scene = new Scene();
|
||||
|
@ -31,10 +132,12 @@ describe("elbow arrow routing", () => {
|
|||
elbowed: true,
|
||||
}) as ExcalidrawElbowArrowElement;
|
||||
scene.insertElement(arrow);
|
||||
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
|
||||
pointFrom(-45 - arrow.x, -100.1 - arrow.y),
|
||||
pointFrom(45 - arrow.x, 99.9 - arrow.y),
|
||||
]);
|
||||
mutateElement(arrow, {
|
||||
points: [
|
||||
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
|
||||
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
|
||||
],
|
||||
});
|
||||
expect(arrow.points).toEqual([
|
||||
[0, 0],
|
||||
[0, 100],
|
||||
|
@ -81,7 +184,9 @@ describe("elbow arrow routing", () => {
|
|||
expect(arrow.startBinding).not.toBe(null);
|
||||
expect(arrow.endBinding).not.toBe(null);
|
||||
|
||||
mutateElbowArrow(arrow, elementsMap, [pointFrom(0, 0), pointFrom(90, 200)]);
|
||||
mutateElement(arrow, {
|
||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
||||
});
|
||||
|
||||
expect(arrow.points).toEqual([
|
||||
[0, 0],
|
||||
|
@ -182,8 +287,6 @@ describe("elbow arrow ui", () => {
|
|||
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
||||
[0, 0],
|
||||
[35, 0],
|
||||
[35, 90],
|
||||
[35, 90], // Note that coordinates are rounded above!
|
||||
[35, 165],
|
||||
[103, 165],
|
||||
]);
|
2111
packages/excalidraw/element/elbowArrow.ts
Normal file
2111
packages/excalidraw/element/elbowArrow.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -452,20 +452,12 @@ const createBindingArrow = (
|
|||
bindingArrow as OrderedExcalidrawElement,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
bindingArrow,
|
||||
[
|
||||
{
|
||||
index: 1,
|
||||
point: bindingArrow.points[1],
|
||||
},
|
||||
],
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
undefined,
|
||||
LinearElementEditor.movePoints(bindingArrow, [
|
||||
{
|
||||
changedElements,
|
||||
index: 1,
|
||||
point: bindingArrow.points[1],
|
||||
},
|
||||
);
|
||||
]);
|
||||
|
||||
return bindingArrow;
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
pointScaleFromOrigin,
|
||||
radiansToDegrees,
|
||||
triangleIncludesPoint,
|
||||
vectorFromPoint,
|
||||
} from "../../math";
|
||||
import { getCenterForBounds, type Bounds } from "./bounds";
|
||||
import type { ExcalidrawBindableElement } from "./types";
|
||||
|
@ -52,9 +53,24 @@ export const vectorToHeading = (vec: Vector): Heading => {
|
|||
return HEADING_UP;
|
||||
};
|
||||
|
||||
export const headingForPoint = <P extends GlobalPoint | LocalPoint>(
|
||||
p: P,
|
||||
o: P,
|
||||
) => vectorToHeading(vectorFromPoint<P>(p, o));
|
||||
|
||||
export const headingForPointIsHorizontal = <P extends GlobalPoint | LocalPoint>(
|
||||
p: P,
|
||||
o: P,
|
||||
) => headingIsHorizontal(headingForPoint<P>(p, o));
|
||||
|
||||
export const compareHeading = (a: Heading, b: Heading) =>
|
||||
a[0] === b[0] && a[1] === b[1];
|
||||
|
||||
export const headingIsHorizontal = (a: Heading) =>
|
||||
compareHeading(a, HEADING_RIGHT) || compareHeading(a, HEADING_LEFT);
|
||||
|
||||
export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
|
||||
|
||||
// Gets the heading for the point by creating a bounding box around the rotated
|
||||
// close fitting bounding box, then creating 4 search cones around the center of
|
||||
// the external bbox.
|
||||
|
@ -63,7 +79,7 @@ export const headingForPointFromElement = <
|
|||
>(
|
||||
element: Readonly<ExcalidrawBindableElement>,
|
||||
aabb: Readonly<Bounds>,
|
||||
p: Readonly<LocalPoint | GlobalPoint>,
|
||||
p: Readonly<Point>,
|
||||
): Heading => {
|
||||
const SEARCH_CONE_MULTIPLIER = 2;
|
||||
|
||||
|
@ -117,14 +133,22 @@ export const headingForPointFromElement = <
|
|||
element.angle,
|
||||
);
|
||||
|
||||
if (triangleIncludesPoint([top, right, midPoint] as Triangle<Point>, p)) {
|
||||
if (
|
||||
triangleIncludesPoint<Point>([top, right, midPoint] as Triangle<Point>, p)
|
||||
) {
|
||||
return headingForDiamond(top, right);
|
||||
} else if (
|
||||
triangleIncludesPoint([right, bottom, midPoint] as Triangle<Point>, p)
|
||||
triangleIncludesPoint<Point>(
|
||||
[right, bottom, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
) {
|
||||
return headingForDiamond(right, bottom);
|
||||
} else if (
|
||||
triangleIncludesPoint([bottom, left, midPoint] as Triangle<Point>, p)
|
||||
triangleIncludesPoint<Point>(
|
||||
[bottom, left, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
) {
|
||||
return headingForDiamond(bottom, left);
|
||||
}
|
||||
|
@ -153,17 +177,17 @@ export const headingForPointFromElement = <
|
|||
SEARCH_CONE_MULTIPLIER,
|
||||
) as Point;
|
||||
|
||||
return triangleIncludesPoint(
|
||||
return triangleIncludesPoint<Point>(
|
||||
[topLeft, topRight, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
? HEADING_UP
|
||||
: triangleIncludesPoint(
|
||||
: triangleIncludesPoint<Point>(
|
||||
[topRight, bottomRight, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
? HEADING_RIGHT
|
||||
: triangleIncludesPoint(
|
||||
: triangleIncludesPoint<Point>(
|
||||
[bottomRight, bottomLeft, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
|
|
|
@ -7,9 +7,10 @@ import type {
|
|||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
OrderedExcalidrawElement,
|
||||
FixedPointBinding,
|
||||
SceneElementsMap,
|
||||
FixedSegment,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||
import type { Bounds } from "./bounds";
|
||||
|
@ -24,6 +25,7 @@ import type {
|
|||
InteractiveCanvasAppState,
|
||||
AppClassProperties,
|
||||
NullableGridSize,
|
||||
Zoom,
|
||||
} from "../types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
|
||||
|
@ -32,7 +34,7 @@ import {
|
|||
getHoveredElementForBinding,
|
||||
isBindingEnabled,
|
||||
} from "./binding";
|
||||
import { invariant, toBrandedType, tupleToCoors } from "../utils";
|
||||
import { invariant, tupleToCoors } from "../utils";
|
||||
import {
|
||||
isBindingElement,
|
||||
isElbowArrow,
|
||||
|
@ -44,7 +46,6 @@ import { DRAGGING_THRESHOLD } from "../constants";
|
|||
import type { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import type { Store } from "../store";
|
||||
import { mutateElbowArrow } from "./routing";
|
||||
import type Scene from "../scene/Scene";
|
||||
import type { Radians } from "../../math";
|
||||
import {
|
||||
|
@ -56,6 +57,8 @@ import {
|
|||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
pointDistance,
|
||||
pointTranslate,
|
||||
vectorFromPoint,
|
||||
} from "../../math";
|
||||
import {
|
||||
getBezierCurveLength,
|
||||
|
@ -65,6 +68,7 @@ import {
|
|||
mapIntervalToBezierT,
|
||||
} from "../shapes";
|
||||
import { getGridPoint } from "../snapping";
|
||||
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||
|
||||
const editorMidPointsCache: {
|
||||
version: number | null;
|
||||
|
@ -144,13 +148,13 @@ export class LinearElementEditor {
|
|||
* @param id the `elementId` from the instance of this class (so that we can
|
||||
* statically guarantee this method returns an ExcalidrawLinearElement)
|
||||
*/
|
||||
static getElement(
|
||||
static getElement<T extends ExcalidrawLinearElement>(
|
||||
id: InstanceType<typeof LinearElementEditor>["elementId"],
|
||||
elementsMap: ElementsMap,
|
||||
) {
|
||||
): T | null {
|
||||
const element = elementsMap.get(id);
|
||||
if (element) {
|
||||
return element as NonDeleted<ExcalidrawLinearElement>;
|
||||
return element as NonDeleted<T>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -291,20 +295,16 @@ export class LinearElementEditor {
|
|||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
[
|
||||
{
|
||||
index: selectedIndex,
|
||||
point: pointFrom(
|
||||
width + referencePoint[0],
|
||||
height + referencePoint[1],
|
||||
),
|
||||
isDragging: selectedIndex === lastClickedPoint,
|
||||
},
|
||||
],
|
||||
elementsMap,
|
||||
);
|
||||
LinearElementEditor.movePoints(element, [
|
||||
{
|
||||
index: selectedIndex,
|
||||
point: pointFrom(
|
||||
width + referencePoint[0],
|
||||
height + referencePoint[1],
|
||||
),
|
||||
isDragging: selectedIndex === lastClickedPoint,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
|
@ -339,7 +339,6 @@ export class LinearElementEditor {
|
|||
isDragging: pointIndex === lastClickedPoint,
|
||||
};
|
||||
}),
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -422,19 +421,15 @@ export class LinearElementEditor {
|
|||
selectedPoint === element.points.length - 1
|
||||
) {
|
||||
if (isPathALoop(element.points, appState.zoom.value)) {
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
[
|
||||
{
|
||||
index: selectedPoint,
|
||||
point:
|
||||
selectedPoint === 0
|
||||
? element.points[element.points.length - 1]
|
||||
: element.points[0],
|
||||
},
|
||||
],
|
||||
elementsMap,
|
||||
);
|
||||
LinearElementEditor.movePoints(element, [
|
||||
{
|
||||
index: selectedPoint,
|
||||
point:
|
||||
selectedPoint === 0
|
||||
? element.points[element.points.length - 1]
|
||||
: element.points[0],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const bindingElement = isBindingEnabled(appState)
|
||||
|
@ -495,6 +490,7 @@ export class LinearElementEditor {
|
|||
|
||||
// Since its not needed outside editor unless 2 pointer lines or bound text
|
||||
if (
|
||||
!isElbowArrow(element) &&
|
||||
!appState.editingLinearElement &&
|
||||
element.points.length > 2 &&
|
||||
!boundText
|
||||
|
@ -533,6 +529,7 @@ export class LinearElementEditor {
|
|||
element,
|
||||
element.points[index],
|
||||
element.points[index + 1],
|
||||
index,
|
||||
appState.zoom,
|
||||
)
|
||||
) {
|
||||
|
@ -573,19 +570,23 @@ export class LinearElementEditor {
|
|||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
if (clickedPointIndex >= 0) {
|
||||
if (!isElbowArrow(element) && clickedPointIndex >= 0) {
|
||||
return null;
|
||||
}
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
element,
|
||||
elementsMap,
|
||||
);
|
||||
if (points.length >= 3 && !appState.editingLinearElement) {
|
||||
if (
|
||||
points.length >= 3 &&
|
||||
!appState.editingLinearElement &&
|
||||
!isElbowArrow(element)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const threshold =
|
||||
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value;
|
||||
(LinearElementEditor.POINT_HANDLE_SIZE + 1) / appState.zoom.value;
|
||||
|
||||
const existingSegmentMidpointHitCoords =
|
||||
linearElementEditor.segmentMidPointHoveredCoords;
|
||||
|
@ -604,10 +605,11 @@ export class LinearElementEditor {
|
|||
let index = 0;
|
||||
const midPoints: typeof editorMidPointsCache["points"] =
|
||||
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
|
||||
|
||||
while (index < midPoints.length) {
|
||||
if (midPoints[index] !== null) {
|
||||
const distance = pointDistance(
|
||||
pointFrom(midPoints[index]![0], midPoints[index]![1]),
|
||||
midPoints[index]!,
|
||||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
);
|
||||
if (distance <= threshold) {
|
||||
|
@ -620,16 +622,25 @@ export class LinearElementEditor {
|
|||
return null;
|
||||
};
|
||||
|
||||
static isSegmentTooShort(
|
||||
static isSegmentTooShort<P extends GlobalPoint | LocalPoint>(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
startPoint: GlobalPoint | LocalPoint,
|
||||
endPoint: GlobalPoint | LocalPoint,
|
||||
zoom: AppState["zoom"],
|
||||
startPoint: P,
|
||||
endPoint: P,
|
||||
index: number,
|
||||
zoom: Zoom,
|
||||
) {
|
||||
let distance = pointDistance(
|
||||
pointFrom(startPoint[0], startPoint[1]),
|
||||
pointFrom(endPoint[0], endPoint[1]),
|
||||
);
|
||||
if (isElbowArrow(element)) {
|
||||
if (index >= 0 && index < element.points.length) {
|
||||
return (
|
||||
pointDistance(startPoint, endPoint) * zoom.value <
|
||||
LinearElementEditor.POINT_HANDLE_SIZE / 2
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
let distance = pointDistance(startPoint, endPoint);
|
||||
if (element.points.length > 2 && element.roundness) {
|
||||
distance = getBezierCurveLength(element, endPoint);
|
||||
}
|
||||
|
@ -748,12 +759,8 @@ export class LinearElementEditor {
|
|||
segmentMidpoint,
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
if (event.altKey && appState.editingLinearElement) {
|
||||
if (
|
||||
linearElementEditor.lastUncommittedPoint == null &&
|
||||
!isElbowArrow(element)
|
||||
) {
|
||||
} else if (event.altKey && appState.editingLinearElement) {
|
||||
if (linearElementEditor.lastUncommittedPoint == null) {
|
||||
mutateElement(element, {
|
||||
points: [
|
||||
...element.points,
|
||||
|
@ -909,12 +916,7 @@ export class LinearElementEditor {
|
|||
|
||||
if (!event.altKey) {
|
||||
if (lastPoint === lastUncommittedPoint) {
|
||||
LinearElementEditor.deletePoints(
|
||||
element,
|
||||
[points.length - 1],
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
);
|
||||
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
||||
}
|
||||
return {
|
||||
...appState.editingLinearElement,
|
||||
|
@ -952,23 +954,14 @@ export class LinearElementEditor {
|
|||
}
|
||||
|
||||
if (lastPoint === lastUncommittedPoint) {
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
[
|
||||
{
|
||||
index: element.points.length - 1,
|
||||
point: newPoint,
|
||||
},
|
||||
],
|
||||
elementsMap,
|
||||
);
|
||||
LinearElementEditor.movePoints(element, [
|
||||
{
|
||||
index: element.points.length - 1,
|
||||
point: newPoint,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
LinearElementEditor.addPoints(
|
||||
element,
|
||||
[{ point: newPoint }],
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
);
|
||||
LinearElementEditor.addPoints(element, [{ point: newPoint }]);
|
||||
}
|
||||
return {
|
||||
...appState.editingLinearElement,
|
||||
|
@ -1197,16 +1190,12 @@ export class LinearElementEditor {
|
|||
// potentially expanding the bounding box
|
||||
if (pointAddedToEnd) {
|
||||
const lastPoint = element.points[element.points.length - 1];
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
[
|
||||
{
|
||||
index: element.points.length - 1,
|
||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||
},
|
||||
],
|
||||
elementsMap,
|
||||
);
|
||||
LinearElementEditor.movePoints(element, [
|
||||
{
|
||||
index: element.points.length - 1,
|
||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1221,8 +1210,6 @@ export class LinearElementEditor {
|
|||
static deletePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
pointIndices: readonly number[],
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
zoom: AppState["zoom"],
|
||||
) {
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
@ -1252,47 +1239,27 @@ export class LinearElementEditor {
|
|||
return acc;
|
||||
}, []);
|
||||
|
||||
LinearElementEditor._updatePoints(
|
||||
element,
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
elementsMap,
|
||||
);
|
||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||
}
|
||||
|
||||
static addPoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
targetPoints: { point: LocalPoint }[],
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
zoom: AppState["zoom"],
|
||||
) {
|
||||
const offsetX = 0;
|
||||
const offsetY = 0;
|
||||
|
||||
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
||||
LinearElementEditor._updatePoints(
|
||||
element,
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
elementsMap,
|
||||
);
|
||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||
}
|
||||
|
||||
static movePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
otherUpdates?: {
|
||||
startBinding?: PointBinding | null;
|
||||
endBinding?: PointBinding | null;
|
||||
},
|
||||
options?: {
|
||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||
isDragging?: boolean;
|
||||
zoom?: AppState["zoom"];
|
||||
},
|
||||
) {
|
||||
const { points } = element;
|
||||
|
||||
|
@ -1335,7 +1302,6 @@ export class LinearElementEditor {
|
|||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
elementsMap,
|
||||
otherUpdates,
|
||||
{
|
||||
isDragging: targetPoints.reduce(
|
||||
|
@ -1343,8 +1309,6 @@ export class LinearElementEditor {
|
|||
dragging || targetPoint.isDragging === true,
|
||||
false,
|
||||
),
|
||||
changedElements: options?.changedElements,
|
||||
zoom: options?.zoom,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1451,54 +1415,49 @@ export class LinearElementEditor {
|
|||
nextPoints: readonly LocalPoint[],
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
otherUpdates?: {
|
||||
startBinding?: PointBinding | null;
|
||||
endBinding?: PointBinding | null;
|
||||
},
|
||||
options?: {
|
||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||
isDragging?: boolean;
|
||||
zoom?: AppState["zoom"];
|
||||
},
|
||||
) {
|
||||
if (isElbowArrow(element)) {
|
||||
const bindings: {
|
||||
const updates: {
|
||||
startBinding?: FixedPointBinding | null;
|
||||
endBinding?: FixedPointBinding | null;
|
||||
points?: LocalPoint[];
|
||||
} = {};
|
||||
if (otherUpdates?.startBinding !== undefined) {
|
||||
bindings.startBinding =
|
||||
updates.startBinding =
|
||||
otherUpdates.startBinding !== null &&
|
||||
isFixedPointBinding(otherUpdates.startBinding)
|
||||
? otherUpdates.startBinding
|
||||
: null;
|
||||
}
|
||||
if (otherUpdates?.endBinding !== undefined) {
|
||||
bindings.endBinding =
|
||||
updates.endBinding =
|
||||
otherUpdates.endBinding !== null &&
|
||||
isFixedPointBinding(otherUpdates.endBinding)
|
||||
? otherUpdates.endBinding
|
||||
: null;
|
||||
}
|
||||
|
||||
const mergedElementsMap = options?.changedElements
|
||||
? toBrandedType<SceneElementsMap>(
|
||||
new Map([...elementsMap, ...options.changedElements]),
|
||||
)
|
||||
: elementsMap;
|
||||
|
||||
mutateElbowArrow(
|
||||
element,
|
||||
mergedElementsMap,
|
||||
nextPoints,
|
||||
updates.points = Array.from(nextPoints);
|
||||
updates.points[0] = pointTranslate(
|
||||
updates.points[0],
|
||||
vector(offsetX, offsetY),
|
||||
bindings,
|
||||
{
|
||||
isDragging: options?.isDragging,
|
||||
zoom: options?.zoom,
|
||||
},
|
||||
);
|
||||
updates.points[updates.points.length - 1] = pointTranslate(
|
||||
updates.points[updates.points.length - 1],
|
||||
vector(offsetX, offsetY),
|
||||
);
|
||||
|
||||
mutateElement(element, updates, true, {
|
||||
isDragging: options?.isDragging,
|
||||
});
|
||||
} else {
|
||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||
const prevCoords = getElementPointsCoords(element, element.points);
|
||||
|
@ -1773,6 +1732,99 @@ export class LinearElementEditor {
|
|||
|
||||
return coords;
|
||||
};
|
||||
|
||||
static moveFixedSegment(
|
||||
linearElement: LinearElementEditor,
|
||||
index: number,
|
||||
x: number,
|
||||
y: number,
|
||||
elementsMap: ElementsMap,
|
||||
): LinearElementEditor {
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElement.elementId,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
if (!element || !isElbowArrow(element)) {
|
||||
return linearElement;
|
||||
}
|
||||
|
||||
if (index && index > 0 && index < element.points.length) {
|
||||
const isHorizontal = headingIsHorizontal(
|
||||
vectorToHeading(
|
||||
vectorFromPoint(element.points[index], element.points[index - 1]),
|
||||
),
|
||||
);
|
||||
|
||||
const fixedSegments = (element.fixedSegments ?? []).reduce(
|
||||
(segments, s) => {
|
||||
segments[s.index] = s;
|
||||
return segments;
|
||||
},
|
||||
{} as Record<number, FixedSegment>,
|
||||
);
|
||||
fixedSegments[index] = {
|
||||
index,
|
||||
start: pointFrom<LocalPoint>(
|
||||
!isHorizontal ? x - element.x : element.points[index - 1][0],
|
||||
isHorizontal ? y - element.y : element.points[index - 1][1],
|
||||
),
|
||||
end: pointFrom<LocalPoint>(
|
||||
!isHorizontal ? x - element.x : element.points[index][0],
|
||||
isHorizontal ? y - element.y : element.points[index][1],
|
||||
),
|
||||
};
|
||||
const nextFixedSegments = Object.values(fixedSegments).sort(
|
||||
(a, b) => a.index - b.index,
|
||||
);
|
||||
|
||||
const offset = nextFixedSegments
|
||||
.map((segment) => segment.index)
|
||||
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
|
||||
|
||||
mutateElement(element, {
|
||||
fixedSegments: nextFixedSegments,
|
||||
});
|
||||
|
||||
const point = pointFrom<GlobalPoint>(
|
||||
element.x +
|
||||
(element.fixedSegments![offset].start[0] +
|
||||
element.fixedSegments![offset].end[0]) /
|
||||
2,
|
||||
element.y +
|
||||
(element.fixedSegments![offset].start[1] +
|
||||
element.fixedSegments![offset].end[1]) /
|
||||
2,
|
||||
);
|
||||
|
||||
return {
|
||||
...linearElement,
|
||||
segmentMidPointHoveredCoords: point,
|
||||
pointerDownState: {
|
||||
...linearElement.pointerDownState,
|
||||
segmentMidpoint: {
|
||||
added: false,
|
||||
index: element.fixedSegments![offset].index,
|
||||
value: point,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return linearElement;
|
||||
}
|
||||
|
||||
static deleteFixedSegment(
|
||||
element: ExcalidrawElbowArrowElement,
|
||||
index: number,
|
||||
): void {
|
||||
mutateElement(element, {
|
||||
fixedSegments: element.fixedSegments?.filter(
|
||||
(segment) => segment.index !== index,
|
||||
),
|
||||
});
|
||||
mutateElement(element, {}, true);
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeSelectedPoints = (
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import type { ExcalidrawElement } from "./types";
|
||||
import type { ExcalidrawElement, SceneElementsMap } from "./types";
|
||||
import Scene from "../scene/Scene";
|
||||
import { getSizeFromPoints } from "../points";
|
||||
import { randomInteger } from "../random";
|
||||
import { getUpdatedTimestamp } from "../utils";
|
||||
import { getUpdatedTimestamp, toBrandedType } from "../utils";
|
||||
import type { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import { isElbowArrow } from "./typeChecks";
|
||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||
import type { Radians } from "../../math";
|
||||
|
||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
Partial<TElement>,
|
||||
|
@ -19,14 +22,49 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
informMutation = true,
|
||||
options?: {
|
||||
// Currently only for elbow arrows.
|
||||
// If true, the elbow arrow tries to bind to the nearest element. If false
|
||||
// it tries to keep the same bound element, if any.
|
||||
isDragging?: boolean;
|
||||
},
|
||||
): TElement => {
|
||||
let didChange = false;
|
||||
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
const { points, fileId } = updates as any;
|
||||
const { points, fixedSegments, fileId } = updates as any;
|
||||
|
||||
if (typeof points !== "undefined") {
|
||||
if (
|
||||
isElbowArrow(element) &&
|
||||
(Object.keys(updates).length === 0 || // normalization case
|
||||
typeof points !== "undefined" || // repositioning
|
||||
typeof fixedSegments !== "undefined") // segment fixing
|
||||
) {
|
||||
const elementsMap = toBrandedType<SceneElementsMap>(
|
||||
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
|
||||
);
|
||||
|
||||
updates = {
|
||||
...updates,
|
||||
angle: 0 as Radians,
|
||||
...updateElbowArrowPoints(
|
||||
{
|
||||
...element,
|
||||
x: updates.x || element.x,
|
||||
y: updates.y || element.y,
|
||||
},
|
||||
elementsMap,
|
||||
{
|
||||
fixedSegments,
|
||||
points,
|
||||
},
|
||||
{
|
||||
isDragging: options?.isDragging,
|
||||
},
|
||||
),
|
||||
};
|
||||
} else if (typeof points !== "undefined") {
|
||||
updates = { ...getSizeFromPoints(points), ...updates };
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ import type {
|
|||
ExcalidrawIframeElement,
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
FixedSegment,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
import {
|
||||
arrayToMap,
|
||||
|
@ -450,15 +452,34 @@ export const newLinearElement = (
|
|||
};
|
||||
};
|
||||
|
||||
export const newArrowElement = (
|
||||
export const newArrowElement = <T extends boolean>(
|
||||
opts: {
|
||||
type: ExcalidrawArrowElement["type"];
|
||||
startArrowhead?: Arrowhead | null;
|
||||
endArrowhead?: Arrowhead | null;
|
||||
points?: ExcalidrawArrowElement["points"];
|
||||
elbowed?: boolean;
|
||||
elbowed?: T;
|
||||
fixedSegments?: FixedSegment[] | null;
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawArrowElement> => {
|
||||
): T extends true
|
||||
? NonDeleted<ExcalidrawElbowArrowElement>
|
||||
: NonDeleted<ExcalidrawArrowElement> => {
|
||||
if (opts.elbowed) {
|
||||
return {
|
||||
..._newElementBase<ExcalidrawElbowArrowElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
lastCommittedPoint: null,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
startArrowhead: opts.startArrowhead || null,
|
||||
endArrowhead: opts.endArrowhead || null,
|
||||
elbowed: true,
|
||||
fixedSegments: opts.fixedSegments || [],
|
||||
startIsSpecial: false,
|
||||
endIsSpecial: false,
|
||||
} as NonDeleted<ExcalidrawElbowArrowElement>;
|
||||
}
|
||||
|
||||
return {
|
||||
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
|
@ -467,8 +488,10 @@ export const newArrowElement = (
|
|||
endBinding: null,
|
||||
startArrowhead: opts.startArrowhead || null,
|
||||
endArrowhead: opts.endArrowhead || null,
|
||||
elbowed: opts.elbowed || false,
|
||||
};
|
||||
elbowed: false,
|
||||
} as T extends true
|
||||
? NonDeleted<ExcalidrawElbowArrowElement>
|
||||
: NonDeleted<ExcalidrawArrowElement>;
|
||||
};
|
||||
|
||||
export const newImageElement = (
|
||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
|||
ExcalidrawImageElement,
|
||||
ElementsMap,
|
||||
SceneElementsMap,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
import type { Mutable } from "../utility-types";
|
||||
import {
|
||||
|
@ -53,7 +54,6 @@ import {
|
|||
import { wrapText } from "./textWrapping";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { isInGroup } from "../groups";
|
||||
import { mutateElbowArrow } from "./routing";
|
||||
import type { GlobalPoint } from "../../math";
|
||||
import {
|
||||
pointCenter,
|
||||
|
@ -177,10 +177,10 @@ export const transformElements = (
|
|||
elementsMap,
|
||||
transformHandleType,
|
||||
scene,
|
||||
originalElements,
|
||||
{
|
||||
shouldResizeFromCenter,
|
||||
shouldMaintainAspectRatio,
|
||||
originalElementsMap: originalElements,
|
||||
flipByX,
|
||||
flipByY,
|
||||
nextWidth,
|
||||
|
@ -531,8 +531,10 @@ const rotateMultipleElements = (
|
|||
);
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
const points = getArrowLocalFixedPoints(element, elementsMap);
|
||||
mutateElbowArrow(element, elementsMap, points);
|
||||
// Needed to re-route the arrow
|
||||
mutateElement(element, {
|
||||
points: getArrowLocalFixedPoints(element, elementsMap),
|
||||
});
|
||||
} else {
|
||||
mutateElement(
|
||||
element,
|
||||
|
@ -1201,6 +1203,7 @@ export const resizeMultipleElements = (
|
|||
elementsMap: ElementsMap,
|
||||
handleDirection: TransformHandleDirection,
|
||||
scene: Scene,
|
||||
originalElementsMap: ElementsMap,
|
||||
{
|
||||
shouldMaintainAspectRatio = false,
|
||||
shouldResizeFromCenter = false,
|
||||
|
@ -1208,7 +1211,6 @@ export const resizeMultipleElements = (
|
|||
flipByY = false,
|
||||
nextHeight,
|
||||
nextWidth,
|
||||
originalElementsMap,
|
||||
originalBoundingBox,
|
||||
}: {
|
||||
nextWidth?: number;
|
||||
|
@ -1217,7 +1219,6 @@ export const resizeMultipleElements = (
|
|||
shouldResizeFromCenter?: boolean;
|
||||
flipByX?: boolean;
|
||||
flipByY?: boolean;
|
||||
originalElementsMap?: ElementsMap;
|
||||
// added to improve performance
|
||||
originalBoundingBox?: BoundingBox;
|
||||
} = {},
|
||||
|
@ -1387,6 +1388,9 @@ export const resizeMultipleElements = (
|
|||
fontSize?: ExcalidrawTextElement["fontSize"];
|
||||
scale?: ExcalidrawImageElement["scale"];
|
||||
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
|
||||
startBinding?: ExcalidrawElbowArrowElement["startBinding"];
|
||||
endBinding?: ExcalidrawElbowArrowElement["endBinding"];
|
||||
fixedSegments?: ExcalidrawElbowArrowElement["fixedSegments"];
|
||||
};
|
||||
}[] = [];
|
||||
|
||||
|
@ -1427,6 +1431,44 @@ export const resizeMultipleElements = (
|
|||
...rescaledPoints,
|
||||
};
|
||||
|
||||
if (isElbowArrow(orig)) {
|
||||
// Mirror fixed point binding for elbow arrows
|
||||
// when resize goes into the negative direction
|
||||
if (orig.startBinding) {
|
||||
update.startBinding = {
|
||||
...orig.startBinding,
|
||||
fixedPoint: [
|
||||
flipByX
|
||||
? -orig.startBinding.fixedPoint[0] + 1
|
||||
: orig.startBinding.fixedPoint[0],
|
||||
flipByY
|
||||
? -orig.startBinding.fixedPoint[1] + 1
|
||||
: orig.startBinding.fixedPoint[1],
|
||||
],
|
||||
};
|
||||
}
|
||||
if (orig.endBinding) {
|
||||
update.endBinding = {
|
||||
...orig.endBinding,
|
||||
fixedPoint: [
|
||||
flipByX
|
||||
? -orig.endBinding.fixedPoint[0] + 1
|
||||
: orig.endBinding.fixedPoint[0],
|
||||
flipByY
|
||||
? -orig.endBinding.fixedPoint[1] + 1
|
||||
: orig.endBinding.fixedPoint[1],
|
||||
],
|
||||
};
|
||||
}
|
||||
if (orig.fixedSegments && rescaledPoints.points) {
|
||||
update.fixedSegments = orig.fixedSegments.map((segment) => ({
|
||||
...segment,
|
||||
start: rescaledPoints.points[segment.index - 1],
|
||||
end: rescaledPoints.points[segment.index],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (isImageElement(orig)) {
|
||||
update.scale = [
|
||||
orig.scale[0] * flipFactorX,
|
||||
|
@ -1472,7 +1514,10 @@ export const resizeMultipleElements = (
|
|||
} of elementsAndUpdates) {
|
||||
const { width, height, angle } = update;
|
||||
|
||||
mutateElement(element, update, false);
|
||||
mutateElement(element, update, false, {
|
||||
// needed for the fixed binding point udpate to take effect
|
||||
isDragging: true,
|
||||
});
|
||||
|
||||
updateBoundElements(element, elementsMap as SceneElementsMap, {
|
||||
simultaneouslyUpdated: elementsToUpdate,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -319,6 +319,12 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||
endArrowhead: Arrowhead | null;
|
||||
}>;
|
||||
|
||||
export type FixedSegment = {
|
||||
start: LocalPoint;
|
||||
end: LocalPoint;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type ExcalidrawArrowElement = ExcalidrawLinearElement &
|
||||
Readonly<{
|
||||
type: "arrow";
|
||||
|
@ -331,6 +337,23 @@ export type ExcalidrawElbowArrowElement = Merge<
|
|||
elbowed: true;
|
||||
startBinding: FixedPointBinding | null;
|
||||
endBinding: FixedPointBinding | null;
|
||||
fixedSegments: FixedSegment[] | null;
|
||||
/**
|
||||
* Marks that the 3rd point should be used as the 2nd point of the arrow in
|
||||
* order to temporarily hide the first segment of the arrow without losing
|
||||
* the data from the points array. It allows creating the expected arrow
|
||||
* path when the arrow with fixed segments is bound on a horizontal side and
|
||||
* moved to a vertical and vica versa.
|
||||
*/
|
||||
startIsSpecial: boolean | null;
|
||||
/**
|
||||
* Marks that the 3rd point backwards from the end should be used as the 2nd
|
||||
* point of the arrow in order to temporarily hide the last segment of the
|
||||
* arrow without losing the data from the points array. It allows creating
|
||||
* the expected arrow path when the arrow with fixed segments is bound on a
|
||||
* horizontal side and moved to a vertical and vica versa.
|
||||
*/
|
||||
endIsSpecial: boolean | null;
|
||||
}
|
||||
>;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue