refactor code

This commit is contained in:
Ryan Di 2025-02-28 16:12:11 +11:00
parent ba7e3439f4
commit 5d8fd603bb
4 changed files with 168 additions and 259 deletions

View file

@ -6,6 +6,8 @@ import type {
NonDeleted,
ExcalidrawTextElementWithContainer,
ElementsMap,
ExcalidrawRectanguloidElement,
ExcalidrawEllipseElement,
} from "./types";
import rough from "roughjs/bin/rough";
import type { Point as RoughPoint } from "roughjs/bin/geometry";
@ -25,6 +27,7 @@ import { LinearElementEditor } from "./linearElementEditor";
import { ShapeCache } from "../scene/ShapeCache";
import { arrayToMap, invariant } from "../utils";
import type {
Curve,
Degrees,
GlobalPoint,
LineSegment,
@ -40,6 +43,12 @@ import {
pointRotateRads,
} from "../../math";
import type { Mutable } from "../utility-types";
import { getElementShape } from "../shapes";
import { pointsOnBezierCurves } from "points-on-curve";
import {
deconstructDiamondElement,
deconstructRectanguloidElement,
} from "./utils";
export type RectangleBox = {
x: number;
@ -246,50 +255,69 @@ 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") {
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],
@ -302,28 +330,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),
@ -336,6 +342,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.
*

View file

@ -1,36 +1,19 @@
import { pointsOnBezierCurves } from "points-on-curve";
import {
type Curve,
type GlobalPoint,
type LineSegment,
type Radians,
lineSegment,
pointFrom,
pointRotateRads,
} from "../../math";
import { type GlobalPoint, type LineSegment, pointFrom } from "../../math";
import { AnimatedTrail } from "../animated-trail";
import { type AnimationFrameHandler } from "../animation-frame-handler";
import type App from "../components/App";
import { getElementLineSegments } from "../element/bounds";
import { LinearElementEditor } from "../element/linearElementEditor";
import { isFrameLikeElement, isLinearElement } from "../element/typeChecks";
import type {
ElementsMap,
ExcalidrawElement,
ExcalidrawEllipseElement,
ExcalidrawLinearElement,
ExcalidrawRectanguloidElement,
NonDeleted,
} from "../element/types";
import { getFrameChildren } from "../frame";
import { selectGroupsForSelectedElements } from "../groups";
import { getElementShape } from "../shapes";
import { arrayToMap, easeOut } from "../utils";
import type { LassoWorkerInput, LassoWorkerOutput } from "./worker";
import {
deconstructDiamondElement,
deconstructRectanguloidElement,
} from "../element/utils";
import { getElementAbsoluteCoords } from "../element";
import type { LassoWorkerInput, LassoWorkerOutput } from "./types";
export class LassoTrail extends AnimatedTrail {
private intersectedElements: Set<ExcalidrawElement["id"]> = new Set();
@ -187,178 +170,3 @@ export class LassoTrail extends AnimatedTrail {
this.worker?.terminate();
}
}
/**
* Given an element, return the line segments that make up the element.
*
* Uses helpers from /math
*/
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);
if (shape.type === "polycurve") {
const curves = shape.data;
const points = curves
.map((curve) => pointsOnBezierCurves(curve, 10))
.flat();
let i = 0;
const segments: LineSegment<GlobalPoint>[] = [];
while (i < points.length - 1) {
segments.push(
lineSegment(
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") {
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, , , w, e] = (
[
[x1, y1],
[x2, y1],
[x1, y2],
[x2, y2],
[cx, y1],
[cx, y2],
[x1, cy],
[x2, cy],
] as GlobalPoint[]
).map((point) => pointRotateRads(point, center, element.angle));
return [
lineSegment(nw, ne),
lineSegment(sw, se),
lineSegment(nw, sw),
lineSegment(ne, se),
lineSegment(nw, e),
lineSegment(sw, e),
lineSegment(ne, w),
lineSegment(se, w),
];
};
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;
};

View file

@ -0,0 +1,17 @@
import type { GlobalPoint, LineSegment } from "../../math";
import type { ExcalidrawElement } from "../element/types";
export type ElementsSegmentsMap = Map<string, LineSegment<GlobalPoint>[]>;
export type LassoWorkerInput = {
lassoPath: GlobalPoint[];
elements: readonly ExcalidrawElement[];
elementsSegments: ElementsSegmentsMap;
intersectedElements: Set<ExcalidrawElement["id"]>;
enclosedElements: Set<ExcalidrawElement["id"]>;
simplifyDistance: number;
};
export type LassoWorkerOutput = {
selectedElementIds: string[];
};

View file

@ -3,6 +3,11 @@ import { polygonFromPoints, polygonIncludesPoint } from "../../math/polygon";
import type { ExcalidrawElement } from "../element/types";
import { lineSegment, lineSegmentIntersectionPoints } from "../../math/segment";
import { simplify } from "points-on-curve";
import type {
ElementsSegmentsMap,
LassoWorkerInput,
LassoWorkerOutput,
} from "./types";
// variables to track processing state and latest input data
// for "backpressure" purposes
@ -71,21 +76,6 @@ const processInputData = () => {
}
};
type ElementsSegments = Map<string, LineSegment<GlobalPoint>[]>;
export type LassoWorkerInput = {
lassoPath: GlobalPoint[];
elements: readonly ExcalidrawElement[];
elementsSegments: ElementsSegments;
intersectedElements: Set<ExcalidrawElement["id"]>;
enclosedElements: Set<ExcalidrawElement["id"]>;
simplifyDistance: number;
};
export type LassoWorkerOutput = {
selectedElementIds: string[];
};
export const updateSelection = (input: LassoWorkerInput): LassoWorkerOutput => {
const {
lassoPath,
@ -96,7 +86,7 @@ export const updateSelection = (input: LassoWorkerInput): LassoWorkerOutput => {
simplifyDistance,
} = input;
// simplify the path to reduce the number of points
let path = simplify(lassoPath, simplifyDistance) as GlobalPoint[];
const path = simplify(lassoPath, simplifyDistance) as GlobalPoint[];
// close the path to form a polygon for enclosure check
const closedPath = polygonFromPoints(path);
// as the path might not enclose a shape anymore, clear before checking
@ -132,7 +122,7 @@ export const updateSelection = (input: LassoWorkerInput): LassoWorkerOutput => {
const enclosureTest = (
lassoPath: GlobalPoint[],
element: ExcalidrawElement,
elementsSegments: ElementsSegments,
elementsSegments: ElementsSegmentsMap,
): boolean => {
const lassoPolygon = polygonFromPoints(lassoPath);
const segments = elementsSegments.get(element.id);
@ -148,7 +138,7 @@ const enclosureTest = (
const intersectionTest = (
lassoPath: GlobalPoint[],
element: ExcalidrawElement,
elementsSegments: ElementsSegments,
elementsSegments: ElementsSegmentsMap,
): boolean => {
const elementSegments = elementsSegments.get(element.id);
if (!elementSegments) {