mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: Remove GA code from binding (#9042)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
31e8476c78
commit
0ffeaeaecf
44 changed files with 2112 additions and 1832 deletions
|
@ -1,5 +1,7 @@
|
|||
import { pointFrom, pointRotateRads } from "./point";
|
||||
import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
|
||||
import type { Bounds } from "../excalidraw/element/bounds";
|
||||
import { isPoint, pointDistance, pointFrom } from "./point";
|
||||
import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
|
||||
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -18,206 +20,263 @@ export function curve<Point extends GlobalPoint | LocalPoint>(
|
|||
return [a, b, c, d] as Curve<Point>;
|
||||
}
|
||||
|
||||
export const curveRotate = <Point extends LocalPoint | GlobalPoint>(
|
||||
curve: Curve<Point>,
|
||||
angle: Radians,
|
||||
origin: Point,
|
||||
) => {
|
||||
return curve.map((p) => pointRotateRads(p, origin, angle));
|
||||
};
|
||||
function gradient(
|
||||
f: (t: number, s: number) => number,
|
||||
t0: number,
|
||||
s0: number,
|
||||
delta: number = 1e-6,
|
||||
): number[] {
|
||||
return [
|
||||
(f(t0 + delta, s0) - f(t0 - delta, s0)) / (2 * delta),
|
||||
(f(t0, s0 + delta) - f(t0, s0 - delta)) / (2 * delta),
|
||||
];
|
||||
}
|
||||
|
||||
function solve(
|
||||
f: (t: number, s: number) => [number, number],
|
||||
t0: number,
|
||||
s0: number,
|
||||
tolerance: number = 1e-3,
|
||||
iterLimit: number = 10,
|
||||
): number[] | null {
|
||||
let error = Infinity;
|
||||
let iter = 0;
|
||||
|
||||
while (error >= tolerance) {
|
||||
if (iter >= iterLimit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const y0 = f(t0, s0);
|
||||
const jacobian = [
|
||||
gradient((t, s) => f(t, s)[0], t0, s0),
|
||||
gradient((t, s) => f(t, s)[1], t0, s0),
|
||||
];
|
||||
const b = [[-y0[0]], [-y0[1]]];
|
||||
const det =
|
||||
jacobian[0][0] * jacobian[1][1] - jacobian[0][1] * jacobian[1][0];
|
||||
|
||||
if (det === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iJ = [
|
||||
[jacobian[1][1] / det, -jacobian[0][1] / det],
|
||||
[-jacobian[1][0] / det, jacobian[0][0] / det],
|
||||
];
|
||||
const h = [
|
||||
[iJ[0][0] * b[0][0] + iJ[0][1] * b[1][0]],
|
||||
[iJ[1][0] * b[0][0] + iJ[1][1] * b[1][0]],
|
||||
];
|
||||
|
||||
t0 = t0 + h[0][0];
|
||||
s0 = s0 + h[1][0];
|
||||
|
||||
const [tErr, sErr] = f(t0, s0);
|
||||
error = Math.max(Math.abs(tErr), Math.abs(sErr));
|
||||
iter += 1;
|
||||
}
|
||||
|
||||
return [t0, s0];
|
||||
}
|
||||
|
||||
const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
t: number,
|
||||
) =>
|
||||
pointFrom<Point>(
|
||||
(1 - t) ** 3 * c[0][0] +
|
||||
3 * (1 - t) ** 2 * t * c[1][0] +
|
||||
3 * (1 - t) * t ** 2 * c[2][0] +
|
||||
t ** 3 * c[3][0],
|
||||
(1 - t) ** 3 * c[0][1] +
|
||||
3 * (1 - t) ** 2 * t * c[1][1] +
|
||||
3 * (1 - t) * t ** 2 * c[2][1] +
|
||||
t ** 3 * c[3][1],
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pointsIn
|
||||
* @param curveTightness
|
||||
* @returns
|
||||
* Computes the intersection between a cubic spline and a line segment.
|
||||
*/
|
||||
export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
|
||||
pointsIn: readonly Point[],
|
||||
curveTightness = 0,
|
||||
): Point[] {
|
||||
const len = pointsIn.length;
|
||||
if (len < 3) {
|
||||
throw new Error("A curve must have at least three points.");
|
||||
export function curveIntersectLineSegment<
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(c: Curve<Point>, l: LineSegment<Point>): Point[] {
|
||||
// Optimize by doing a cheap bounding box check first
|
||||
const bounds = curveBounds(c);
|
||||
if (
|
||||
rectangleIntersectLineSegment(
|
||||
rectangle(
|
||||
pointFrom(bounds[0], bounds[1]),
|
||||
pointFrom(bounds[2], bounds[3]),
|
||||
),
|
||||
l,
|
||||
).length === 0
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const out: Point[] = [];
|
||||
if (len === 3) {
|
||||
out.push(
|
||||
pointFrom(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
|
||||
pointFrom(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
|
||||
pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
||||
pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
|
||||
|
||||
const line = (s: number) =>
|
||||
pointFrom<Point>(
|
||||
l[0][0] + s * (l[1][0] - l[0][0]),
|
||||
l[0][1] + s * (l[1][1] - l[0][1]),
|
||||
);
|
||||
} else {
|
||||
const points: Point[] = [];
|
||||
points.push(pointsIn[0], pointsIn[0]);
|
||||
for (let i = 1; i < pointsIn.length; i++) {
|
||||
points.push(pointsIn[i]);
|
||||
if (i === pointsIn.length - 1) {
|
||||
points.push(pointsIn[i]);
|
||||
}
|
||||
|
||||
const initial_guesses: [number, number][] = [
|
||||
[0.5, 0],
|
||||
[0.2, 0],
|
||||
[0.8, 0],
|
||||
];
|
||||
|
||||
const calculate = ([t0, s0]: [number, number]) => {
|
||||
const solution = solve(
|
||||
(t: number, s: number) => {
|
||||
const bezier_point = bezierEquation(c, t);
|
||||
const line_point = line(s);
|
||||
|
||||
return [
|
||||
bezier_point[0] - line_point[0],
|
||||
bezier_point[1] - line_point[1],
|
||||
];
|
||||
},
|
||||
t0,
|
||||
s0,
|
||||
);
|
||||
|
||||
if (!solution) {
|
||||
return null;
|
||||
}
|
||||
const b: Point[] = [];
|
||||
const s = 1 - curveTightness;
|
||||
out.push(pointFrom(points[0][0], points[0][1]));
|
||||
for (let i = 1; i + 2 < points.length; i++) {
|
||||
const cachedVertArray = points[i];
|
||||
b[0] = pointFrom(cachedVertArray[0], cachedVertArray[1]);
|
||||
b[1] = pointFrom(
|
||||
cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
|
||||
cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
|
||||
);
|
||||
b[2] = pointFrom(
|
||||
points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
|
||||
points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
|
||||
);
|
||||
b[3] = pointFrom(points[i + 1][0], points[i + 1][1]);
|
||||
out.push(b[1], b[2], b[3]);
|
||||
|
||||
const [t, s] = solution;
|
||||
|
||||
if (t < 0 || t > 1 || s < 0 || s > 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bezierEquation(c, t);
|
||||
};
|
||||
|
||||
let solution = calculate(initial_guesses[0]);
|
||||
if (solution) {
|
||||
return [solution];
|
||||
}
|
||||
return out;
|
||||
|
||||
solution = calculate(initial_guesses[1]);
|
||||
if (solution) {
|
||||
return [solution];
|
||||
}
|
||||
|
||||
solution = calculate(initial_guesses[2]);
|
||||
if (solution) {
|
||||
return [solution];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the closest point on the Bezier curve from another point
|
||||
*
|
||||
* @param t
|
||||
* @param controlPoints
|
||||
* @param x
|
||||
* @param y
|
||||
* @param P0
|
||||
* @param P1
|
||||
* @param P2
|
||||
* @param P3
|
||||
* @param tolerance
|
||||
* @param maxLevel
|
||||
* @returns
|
||||
*/
|
||||
export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||
t: number,
|
||||
controlPoints: Curve<Point>,
|
||||
): Point => {
|
||||
const [p0, p1, p2, p3] = controlPoints;
|
||||
export function curveClosestPoint<Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
p: Point,
|
||||
tolerance: number = 1e-3,
|
||||
): Point | null {
|
||||
const localMinimum = (
|
||||
min: number,
|
||||
max: number,
|
||||
f: (t: number) => number,
|
||||
e: number = tolerance,
|
||||
) => {
|
||||
let m = min;
|
||||
let n = max;
|
||||
let k;
|
||||
|
||||
const x =
|
||||
Math.pow(1 - t, 3) * p0[0] +
|
||||
3 * Math.pow(1 - t, 2) * t * p1[0] +
|
||||
3 * (1 - t) * Math.pow(t, 2) * p2[0] +
|
||||
Math.pow(t, 3) * p3[0];
|
||||
|
||||
const y =
|
||||
Math.pow(1 - t, 3) * p0[1] +
|
||||
3 * Math.pow(1 - t, 2) * t * p1[1] +
|
||||
3 * (1 - t) * Math.pow(t, 2) * p2[1] +
|
||||
Math.pow(t, 3) * p3[1];
|
||||
|
||||
return pointFrom(x, y);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param point
|
||||
* @param controlPoints
|
||||
* @returns
|
||||
*/
|
||||
export const cubicBezierDistance = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
controlPoints: Curve<Point>,
|
||||
) => {
|
||||
// Calculate the closest point on the Bezier curve to the given point
|
||||
const t = findClosestParameter(point, controlPoints);
|
||||
|
||||
// Calculate the coordinates of the closest point on the curve
|
||||
const [closestX, closestY] = cubicBezierPoint(t, controlPoints);
|
||||
|
||||
// Calculate the distance between the given point and the closest point on the curve
|
||||
const distance = Math.sqrt(
|
||||
(point[0] - closestX) ** 2 + (point[1] - closestY) ** 2,
|
||||
);
|
||||
|
||||
return distance;
|
||||
};
|
||||
|
||||
const solveCubic = (a: number, b: number, c: number, d: number) => {
|
||||
// This function solves the cubic equation ax^3 + bx^2 + cx + d = 0
|
||||
const roots: number[] = [];
|
||||
|
||||
const discriminant =
|
||||
18 * a * b * c * d -
|
||||
4 * Math.pow(b, 3) * d +
|
||||
Math.pow(b, 2) * Math.pow(c, 2) -
|
||||
4 * a * Math.pow(c, 3) -
|
||||
27 * Math.pow(a, 2) * Math.pow(d, 2);
|
||||
|
||||
if (discriminant >= 0) {
|
||||
const C = Math.cbrt((discriminant + Math.sqrt(discriminant)) / 2);
|
||||
const D = Math.cbrt((discriminant - Math.sqrt(discriminant)) / 2);
|
||||
|
||||
const root1 = (-b - C - D) / (3 * a);
|
||||
const root2 = (-b + (C + D) / 2) / (3 * a);
|
||||
const root3 = (-b + (C + D) / 2) / (3 * a);
|
||||
|
||||
roots.push(root1, root2, root3);
|
||||
} else {
|
||||
const realPart = -b / (3 * a);
|
||||
|
||||
const root1 =
|
||||
2 * Math.sqrt(-b / (3 * a)) * Math.cos(Math.acos(realPart) / 3);
|
||||
const root2 =
|
||||
2 *
|
||||
Math.sqrt(-b / (3 * a)) *
|
||||
Math.cos((Math.acos(realPart) + 2 * Math.PI) / 3);
|
||||
const root3 =
|
||||
2 *
|
||||
Math.sqrt(-b / (3 * a)) *
|
||||
Math.cos((Math.acos(realPart) + 4 * Math.PI) / 3);
|
||||
|
||||
roots.push(root1, root2, root3);
|
||||
}
|
||||
|
||||
return roots;
|
||||
};
|
||||
|
||||
const findClosestParameter = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
controlPoints: Curve<Point>,
|
||||
) => {
|
||||
// This function finds the parameter t that minimizes the distance between the point
|
||||
// and any point on the cubic Bezier curve.
|
||||
|
||||
const [p0, p1, p2, p3] = controlPoints;
|
||||
|
||||
// Use the direct formula to find the parameter t
|
||||
const a = p3[0] - 3 * p2[0] + 3 * p1[0] - p0[0];
|
||||
const b = 3 * p2[0] - 6 * p1[0] + 3 * p0[0];
|
||||
const c = 3 * p1[0] - 3 * p0[0];
|
||||
const d = p0[0] - point[0];
|
||||
|
||||
const rootsX = solveCubic(a, b, c, d);
|
||||
|
||||
// Do the same for the y-coordinate
|
||||
const e = p3[1] - 3 * p2[1] + 3 * p1[1] - p0[1];
|
||||
const f = 3 * p2[1] - 6 * p1[1] + 3 * p0[1];
|
||||
const g = 3 * p1[1] - 3 * p0[1];
|
||||
const h = p0[1] - point[1];
|
||||
|
||||
const rootsY = solveCubic(e, f, g, h);
|
||||
|
||||
// Select the real root that is between 0 and 1 (inclusive)
|
||||
const validRootsX = rootsX.filter((root) => root >= 0 && root <= 1);
|
||||
const validRootsY = rootsY.filter((root) => root >= 0 && root <= 1);
|
||||
|
||||
if (validRootsX.length === 0 || validRootsY.length === 0) {
|
||||
// No valid roots found, use the midpoint as a fallback
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
// Choose the parameter t that minimizes the distance
|
||||
let minDistance = Infinity;
|
||||
let closestT = 0;
|
||||
|
||||
for (const rootX of validRootsX) {
|
||||
for (const rootY of validRootsY) {
|
||||
const distance = Math.sqrt(
|
||||
(rootX - point[0]) ** 2 + (rootY - point[1]) ** 2,
|
||||
);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestT = (rootX + rootY) / 2; // Use the average for a smoother result
|
||||
while (n - m > e) {
|
||||
k = (n + m) / 2;
|
||||
if (f(k - e) < f(k + e)) {
|
||||
n = k;
|
||||
} else {
|
||||
m = k;
|
||||
}
|
||||
}
|
||||
|
||||
return k;
|
||||
};
|
||||
|
||||
const maxSteps = 30;
|
||||
let closestStep = 0;
|
||||
for (let min = Infinity, step = 0; step < maxSteps; step++) {
|
||||
const d = pointDistance(p, bezierEquation(c, step / maxSteps));
|
||||
if (d < min) {
|
||||
min = d;
|
||||
closestStep = step;
|
||||
}
|
||||
}
|
||||
|
||||
return closestT;
|
||||
};
|
||||
const t0 = Math.max((closestStep - 1) / maxSteps, 0);
|
||||
const t1 = Math.min((closestStep + 1) / maxSteps, 1);
|
||||
const solution = localMinimum(t0, t1, (t) =>
|
||||
pointDistance(p, bezierEquation(c, t)),
|
||||
);
|
||||
|
||||
if (!solution) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bezierEquation(c, solution);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the distance between a point and the closest point on the
|
||||
* Bezier curve.
|
||||
*
|
||||
* @param c The curve to test
|
||||
* @param p The point to measure from
|
||||
*/
|
||||
export function curvePointDistance<Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
p: Point,
|
||||
) {
|
||||
const closest = curveClosestPoint(c, p);
|
||||
|
||||
if (!closest) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return pointDistance(p, closest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the parameter is a Curve
|
||||
*/
|
||||
export function isCurve<P extends GlobalPoint | LocalPoint>(
|
||||
v: unknown,
|
||||
): v is Curve<P> {
|
||||
return (
|
||||
Array.isArray(v) &&
|
||||
v.length === 4 &&
|
||||
isPoint(v[0]) &&
|
||||
isPoint(v[1]) &&
|
||||
isPoint(v[2]) &&
|
||||
isPoint(v[3])
|
||||
);
|
||||
}
|
||||
|
||||
function curveBounds<Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
): Bounds {
|
||||
const [P0, P1, P2, P3] = c;
|
||||
const x = [P0[0], P1[0], P2[0], P3[0]];
|
||||
const y = [P0[1], P1[1], P2[1], P3[1]];
|
||||
return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)];
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue