excalidraw/packages/math/src/segment.ts
Ryan Di ce267aa0d3
feat: lasso selection (#9169)
* lasso without 'real' shape detection

* select a single linear el

* improve ux

* feed segments to worker

* simplify path threshold adaptive to zoom

* add a tiny threshold for checks

* refactor code

* lasso tests

* fix: ts

* do not capture lasso tool

* try worker-loader in next config

* update config

* refactor

* lint

* feat: show active tool when using "more tools"

* keep lasso if selected from toolbar

* fix incorrect checks for resetting to selection

* shift for additive selection

* bound text related fixes

* lint

* keep alt toggled lasso selection if shift pressed

* fix regression

* fix 'dead' lassos

* lint

* use workerpool and polyfill

* fix worker bundled with window related code

* refactor

* add file extension for worker constructor error

* another attempt at constructor error

* attempt at build issue

* attempt with dynamic import

* test not importing from math

* narrow down imports

* Reusing existing workers infrastructure (fallback to the main thread, type-safety)

* Points on curve inside the shared chunk

* Give up on experimental code splitting

* Remove potentially unnecessary optimisation

* Removing workers as the complexit is much worse, while perf. does not seem to be much better

* fix selecting text containers and containing frames together

* render fill directly from animated trail

* do not re-render static when setting selected element ids in lasso

* remove unnecessary property

* tweak trail animation

* slice points to remove notch

* always start alt-lasso from initial point

* revert build & worker changes (unused)

* remove `lasso` from `hasStrokeColor`

* label change

* remove unused props

* remove unsafe optimization

* snaps

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
Co-authored-by: Marcel Mraz <marcel@excalidraw.com>
2025-04-07 16:44:25 +10:00

179 lines
3.9 KiB
TypeScript

import { line, linesIntersectAt } from "./line";
import {
isPoint,
pointCenter,
pointFromVector,
pointRotateRads,
} from "./point";
import { PRECISION } from "./utils";
import {
vectorAdd,
vectorCross,
vectorFromPoint,
vectorScale,
vectorSubtract,
} from "./vector";
import type { GlobalPoint, LineSegment, LocalPoint, Radians } from "./types";
/**
* Create a line segment from two points.
*
* @param points The two points delimiting the line segment on each end
* @returns The line segment delineated by the points
*/
export function lineSegment<P extends GlobalPoint | LocalPoint>(
a: P,
b: P,
): LineSegment<P> {
return [a, b] as LineSegment<P>;
}
/**
*
* @param segment
* @returns
*/
export const isLineSegment = <Point extends GlobalPoint | LocalPoint>(
segment: unknown,
): segment is LineSegment<Point> =>
Array.isArray(segment) &&
segment.length === 2 &&
isPoint(segment[0]) &&
isPoint(segment[0]);
/**
* Return the coordinates resulting from rotating the given line about an origin by an angle in radians
* note that when the origin is not given, the midpoint of the given line is used as the origin.
*
* @param l
* @param angle
* @param origin
* @returns
*/
export const lineSegmentRotate = <Point extends LocalPoint | GlobalPoint>(
l: LineSegment<Point>,
angle: Radians,
origin?: Point,
): LineSegment<Point> => {
return lineSegment(
pointRotateRads(l[0], origin || pointCenter(l[0], l[1]), angle),
pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle),
);
};
/**
* Calculates the point two line segments with a definite start and end point
* intersect at.
*/
export const segmentsIntersectAt = <Point extends GlobalPoint | LocalPoint>(
a: Readonly<LineSegment<Point>>,
b: Readonly<LineSegment<Point>>,
): Point | null => {
const a0 = vectorFromPoint(a[0]);
const a1 = vectorFromPoint(a[1]);
const b0 = vectorFromPoint(b[0]);
const b1 = vectorFromPoint(b[1]);
const r = vectorSubtract(a1, a0);
const s = vectorSubtract(b1, b0);
const denominator = vectorCross(r, s);
if (denominator === 0) {
return null;
}
const i = vectorSubtract(vectorFromPoint(b[0]), vectorFromPoint(a[0]));
const u = vectorCross(i, r) / denominator;
const t = vectorCross(i, s) / denominator;
if (u === 0) {
return null;
}
const p = vectorAdd(a0, vectorScale(r, t));
if (t >= 0 && t < 1 && u >= 0 && u < 1) {
return pointFromVector<Point>(p);
}
return null;
};
export const pointOnLineSegment = <Point extends LocalPoint | GlobalPoint>(
point: Point,
line: LineSegment<Point>,
threshold = PRECISION,
) => {
const distance = distanceToLineSegment(point, line);
if (distance === 0) {
return true;
}
return distance < threshold;
};
export const distanceToLineSegment = <Point extends LocalPoint | GlobalPoint>(
point: Point,
line: LineSegment<Point>,
) => {
const [x, y] = point;
const [[x1, y1], [x2, y2]] = line;
const A = x - x1;
const B = y - y1;
const C = x2 - x1;
const D = y2 - y1;
const dot = A * C + B * D;
const len_sq = C * C + D * D;
let param = -1;
if (len_sq !== 0) {
param = dot / len_sq;
}
let xx;
let yy;
if (param < 0) {
xx = x1;
yy = y1;
} else if (param > 1) {
xx = x2;
yy = y2;
} else {
xx = x1 + param * C;
yy = y1 + param * D;
}
const dx = x - xx;
const dy = y - yy;
return Math.sqrt(dx * dx + dy * dy);
};
/**
* Returns the intersection point of a segment and a line
*
* @param l
* @param s
* @returns
*/
export function lineSegmentIntersectionPoints<
Point extends GlobalPoint | LocalPoint,
>(
l: LineSegment<Point>,
s: LineSegment<Point>,
threshold?: number,
): Point | null {
const candidate = linesIntersectAt(line(l[0], l[1]), line(s[0], s[1]));
if (
!candidate ||
!pointOnLineSegment(candidate, s, threshold) ||
!pointOnLineSegment(candidate, l, threshold)
) {
return null;
}
return candidate;
}