mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
merge with master
This commit is contained in:
commit
e8e97adace
86 changed files with 4355 additions and 1113 deletions
|
@ -6,6 +6,7 @@ import {
|
|||
invariant,
|
||||
isDevEnv,
|
||||
isTestEnv,
|
||||
elementCenterPoint,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
|
@ -903,13 +904,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||
|
||||
if (!distance) {
|
||||
return vectorToHeading(
|
||||
vectorFromPoint(
|
||||
p,
|
||||
pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
),
|
||||
),
|
||||
vectorFromPoint(p, elementCenterPoint(bindableElement)),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1039,10 +1034,7 @@ export const avoidRectangularCorner = (
|
|||
element: ExcalidrawBindableElement,
|
||||
p: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||
|
||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||
|
@ -1139,10 +1131,9 @@ export const snapToMid = (
|
|||
tolerance: number = 0.05,
|
||||
): GlobalPoint => {
|
||||
const { x, y, width, height, angle } = element;
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
x + width / 2 - 0.1,
|
||||
y + height / 2 - 0.1,
|
||||
);
|
||||
|
||||
const center = elementCenterPoint(element, -0.1, -0.1);
|
||||
|
||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||
|
||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||
|
@ -1227,10 +1218,7 @@ const updateBoundPoint = (
|
|||
startOrEnd === "startBinding" ? "start" : "end",
|
||||
elementsMap,
|
||||
).fixedPoint;
|
||||
const globalMidPoint = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const globalMidPoint = elementCenterPoint(bindableElement);
|
||||
const global = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||
|
@ -1274,10 +1262,7 @@ const updateBoundPoint = (
|
|||
elementsMap,
|
||||
);
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(bindableElement);
|
||||
const interceptorLength =
|
||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||
pointDistance(adjacentPoint, center) +
|
||||
|
@ -1422,20 +1407,20 @@ const getLinearElementEdgeCoors = (
|
|||
);
|
||||
};
|
||||
|
||||
export const fixBindingsAfterDuplication = (
|
||||
newElements: ExcalidrawElement[],
|
||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||
duplicatedElementsMap: NonDeletedSceneElementsMap,
|
||||
export const fixDuplicatedBindingsAfterDuplication = (
|
||||
duplicatedElements: ExcalidrawElement[],
|
||||
origIdToDuplicateId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||
duplicateElementsMap: NonDeletedSceneElementsMap,
|
||||
) => {
|
||||
for (const element of newElements) {
|
||||
if ("boundElements" in element && element.boundElements) {
|
||||
Object.assign(element, {
|
||||
boundElements: element.boundElements.reduce(
|
||||
for (const duplicateElement of duplicatedElements) {
|
||||
if ("boundElements" in duplicateElement && duplicateElement.boundElements) {
|
||||
Object.assign(duplicateElement, {
|
||||
boundElements: duplicateElement.boundElements.reduce(
|
||||
(
|
||||
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
||||
binding,
|
||||
) => {
|
||||
const newBindingId = oldIdToDuplicatedId.get(binding.id);
|
||||
const newBindingId = origIdToDuplicateId.get(binding.id);
|
||||
if (newBindingId) {
|
||||
acc.push({ ...binding, id: newBindingId });
|
||||
}
|
||||
|
@ -1446,46 +1431,47 @@ export const fixBindingsAfterDuplication = (
|
|||
});
|
||||
}
|
||||
|
||||
if ("containerId" in element && element.containerId) {
|
||||
Object.assign(element, {
|
||||
containerId: oldIdToDuplicatedId.get(element.containerId) ?? null,
|
||||
if ("containerId" in duplicateElement && duplicateElement.containerId) {
|
||||
Object.assign(duplicateElement, {
|
||||
containerId:
|
||||
origIdToDuplicateId.get(duplicateElement.containerId) ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
if ("endBinding" in element && element.endBinding) {
|
||||
const newEndBindingId = oldIdToDuplicatedId.get(
|
||||
element.endBinding.elementId,
|
||||
if ("endBinding" in duplicateElement && duplicateElement.endBinding) {
|
||||
const newEndBindingId = origIdToDuplicateId.get(
|
||||
duplicateElement.endBinding.elementId,
|
||||
);
|
||||
Object.assign(element, {
|
||||
Object.assign(duplicateElement, {
|
||||
endBinding: newEndBindingId
|
||||
? {
|
||||
...element.endBinding,
|
||||
...duplicateElement.endBinding,
|
||||
elementId: newEndBindingId,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
if ("startBinding" in element && element.startBinding) {
|
||||
const newEndBindingId = oldIdToDuplicatedId.get(
|
||||
element.startBinding.elementId,
|
||||
if ("startBinding" in duplicateElement && duplicateElement.startBinding) {
|
||||
const newEndBindingId = origIdToDuplicateId.get(
|
||||
duplicateElement.startBinding.elementId,
|
||||
);
|
||||
Object.assign(element, {
|
||||
Object.assign(duplicateElement, {
|
||||
startBinding: newEndBindingId
|
||||
? {
|
||||
...element.startBinding,
|
||||
...duplicateElement.startBinding,
|
||||
elementId: newEndBindingId,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
if (isElbowArrow(duplicateElement)) {
|
||||
Object.assign(
|
||||
element,
|
||||
updateElbowArrowPoints(element, duplicatedElementsMap, {
|
||||
duplicateElement,
|
||||
updateElbowArrowPoints(duplicateElement, duplicateElementsMap, {
|
||||
points: [
|
||||
element.points[0],
|
||||
element.points[element.points.length - 1],
|
||||
duplicateElement.points[0],
|
||||
duplicateElement.points[duplicateElement.points.length - 1],
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
@ -1580,10 +1566,7 @@ const determineFocusDistance = (
|
|||
// Another point on the line, in absolute coordinates (closer to element)
|
||||
b: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
if (pointsEqual(a, b)) {
|
||||
return 0;
|
||||
|
@ -1713,10 +1696,7 @@ const determineFocusPoint = (
|
|||
focus: number,
|
||||
adjacentPoint: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
if (focus === 0) {
|
||||
return center;
|
||||
|
@ -2147,10 +2127,7 @@ export const getGlobalFixedPointForBindableElement = (
|
|||
element.x + element.width * fixedX,
|
||||
element.y + element.height * fixedY,
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
),
|
||||
elementCenterPoint(element),
|
||||
element.angle,
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import rough from "roughjs/bin/rough";
|
||||
|
||||
import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common";
|
||||
import {
|
||||
rescalePoints,
|
||||
arrayToMap,
|
||||
invariant,
|
||||
sizeOf,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
degreesToRadians,
|
||||
|
@ -13,7 +18,10 @@ import {
|
|||
|
||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||
|
||||
import { pointsOnBezierCurves } from "points-on-curve";
|
||||
|
||||
import type {
|
||||
Curve,
|
||||
Degrees,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
|
@ -37,6 +45,13 @@ import {
|
|||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import { getElementShape } from "./shapes";
|
||||
|
||||
import {
|
||||
deconstructDiamondElement,
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
|
@ -45,6 +60,9 @@ import type {
|
|||
NonDeleted,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMap,
|
||||
ExcalidrawRectanguloidElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ElementsMapOrArray,
|
||||
} from "./types";
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
|
@ -254,50 +272,82 @@ export const getElementAbsoluteCoords = (
|
|||
* that can be used for visual collision detection (useful for frames)
|
||||
* as opposed to bounding box collision detection
|
||||
*/
|
||||
/**
|
||||
* Given an element, return the line segments that make up the element.
|
||||
*
|
||||
* Uses helpers from /math
|
||||
*/
|
||||
export const getElementLineSegments = (
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): LineSegment<GlobalPoint>[] => {
|
||||
const shape = getElementShape(element, elementsMap);
|
||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
elementsMap,
|
||||
);
|
||||
const center = pointFrom<GlobalPoint>(cx, cy);
|
||||
|
||||
const center: GlobalPoint = pointFrom(cx, cy);
|
||||
|
||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
|
||||
if (shape.type === "polycurve") {
|
||||
const curves = shape.data;
|
||||
const points = curves
|
||||
.map((curve) => pointsOnBezierCurves(curve, 10))
|
||||
.flat();
|
||||
let i = 0;
|
||||
|
||||
while (i < element.points.length - 1) {
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
while (i < points.length - 1) {
|
||||
segments.push(
|
||||
lineSegment(
|
||||
pointRotateRads(
|
||||
pointFrom(
|
||||
element.points[i][0] + element.x,
|
||||
element.points[i][1] + element.y,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom(
|
||||
element.points[i + 1][0] + element.x,
|
||||
element.points[i + 1][1] + element.y,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointFrom(points[i][0], points[i][1]),
|
||||
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||
),
|
||||
);
|
||||
i++;
|
||||
}
|
||||
|
||||
return segments;
|
||||
} else if (shape.type === "polyline") {
|
||||
return shape.data as LineSegment<GlobalPoint>[];
|
||||
} else if (_isRectanguloidElement(element)) {
|
||||
const [sides, corners] = deconstructRectanguloidElement(element);
|
||||
const cornerSegments: LineSegment<GlobalPoint>[] = corners
|
||||
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||
.flat();
|
||||
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||
return [...rotatedSides, ...cornerSegments];
|
||||
} else if (element.type === "diamond") {
|
||||
const [sides, corners] = deconstructDiamondElement(element);
|
||||
const cornerSegments = corners
|
||||
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||
.flat();
|
||||
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||
|
||||
return [...rotatedSides, ...cornerSegments];
|
||||
} else if (shape.type === "polygon") {
|
||||
if (isTextElement(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (container && isLinearElement(container)) {
|
||||
const segments: LineSegment<GlobalPoint>[] = [
|
||||
lineSegment(pointFrom(x1, y1), pointFrom(x2, y1)),
|
||||
lineSegment(pointFrom(x2, y1), pointFrom(x2, y2)),
|
||||
lineSegment(pointFrom(x2, y2), pointFrom(x1, y2)),
|
||||
lineSegment(pointFrom(x1, y2), pointFrom(x1, y1)),
|
||||
];
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
|
||||
const points = shape.data as GlobalPoint[];
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
segments.push(lineSegment(points[i], points[i + 1]));
|
||||
}
|
||||
return segments;
|
||||
} else if (shape.type === "ellipse") {
|
||||
return getSegmentsOnEllipse(element as ExcalidrawEllipseElement);
|
||||
}
|
||||
|
||||
const [nw, ne, sw, se, n, s, w, e] = (
|
||||
const [nw, ne, sw, se, , , w, e] = (
|
||||
[
|
||||
[x1, y1],
|
||||
[x2, y1],
|
||||
|
@ -310,28 +360,6 @@ export const getElementLineSegments = (
|
|||
] as GlobalPoint[]
|
||||
).map((point) => pointRotateRads(point, center, element.angle));
|
||||
|
||||
if (element.type === "diamond") {
|
||||
return [
|
||||
lineSegment(n, w),
|
||||
lineSegment(n, e),
|
||||
lineSegment(s, w),
|
||||
lineSegment(s, e),
|
||||
];
|
||||
}
|
||||
|
||||
if (element.type === "ellipse") {
|
||||
return [
|
||||
lineSegment(n, w),
|
||||
lineSegment(n, e),
|
||||
lineSegment(s, w),
|
||||
lineSegment(s, e),
|
||||
lineSegment(n, w),
|
||||
lineSegment(n, e),
|
||||
lineSegment(s, w),
|
||||
lineSegment(s, e),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
lineSegment(nw, ne),
|
||||
lineSegment(sw, se),
|
||||
|
@ -344,6 +372,94 @@ export const getElementLineSegments = (
|
|||
];
|
||||
};
|
||||
|
||||
const _isRectanguloidElement = (
|
||||
element: ExcalidrawElement,
|
||||
): element is ExcalidrawRectanguloidElement => {
|
||||
return (
|
||||
element != null &&
|
||||
(element.type === "rectangle" ||
|
||||
element.type === "image" ||
|
||||
element.type === "iframe" ||
|
||||
element.type === "embeddable" ||
|
||||
element.type === "frame" ||
|
||||
element.type === "magicframe" ||
|
||||
(element.type === "text" && !element.containerId))
|
||||
);
|
||||
};
|
||||
|
||||
const getRotatedSides = (
|
||||
sides: LineSegment<GlobalPoint>[],
|
||||
center: GlobalPoint,
|
||||
angle: Radians,
|
||||
) => {
|
||||
return sides.map((side) => {
|
||||
return lineSegment(
|
||||
pointRotateRads<GlobalPoint>(side[0], center, angle),
|
||||
pointRotateRads<GlobalPoint>(side[1], center, angle),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getSegmentsOnCurve = (
|
||||
curve: Curve<GlobalPoint>,
|
||||
center: GlobalPoint,
|
||||
angle: Radians,
|
||||
): LineSegment<GlobalPoint>[] => {
|
||||
const points = pointsOnBezierCurves(curve, 10);
|
||||
let i = 0;
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
while (i < points.length - 1) {
|
||||
segments.push(
|
||||
lineSegment(
|
||||
pointRotateRads<GlobalPoint>(
|
||||
pointFrom(points[i][0], points[i][1]),
|
||||
center,
|
||||
angle,
|
||||
),
|
||||
pointRotateRads<GlobalPoint>(
|
||||
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||
center,
|
||||
angle,
|
||||
),
|
||||
),
|
||||
);
|
||||
i++;
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
|
||||
const getSegmentsOnEllipse = (
|
||||
ellipse: ExcalidrawEllipseElement,
|
||||
): LineSegment<GlobalPoint>[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
ellipse.x + ellipse.width / 2,
|
||||
ellipse.y + ellipse.height / 2,
|
||||
);
|
||||
|
||||
const a = ellipse.width / 2;
|
||||
const b = ellipse.height / 2;
|
||||
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
const points: GlobalPoint[] = [];
|
||||
const n = 90;
|
||||
const deltaT = (Math.PI * 2) / n;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = i * deltaT;
|
||||
const x = center[0] + a * Math.cos(t);
|
||||
const y = center[1] + b * Math.sin(t);
|
||||
points.push(pointRotateRads(pointFrom(x, y), center, ellipse.angle));
|
||||
}
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
segments.push(lineSegment(points[i], points[i + 1]));
|
||||
}
|
||||
|
||||
segments.push(lineSegment(points[points.length - 1], points[0]));
|
||||
return segments;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
||||
*
|
||||
|
@ -828,10 +944,10 @@ export const getElementBounds = (
|
|||
};
|
||||
|
||||
export const getCommonBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: ElementsMapOrArray,
|
||||
elementsMap?: ElementsMap,
|
||||
): Bounds => {
|
||||
if (!elements.length) {
|
||||
if (!sizeOf(elements)) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { isTransparent } from "@excalidraw/common";
|
||||
import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
||||
import {
|
||||
curveIntersectLineSegment,
|
||||
isPointWithinBounds,
|
||||
|
@ -16,7 +16,7 @@ import {
|
|||
} from "@excalidraw/math/ellipse";
|
||||
|
||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||
import { getPolygonShape } from "@excalidraw/utils/shape";
|
||||
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
||||
|
||||
import type {
|
||||
GlobalPoint,
|
||||
|
@ -26,8 +26,6 @@ import type {
|
|||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
||||
|
||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getBoundTextShape, isPathALoop } from "./shapes";
|
||||
|
@ -191,10 +189,7 @@ const intersectRectanguloidWithLineSegment = (
|
|||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||
// instead. It's all the same distance-wise.
|
||||
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||
|
@ -253,10 +248,7 @@ const intersectDiamondWithLineSegment = (
|
|||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||
// points. It's all the same distance-wise.
|
||||
|
@ -304,10 +296,7 @@ const intersectEllipseWithLineSegment = (
|
|||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
} from "@excalidraw/math";
|
||||
import { type Point } from "points-on-curve";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
getResizedElementAbsoluteCoords,
|
||||
|
@ -61,7 +63,7 @@ export const cropElement = (
|
|||
|
||||
const rotatedPointer = pointRotateRads(
|
||||
pointFrom(pointerX, pointerY),
|
||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
||||
elementCenterPoint(element),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import {
|
||||
curvePointDistance,
|
||||
distanceToLineSegment,
|
||||
pointFrom,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
|
@ -53,10 +54,7 @@ const distanceToRectanguloidElement = (
|
|||
element: ExcalidrawRectanguloidElement,
|
||||
p: GlobalPoint,
|
||||
) => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||
// instead. It's all the same distance-wise.
|
||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||
|
@ -84,10 +82,7 @@ const distanceToDiamondElement = (
|
|||
element: ExcalidrawDiamondElement,
|
||||
p: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||
// points. It's all the same distance-wise.
|
||||
|
@ -115,10 +110,7 @@ const distanceToEllipseElement = (
|
|||
element: ExcalidrawEllipseElement,
|
||||
p: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
return ellipseDistanceFromPoint(
|
||||
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||
pointRotateRads(p, center, -element.angle as Radians),
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
|
||||
import { fixBindingsAfterDuplication } from "./binding";
|
||||
import { fixDuplicatedBindingsAfterDuplication } from "./binding";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
|
@ -57,16 +57,14 @@ import type {
|
|||
* multiple elements at once, share this map
|
||||
* amongst all of them
|
||||
* @param element Element to duplicate
|
||||
* @param overrides Any element properties to override
|
||||
*/
|
||||
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||
editingGroupId: AppState["editingGroupId"],
|
||||
groupIdMapForOperation: Map<GroupId, GroupId>,
|
||||
element: TElement,
|
||||
overrides?: Partial<TElement>,
|
||||
randomizeSeed?: boolean,
|
||||
): Readonly<TElement> => {
|
||||
let copy = deepCopyElement(element);
|
||||
const copy = deepCopyElement(element);
|
||||
|
||||
if (isTestEnv()) {
|
||||
__test__defineOrigId(copy, element.id);
|
||||
|
@ -89,9 +87,6 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
|
|||
return groupIdMapForOperation.get(groupId)!;
|
||||
},
|
||||
);
|
||||
if (overrides) {
|
||||
copy = Object.assign(copy, overrides);
|
||||
}
|
||||
return copy;
|
||||
};
|
||||
|
||||
|
@ -99,9 +94,14 @@ export const duplicateElements = (
|
|||
opts: {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
randomizeSeed?: boolean;
|
||||
overrides?: (
|
||||
originalElement: ExcalidrawElement,
|
||||
) => Partial<ExcalidrawElement>;
|
||||
overrides?: (data: {
|
||||
duplicateElement: ExcalidrawElement;
|
||||
origElement: ExcalidrawElement;
|
||||
origIdToDuplicateId: Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement["id"]
|
||||
>;
|
||||
}) => Partial<ExcalidrawElement>;
|
||||
} & (
|
||||
| {
|
||||
/**
|
||||
|
@ -129,14 +129,6 @@ export const duplicateElements = (
|
|||
editingGroupId: AppState["editingGroupId"];
|
||||
selectedGroupIds: AppState["selectedGroupIds"];
|
||||
};
|
||||
/**
|
||||
* If true, duplicated elements are inserted _before_ specified
|
||||
* elements. Case: alt-dragging elements to duplicate them.
|
||||
*
|
||||
* TODO: remove this once (if) we stop replacing the original element
|
||||
* with the duplicated one in the scene array.
|
||||
*/
|
||||
reverseOrder: boolean;
|
||||
}
|
||||
),
|
||||
) => {
|
||||
|
@ -150,8 +142,6 @@ export const duplicateElements = (
|
|||
selectedGroupIds: {},
|
||||
} as const);
|
||||
|
||||
const reverseOrder = opts.type === "in-place" ? opts.reverseOrder : false;
|
||||
|
||||
// Ids of elements that have already been processed so we don't push them
|
||||
// into the array twice if we end up backtracking when retrieving
|
||||
// discontiguous group of elements (can happen due to a bug, or in edge
|
||||
|
@ -164,10 +154,17 @@ export const duplicateElements = (
|
|||
// loop over them.
|
||||
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
||||
const groupIdMap = new Map();
|
||||
const newElements: ExcalidrawElement[] = [];
|
||||
const oldElements: ExcalidrawElement[] = [];
|
||||
const oldIdToDuplicatedId = new Map();
|
||||
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
|
||||
const duplicatedElements: ExcalidrawElement[] = [];
|
||||
const origElements: ExcalidrawElement[] = [];
|
||||
const origIdToDuplicateId = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement["id"]
|
||||
>();
|
||||
const duplicateIdToOrigElement = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement
|
||||
>();
|
||||
const duplicateElementsMap = new Map<string, ExcalidrawElement>();
|
||||
const elementsMap = arrayToMap(elements) as ElementsMap;
|
||||
const _idsOfElementsToDuplicate =
|
||||
opts.type === "in-place"
|
||||
|
@ -185,7 +182,7 @@ export const duplicateElements = (
|
|||
|
||||
elements = normalizeElementOrder(elements);
|
||||
|
||||
const elementsWithClones: ExcalidrawElement[] = elements.slice();
|
||||
const elementsWithDuplicates: ExcalidrawElement[] = elements.slice();
|
||||
|
||||
// helper functions
|
||||
// -------------------------------------------------------------------------
|
||||
|
@ -211,17 +208,17 @@ export const duplicateElements = (
|
|||
appState.editingGroupId,
|
||||
groupIdMap,
|
||||
element,
|
||||
opts.overrides?.(element),
|
||||
opts.randomizeSeed,
|
||||
);
|
||||
|
||||
processedIds.set(newElement.id, true);
|
||||
|
||||
duplicatedElementsMap.set(newElement.id, newElement);
|
||||
oldIdToDuplicatedId.set(element.id, newElement.id);
|
||||
duplicateElementsMap.set(newElement.id, newElement);
|
||||
origIdToDuplicateId.set(element.id, newElement.id);
|
||||
duplicateIdToOrigElement.set(newElement.id, element);
|
||||
|
||||
oldElements.push(element);
|
||||
newElements.push(newElement);
|
||||
origElements.push(element);
|
||||
duplicatedElements.push(newElement);
|
||||
|
||||
acc.push(newElement);
|
||||
return acc;
|
||||
|
@ -245,21 +242,12 @@ export const duplicateElements = (
|
|||
return;
|
||||
}
|
||||
|
||||
if (reverseOrder && index < 1) {
|
||||
elementsWithClones.unshift(...castArray(elements));
|
||||
if (index > elementsWithDuplicates.length - 1) {
|
||||
elementsWithDuplicates.push(...castArray(elements));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reverseOrder && index > elementsWithClones.length - 1) {
|
||||
elementsWithClones.push(...castArray(elements));
|
||||
return;
|
||||
}
|
||||
|
||||
elementsWithClones.splice(
|
||||
index + (reverseOrder ? 0 : 1),
|
||||
0,
|
||||
...castArray(elements),
|
||||
);
|
||||
elementsWithDuplicates.splice(index + 1, 0, ...castArray(elements));
|
||||
};
|
||||
|
||||
const frameIdsToDuplicate = new Set(
|
||||
|
@ -291,13 +279,9 @@ export const duplicateElements = (
|
|||
: [element],
|
||||
);
|
||||
|
||||
const targetIndex = reverseOrder
|
||||
? elementsWithClones.findIndex((el) => {
|
||||
return el.groupIds?.includes(groupId);
|
||||
})
|
||||
: findLastIndex(elementsWithClones, (el) => {
|
||||
return el.groupIds?.includes(groupId);
|
||||
});
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return el.groupIds?.includes(groupId);
|
||||
});
|
||||
|
||||
insertBeforeOrAfterIndex(targetIndex, copyElements(groupElements));
|
||||
continue;
|
||||
|
@ -315,7 +299,7 @@ export const duplicateElements = (
|
|||
|
||||
const frameChildren = getFrameChildren(elements, frameId);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return el.frameId === frameId || el.id === frameId;
|
||||
});
|
||||
|
||||
|
@ -332,7 +316,7 @@ export const duplicateElements = (
|
|||
if (hasBoundTextElement(element)) {
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return (
|
||||
el.id === element.id ||
|
||||
("containerId" in el && el.containerId === element.id)
|
||||
|
@ -341,7 +325,7 @@ export const duplicateElements = (
|
|||
|
||||
if (boundTextElement) {
|
||||
insertBeforeOrAfterIndex(
|
||||
targetIndex + (reverseOrder ? -1 : 0),
|
||||
targetIndex,
|
||||
copyElements([element, boundTextElement]),
|
||||
);
|
||||
} else {
|
||||
|
@ -354,7 +338,7 @@ export const duplicateElements = (
|
|||
if (isBoundToContainer(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return el.id === element.id || el.id === container?.id;
|
||||
});
|
||||
|
||||
|
@ -374,28 +358,46 @@ export const duplicateElements = (
|
|||
// -------------------------------------------------------------------------
|
||||
|
||||
insertBeforeOrAfterIndex(
|
||||
findLastIndex(elementsWithClones, (el) => el.id === element.id),
|
||||
findLastIndex(elementsWithDuplicates, (el) => el.id === element.id),
|
||||
copyElements(element),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fixBindingsAfterDuplication(
|
||||
newElements,
|
||||
oldIdToDuplicatedId,
|
||||
duplicatedElementsMap as NonDeletedSceneElementsMap,
|
||||
fixDuplicatedBindingsAfterDuplication(
|
||||
duplicatedElements,
|
||||
origIdToDuplicateId,
|
||||
duplicateElementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
|
||||
bindElementsToFramesAfterDuplication(
|
||||
elementsWithClones,
|
||||
oldElements,
|
||||
oldIdToDuplicatedId,
|
||||
elementsWithDuplicates,
|
||||
origElements,
|
||||
origIdToDuplicateId,
|
||||
);
|
||||
|
||||
if (opts.overrides) {
|
||||
for (const duplicateElement of duplicatedElements) {
|
||||
const origElement = duplicateIdToOrigElement.get(duplicateElement.id);
|
||||
if (origElement) {
|
||||
Object.assign(
|
||||
duplicateElement,
|
||||
opts.overrides({
|
||||
duplicateElement,
|
||||
origElement,
|
||||
origIdToDuplicateId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
newElements,
|
||||
elementsWithClones,
|
||||
duplicatedElements,
|
||||
duplicateElementsMap,
|
||||
elementsWithDuplicates,
|
||||
origIdToDuplicateId,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -41,30 +41,28 @@ import type {
|
|||
// --------------------------- Frame State ------------------------------------
|
||||
export const bindElementsToFramesAfterDuplication = (
|
||||
nextElements: readonly ExcalidrawElement[],
|
||||
oldElements: readonly ExcalidrawElement[],
|
||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||
origElements: readonly ExcalidrawElement[],
|
||||
origIdToDuplicateId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||
) => {
|
||||
const nextElementMap = arrayToMap(nextElements) as Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement
|
||||
>;
|
||||
|
||||
for (const element of oldElements) {
|
||||
for (const element of origElements) {
|
||||
if (element.frameId) {
|
||||
// use its frameId to get the new frameId
|
||||
const nextElementId = oldIdToDuplicatedId.get(element.id);
|
||||
const nextFrameId = oldIdToDuplicatedId.get(element.frameId);
|
||||
if (nextElementId) {
|
||||
const nextElement = nextElementMap.get(nextElementId);
|
||||
if (nextElement) {
|
||||
mutateElement(
|
||||
nextElement,
|
||||
{
|
||||
frameId: nextFrameId ?? element.frameId,
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
const nextElementId = origIdToDuplicateId.get(element.id);
|
||||
const nextFrameId = origIdToDuplicateId.get(element.frameId);
|
||||
const nextElement = nextElementId && nextElementMap.get(nextElementId);
|
||||
if (nextElement) {
|
||||
mutateElement(
|
||||
nextElement,
|
||||
{
|
||||
frameId: nextFrameId ?? null,
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { invariant, isDevEnv, isTestEnv } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
normalizeRadians,
|
||||
pointFrom,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
pointScaleFromOrigin,
|
||||
radiansToDegrees,
|
||||
pointsEqual,
|
||||
triangleIncludesPoint,
|
||||
vectorCross,
|
||||
vectorFromPoint,
|
||||
vectorScale,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
|
@ -13,7 +17,6 @@ import type {
|
|||
GlobalPoint,
|
||||
Triangle,
|
||||
Vector,
|
||||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { getCenterForBounds, type Bounds } from "./bounds";
|
||||
|
@ -26,24 +29,6 @@ export const HEADING_LEFT = [-1, 0] as Heading;
|
|||
export const HEADING_UP = [0, -1] as Heading;
|
||||
export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
|
||||
|
||||
export const headingForDiamond = <Point extends GlobalPoint | LocalPoint>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
) => {
|
||||
const angle = radiansToDegrees(
|
||||
normalizeRadians(Math.atan2(b[1] - a[1], b[0] - a[0]) as Radians),
|
||||
);
|
||||
|
||||
if (angle >= 315 || angle < 45) {
|
||||
return HEADING_UP;
|
||||
} else if (angle >= 45 && angle < 135) {
|
||||
return HEADING_RIGHT;
|
||||
} else if (angle >= 135 && angle < 225) {
|
||||
return HEADING_DOWN;
|
||||
}
|
||||
return HEADING_LEFT;
|
||||
};
|
||||
|
||||
export const vectorToHeading = (vec: Vector): Heading => {
|
||||
const [x, y] = vec;
|
||||
const absX = Math.abs(x);
|
||||
|
@ -76,6 +61,165 @@ export const headingIsHorizontal = (a: Heading) =>
|
|||
|
||||
export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
|
||||
|
||||
const headingForPointFromDiamondElement = (
|
||||
element: Readonly<ExcalidrawBindableElement>,
|
||||
aabb: Readonly<Bounds>,
|
||||
point: Readonly<GlobalPoint>,
|
||||
): Heading => {
|
||||
const midPoint = getCenterForBounds(aabb);
|
||||
|
||||
if (isDevEnv() || isTestEnv()) {
|
||||
invariant(
|
||||
element.width > 0 && element.height > 0,
|
||||
"Diamond element has no width or height",
|
||||
);
|
||||
invariant(
|
||||
!pointsEqual(midPoint, point),
|
||||
"The point is too close to the element mid point to determine heading",
|
||||
);
|
||||
}
|
||||
|
||||
const SHRINK = 0.95; // Rounded elements tolerance
|
||||
const top = pointFromVector(
|
||||
vectorScale(
|
||||
vectorFromPoint(
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x + element.width / 2, element.y),
|
||||
midPoint,
|
||||
element.angle,
|
||||
),
|
||||
midPoint,
|
||||
),
|
||||
SHRINK,
|
||||
),
|
||||
midPoint,
|
||||
);
|
||||
const right = pointFromVector(
|
||||
vectorScale(
|
||||
vectorFromPoint(
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width,
|
||||
element.y + element.height / 2,
|
||||
),
|
||||
midPoint,
|
||||
element.angle,
|
||||
),
|
||||
midPoint,
|
||||
),
|
||||
SHRINK,
|
||||
),
|
||||
midPoint,
|
||||
);
|
||||
const bottom = pointFromVector(
|
||||
vectorScale(
|
||||
vectorFromPoint(
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height,
|
||||
),
|
||||
midPoint,
|
||||
element.angle,
|
||||
),
|
||||
midPoint,
|
||||
),
|
||||
SHRINK,
|
||||
),
|
||||
midPoint,
|
||||
);
|
||||
const left = pointFromVector(
|
||||
vectorScale(
|
||||
vectorFromPoint(
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y + element.height / 2),
|
||||
midPoint,
|
||||
element.angle,
|
||||
),
|
||||
midPoint,
|
||||
),
|
||||
SHRINK,
|
||||
),
|
||||
midPoint,
|
||||
);
|
||||
|
||||
// Corners
|
||||
if (
|
||||
vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, right)) <=
|
||||
0 &&
|
||||
vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, left)) > 0
|
||||
) {
|
||||
return headingForPoint(top, midPoint);
|
||||
} else if (
|
||||
vectorCross(
|
||||
vectorFromPoint(point, right),
|
||||
vectorFromPoint(right, bottom),
|
||||
) <= 0 &&
|
||||
vectorCross(vectorFromPoint(point, right), vectorFromPoint(right, top)) > 0
|
||||
) {
|
||||
return headingForPoint(right, midPoint);
|
||||
} else if (
|
||||
vectorCross(
|
||||
vectorFromPoint(point, bottom),
|
||||
vectorFromPoint(bottom, left),
|
||||
) <= 0 &&
|
||||
vectorCross(
|
||||
vectorFromPoint(point, bottom),
|
||||
vectorFromPoint(bottom, right),
|
||||
) > 0
|
||||
) {
|
||||
return headingForPoint(bottom, midPoint);
|
||||
} else if (
|
||||
vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, top)) <=
|
||||
0 &&
|
||||
vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, bottom)) > 0
|
||||
) {
|
||||
return headingForPoint(left, midPoint);
|
||||
}
|
||||
|
||||
// Sides
|
||||
if (
|
||||
vectorCross(
|
||||
vectorFromPoint(point, midPoint),
|
||||
vectorFromPoint(top, midPoint),
|
||||
) <= 0 &&
|
||||
vectorCross(
|
||||
vectorFromPoint(point, midPoint),
|
||||
vectorFromPoint(right, midPoint),
|
||||
) > 0
|
||||
) {
|
||||
const p = element.width > element.height ? top : right;
|
||||
return headingForPoint(p, midPoint);
|
||||
} else if (
|
||||
vectorCross(
|
||||
vectorFromPoint(point, midPoint),
|
||||
vectorFromPoint(right, midPoint),
|
||||
) <= 0 &&
|
||||
vectorCross(
|
||||
vectorFromPoint(point, midPoint),
|
||||
vectorFromPoint(bottom, midPoint),
|
||||
) > 0
|
||||
) {
|
||||
const p = element.width > element.height ? bottom : right;
|
||||
return headingForPoint(p, midPoint);
|
||||
} else if (
|
||||
vectorCross(
|
||||
vectorFromPoint(point, midPoint),
|
||||
vectorFromPoint(bottom, midPoint),
|
||||
) <= 0 &&
|
||||
vectorCross(
|
||||
vectorFromPoint(point, midPoint),
|
||||
vectorFromPoint(left, midPoint),
|
||||
) > 0
|
||||
) {
|
||||
const p = element.width > element.height ? bottom : left;
|
||||
return headingForPoint(p, midPoint);
|
||||
}
|
||||
|
||||
const p = element.width > element.height ? top : left;
|
||||
return headingForPoint(p, midPoint);
|
||||
};
|
||||
|
||||
// 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.
|
||||
|
@ -89,74 +233,7 @@ export const headingForPointFromElement = <Point extends GlobalPoint>(
|
|||
const midPoint = getCenterForBounds(aabb);
|
||||
|
||||
if (element.type === "diamond") {
|
||||
if (p[0] < element.x) {
|
||||
return HEADING_LEFT;
|
||||
} else if (p[1] < element.y) {
|
||||
return HEADING_UP;
|
||||
} else if (p[0] > element.x + element.width) {
|
||||
return HEADING_RIGHT;
|
||||
} else if (p[1] > element.y + element.height) {
|
||||
return HEADING_DOWN;
|
||||
}
|
||||
|
||||
const top = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
pointFrom(element.x + element.width / 2, element.y),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
midPoint,
|
||||
element.angle,
|
||||
);
|
||||
const right = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
pointFrom(element.x + element.width, element.y + element.height / 2),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
midPoint,
|
||||
element.angle,
|
||||
);
|
||||
const bottom = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
pointFrom(element.x + element.width / 2, element.y + element.height),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
midPoint,
|
||||
element.angle,
|
||||
);
|
||||
const left = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
pointFrom(element.x, element.y + element.height / 2),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
midPoint,
|
||||
element.angle,
|
||||
);
|
||||
|
||||
if (
|
||||
triangleIncludesPoint<Point>([top, right, midPoint] as Triangle<Point>, p)
|
||||
) {
|
||||
return headingForDiamond(top, right);
|
||||
} else if (
|
||||
triangleIncludesPoint<Point>(
|
||||
[right, bottom, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
) {
|
||||
return headingForDiamond(right, bottom);
|
||||
} else if (
|
||||
triangleIncludesPoint<Point>(
|
||||
[bottom, left, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
) {
|
||||
return headingForDiamond(bottom, left);
|
||||
}
|
||||
|
||||
return headingForDiamond(left, top);
|
||||
return headingForPointFromDiamondElement(element, aabb, p);
|
||||
}
|
||||
|
||||
const topLeft = pointScaleFromOrigin(
|
||||
|
|
|
@ -133,6 +133,7 @@ export class LinearElementEditor {
|
|||
};
|
||||
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
||||
console.error("Linear element is not normalized", Error().stack);
|
||||
LinearElementEditor.normalizePoints(element);
|
||||
}
|
||||
|
||||
this.selectedPointsIndices = null;
|
||||
|
|
|
@ -7,13 +7,20 @@ import type {
|
|||
|
||||
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
||||
import { isElementInViewport } from "./sizeHelpers";
|
||||
import { isBoundToContainer, isFrameLikeElement } from "./typeChecks";
|
||||
import {
|
||||
isBoundToContainer,
|
||||
isFrameLikeElement,
|
||||
isLinearElement,
|
||||
} from "./typeChecks";
|
||||
import {
|
||||
elementOverlapsWithFrame,
|
||||
getContainingFrame,
|
||||
getFrameChildren,
|
||||
} from "./frame";
|
||||
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { selectGroupsForSelectedElements } from "./groups";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
|
@ -254,3 +261,48 @@ export const makeNextSelectedElementIds = (
|
|||
|
||||
return nextSelectedElementIds;
|
||||
};
|
||||
|
||||
const _getLinearElementEditor = (
|
||||
targetElements: readonly ExcalidrawElement[],
|
||||
) => {
|
||||
const linears = targetElements.filter(isLinearElement);
|
||||
if (linears.length === 1) {
|
||||
const linear = linears[0];
|
||||
const boundElements = linear.boundElements?.map((def) => def.id) ?? [];
|
||||
const onlySingleLinearSelected = targetElements.every(
|
||||
(el) => el.id === linear.id || boundElements.includes(el.id),
|
||||
);
|
||||
|
||||
if (onlySingleLinearSelected) {
|
||||
return new LinearElementEditor(linear);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getSelectionStateForElements = (
|
||||
targetElements: readonly ExcalidrawElement[],
|
||||
allElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
return {
|
||||
selectedLinearElement: _getLinearElementEditor(targetElements),
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: appState.editingGroupId,
|
||||
selectedElementIds: excludeElementsInFramesFromSelection(
|
||||
targetElements,
|
||||
).reduce((acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
allElements,
|
||||
appState,
|
||||
null,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
LINE_CONFIRM_THRESHOLD,
|
||||
ROUNDNESS,
|
||||
invariant,
|
||||
elementCenterPoint,
|
||||
} from "@excalidraw/common";
|
||||
import {
|
||||
isPoint,
|
||||
|
@ -297,7 +298,7 @@ export const aabbForElement = (
|
|||
midY: element.y + element.height / 2,
|
||||
};
|
||||
|
||||
const center = pointFrom(bbox.midX, bbox.midY);
|
||||
const center = elementCenterPoint(element);
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(bbox.minX, bbox.minY),
|
||||
center,
|
||||
|
|
|
@ -14,6 +14,7 @@ export const showSelectedShapeActions = (
|
|||
((appState.activeTool.type !== "custom" &&
|
||||
(appState.editingTextElement ||
|
||||
(appState.activeTool.type !== "selection" &&
|
||||
appState.activeTool.type !== "lasso" &&
|
||||
appState.activeTool.type !== "eraser" &&
|
||||
appState.activeTool.type !== "hand" &&
|
||||
appState.activeTool.type !== "laser"))) ||
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
TEXT_ALIGN,
|
||||
VERTICAL_ALIGN,
|
||||
getFontString,
|
||||
isProdEnv,
|
||||
invariant,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
@ -26,6 +28,8 @@ import {
|
|||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import type { Radians } from "../../math/src";
|
||||
|
||||
import type { MaybeTransformHandleType } from "./transformHandles";
|
||||
import type {
|
||||
ElementsMap,
|
||||
|
@ -44,13 +48,25 @@ export const redrawTextBoundingBox = (
|
|||
informMutation = true,
|
||||
) => {
|
||||
let maxWidth = undefined;
|
||||
|
||||
if (!isProdEnv()) {
|
||||
invariant(
|
||||
!container || !isArrowElement(container) || textElement.angle === 0,
|
||||
"text element angle must be 0 if bound to arrow container",
|
||||
);
|
||||
}
|
||||
|
||||
const boundTextUpdates = {
|
||||
x: textElement.x,
|
||||
y: textElement.y,
|
||||
text: textElement.text,
|
||||
width: textElement.width,
|
||||
height: textElement.height,
|
||||
angle: container?.angle ?? textElement.angle,
|
||||
angle: (container
|
||||
? isArrowElement(container)
|
||||
? 0
|
||||
: container.angle
|
||||
: textElement.angle) as Radians,
|
||||
};
|
||||
|
||||
boundTextUpdates.text = textElement.text;
|
||||
|
@ -335,7 +351,10 @@ export const getTextElementAngle = (
|
|||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawTextContainer | null,
|
||||
) => {
|
||||
if (!container || isArrowElement(container)) {
|
||||
if (isArrowElement(container)) {
|
||||
return 0;
|
||||
}
|
||||
if (!container) {
|
||||
return textElement.angle;
|
||||
}
|
||||
return container.angle;
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
type GlobalPoint,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import type { Curve, LineSegment } from "@excalidraw/math";
|
||||
|
||||
import { getCornerRadius } from "./shapes";
|
||||
|
@ -68,10 +70,7 @@ export function deconstructRectanguloidElement(
|
|||
return [sides, []];
|
||||
}
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const r = rectangle(
|
||||
pointFrom(element.x, element.y),
|
||||
|
@ -254,10 +253,7 @@ export function deconstructDiamondElement(
|
|||
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
||||
}
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||
pointFrom(element.x + topX, element.y + topY),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue