Files
AFFiNE-Mirror/blocksuite/framework/global/src/utils/math.ts
T
2024-12-20 15:38:06 +08:00

539 lines
13 KiB
TypeScript

import type { Bound, IBound } from './model/bound.js';
import { PointLocation } from './model/point-location.js';
import { type IVec, Vec } from './model/vec.js';
export const EPSILON = 1e-12;
export const MACHINE_EPSILON = 1.12e-16;
export const PI2 = Math.PI * 2;
export const CURVETIME_EPSILON = 1e-8;
export function randomSeed(): number {
return Math.floor(Math.random() * 2 ** 31);
}
export function lineIntersects(
sp: IVec,
ep: IVec,
sp2: IVec,
ep2: IVec,
infinite = false
): IVec | null {
const v1 = Vec.sub(ep, sp);
const v2 = Vec.sub(ep2, sp2);
const cross = Vec.cpr(v1, v2);
// Avoid divisions by 0, and errors when getting too close to 0
if (almostEqual(cross, 0, MACHINE_EPSILON)) return null;
const d = Vec.sub(sp, sp2);
let u1 = Vec.cpr(v2, d) / cross;
const u2 = Vec.cpr(v1, d) / cross,
// Check the ranges of the u parameters if the line is not
// allowed to extend beyond the definition points, but
// compare with EPSILON tolerance over the [0, 1] bounds.
epsilon = /*#=*/ EPSILON,
uMin = -epsilon,
uMax = 1 + epsilon;
if (infinite || (uMin < u1 && u1 < uMax && uMin < u2 && u2 < uMax)) {
// Address the tolerance at the bounds by clipping to
// the actual range.
if (!infinite) {
u1 = clamp(u1, 0, 1);
}
return Vec.lrp(sp, ep, u1);
}
return null;
}
export function polygonNearestPoint(points: IVec[], point: IVec) {
const len = points.length;
let rst: IVec;
let dis = Infinity;
for (let i = 0; i < len; i++) {
const p = points[i];
const p2 = points[(i + 1) % len];
const temp = Vec.nearestPointOnLineSegment(p, p2, point, true);
const curDis = Vec.dist(temp, point);
if (curDis < dis) {
dis = curDis;
rst = temp;
}
}
return rst!;
}
export function polygonPointDistance(points: IVec[], point: IVec) {
const nearest = polygonNearestPoint(points, point);
return Vec.dist(nearest, point);
}
export function rotatePoints<T extends IVec>(
points: T[],
center: IVec,
rotate: number
): T[] {
const rad = toRadian(rotate);
return points.map(p => Vec.rotWith(p, center, rad)) as T[];
}
export function rotatePoint(
point: [number, number],
center: IVec,
rotate: number
): [number, number] {
const rad = toRadian(rotate);
return Vec.add(center, Vec.rot(Vec.sub(point, center), rad)) as [
number,
number,
];
}
export function toRadian(angle: number) {
return (angle * Math.PI) / 180;
}
export function isPointOnLineSegment(point: IVec, line: IVec[]) {
const [sp, ep] = line;
const v1 = Vec.sub(point, sp);
const v2 = Vec.sub(point, ep);
return almostEqual(Vec.cpr(v1, v2), 0, 0.01) && Vec.dpr(v1, v2) <= 0;
}
export function polygonGetPointTangent(points: IVec[], point: IVec): IVec {
const len = points.length;
for (let i = 0; i < len; i++) {
const p = points[i];
const p2 = points[(i + 1) % len];
if (isPointOnLineSegment(point, [p, p2])) {
return Vec.normalize(Vec.sub(p2, p));
}
}
return [0, 0];
}
export function linePolygonIntersects(
sp: IVec,
ep: IVec,
points: IVec[]
): PointLocation[] | null {
const result: PointLocation[] = [];
const len = points.length;
for (let i = 0; i < len; i++) {
const p = points[i];
const p2 = points[(i + 1) % len];
const rst = lineIntersects(sp, ep, p, p2);
if (rst) {
const v = new PointLocation(rst);
v.tangent = Vec.normalize(Vec.sub(p2, p));
result.push(v);
}
}
return result.length ? result : null;
}
export function linePolylineIntersects(
sp: IVec,
ep: IVec,
points: IVec[]
): PointLocation[] | null {
const result: PointLocation[] = [];
const len = points.length;
for (let i = 0; i < len - 1; i++) {
const p = points[i];
const p2 = points[i + 1];
const rst = lineIntersects(sp, ep, p, p2);
if (rst) {
result.push(new PointLocation(rst, Vec.normalize(Vec.sub(p2, p))));
}
}
return result.length ? result : null;
}
export function polyLineNearestPoint(points: IVec[], point: IVec) {
const len = points.length;
let rst: IVec;
let dis = Infinity;
for (let i = 0; i < len - 1; i++) {
const p = points[i];
const p2 = points[i + 1];
const temp = Vec.nearestPointOnLineSegment(p, p2, point, true);
const curDis = Vec.dist(temp, point);
if (curDis < dis) {
dis = curDis;
rst = temp;
}
}
return rst!;
}
export function isPointOnlines(
element: Bound,
points: readonly [number, number][],
rotate: number,
hitPoint: [number, number],
threshold: number
): boolean {
// credit to Excalidraw hitTestFreeDrawElement
let x: number;
let y: number;
if (rotate === 0) {
x = hitPoint[0] - element.x;
y = hitPoint[1] - element.y;
} else {
// Counter-rotate the point around center before testing
const { minX, minY, maxX, maxY } = element;
const rotatedPoint = rotatePoint(
hitPoint,
[minX + (maxX - minX) / 2, minY + (maxY - minY) / 2],
-rotate
) as [number, number];
x = rotatedPoint[0] - element.x;
y = rotatedPoint[1] - element.y;
}
let [A, B] = points;
let P: readonly [number, number];
// For freedraw dots
if (
distance2d(A[0], A[1], x, y) < threshold ||
distance2d(B[0], B[1], x, y) < threshold
) {
return true;
}
// For freedraw lines
for (let i = 0; i < points.length; i++) {
const delta = [B[0] - A[0], B[1] - A[1]];
const length = Math.hypot(delta[1], delta[0]);
const U = [delta[0] / length, delta[1] / length];
const C = [x - A[0], y - A[1]];
const d = (C[0] * U[0] + C[1] * U[1]) / Math.hypot(U[1], U[0]);
P = [A[0] + U[0] * d, A[1] + U[1] * d];
const da = distance2d(P[0], P[1], A[0], A[1]);
const db = distance2d(P[0], P[1], B[0], B[1]);
P = db < da && da > length ? B : da < db && db > length ? A : P;
if (Math.hypot(y - P[1], x - P[0]) < threshold) {
return true;
}
A = B;
B = points[i + 1];
}
return false;
}
export const distance2d = (x1: number, y1: number, x2: number, y2: number) => {
const xd = x2 - x1;
const yd = y2 - y1;
return Math.hypot(xd, yd);
};
function square(num: number) {
return num * num;
}
function sumSqr(v: IVec, w: IVec) {
return square(v[0] - w[0]) + square(v[1] - w[1]);
}
function distToSegmentSquared(p: IVec, v: IVec, w: IVec) {
const l2 = sumSqr(v, w);
if (l2 == 0) return sumSqr(p, v);
let t = ((p[0] - v[0]) * (w[0] - v[0]) + (p[1] - v[1]) * (w[1] - v[1])) / l2;
t = Math.max(0, Math.min(1, t));
return sumSqr(p, [v[0] + t * (w[0] - v[0]), v[1] + t * (w[1] - v[1])]);
}
function distToSegment(p: IVec, v: IVec, w: IVec) {
return Math.sqrt(distToSegmentSquared(p, v, w));
}
export function isPointIn(a: IBound, x: number, y: number): boolean {
return a.x <= x && x <= a.x + a.w && a.y <= y && y <= a.y + a.h;
}
export function intersects(a: IBound, b: IBound): boolean {
return (
a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y
);
}
export function almostEqual(a: number, b: number, epsilon = 0.0001) {
return Math.abs(a - b) < epsilon;
}
export function isVecZero(v: IVec) {
return v.every(n => isZero(n));
}
export function isZero(x: number) {
return x >= -EPSILON && x <= EPSILON;
}
export function pointAlmostEqual(a: IVec, b: IVec, _epsilon = 0.0001) {
return a.length === b.length && a.every((v, i) => almostEqual(v, b[i]));
}
export function clamp(n: number, min: number, max?: number): number {
return Math.max(min, max !== undefined ? Math.min(n, max) : n);
}
export function pointInEllipse(
A: IVec,
C: IVec,
rx: number,
ry: number,
rotation = 0
): boolean {
const cos = Math.cos(rotation);
const sin = Math.sin(rotation);
const delta = Vec.sub(A, C);
const tdx = cos * delta[0] + sin * delta[1];
const tdy = sin * delta[0] - cos * delta[1];
return (tdx * tdx) / (rx * rx) + (tdy * tdy) / (ry * ry) <= 1;
}
export function pointInPolygon(p: IVec, points: IVec[]): boolean {
let wn = 0; // winding number
points.forEach((a, i) => {
const b = points[(i + 1) % points.length];
if (a[1] <= p[1]) {
if (b[1] > p[1] && Vec.cross(a, b, p) > 0) {
wn += 1;
}
} else if (b[1] <= p[1] && Vec.cross(a, b, p) < 0) {
wn -= 1;
}
});
return wn !== 0;
}
export function pointOnEllipse(
point: IVec,
rx: number,
ry: number,
threshold: number
): boolean {
// slope of point
const t = point[1] / point[0];
const squaredX =
(square(rx) * square(ry)) / (square(rx) * square(t) + square(ry));
const squaredY =
(square(rx) * square(ry) - square(ry) * squaredX) / square(rx);
return (
Math.abs(
Math.sqrt(square(point[1]) + square(point[0])) -
Math.sqrt(squaredX + squaredY)
) < threshold
);
}
export function pointOnPolygonStoke(
p: IVec,
points: IVec[],
threshold: number
): boolean {
for (let i = 0; i < points.length; ++i) {
const next = i + 1 === points.length ? 0 : i + 1;
if (distToSegment(p, points[i], points[next]) <= threshold) {
return true;
}
}
return false;
}
export function getPolygonPathFromPoints(
points: IVec[],
closed = true
): string {
const len = points.length;
if (len < 2) return ``;
const a = points[0];
const b = points[1];
let res = `M${a[0].toFixed(2)},${a[1].toFixed()}L${b[0].toFixed(2)},${b[1].toFixed()}`;
for (let i = 2; i < len; i++) {
const a = points[i];
res += `L${a[0].toFixed(2)},${a[1].toFixed()}`;
}
if (closed) res += 'Z';
return res;
}
export function getSvgPathFromStroke(points: IVec[], closed = true): string {
const len = points.length;
if (len < 4) {
return ``;
}
let a = points[0];
let b = points[1];
const c = points[2];
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(
2
)},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average(
b[1],
c[1]
).toFixed(2)} T`;
for (let i = 2, max = len - 1; i < max; i++) {
a = points[i];
b = points[i + 1];
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(
2
)} `;
}
if (closed) {
result += 'Z';
}
return result;
}
function average(a: number, b: number): number {
return (a + b) / 2;
}
//reference https://www.xarg.org/book/computer-graphics/line-segment-ellipse-intersection/
export function lineEllipseIntersects(
A: IVec,
B: IVec,
C: IVec,
rx: number,
ry: number,
rad = 0
) {
A = Vec.rot(Vec.sub(A, C), -rad);
B = Vec.rot(Vec.sub(B, C), -rad);
rx *= rx;
ry *= ry;
const rst: IVec[] = [];
const v = Vec.sub(B, A);
const a = rx * v[1] * v[1] + ry * v[0] * v[0];
const b = 2 * (rx * A[1] * v[1] + ry * A[0] * v[0]);
const c = rx * A[1] * A[1] + ry * A[0] * A[0] - rx * ry;
const D = b * b - 4 * a * c; // Discriminant
if (D >= 0) {
const sqrtD = Math.sqrt(D);
const t1 = (-b + sqrtD) / (2 * a);
const t2 = (-b - sqrtD) / (2 * a);
if (0 <= t1 && t1 <= 1)
rst.push(Vec.add(Vec.rot(Vec.add(Vec.mul(v, t1), A), rad), C));
if (0 <= t2 && t2 <= 1 && Math.abs(t1 - t2) > 1e-16)
rst.push(Vec.add(Vec.rot(Vec.add(Vec.mul(v, t2), A), rad), C));
}
if (rst.length === 0) return null;
return rst.map(v => {
const pl = new PointLocation(v);
const normalVector = Vec.uni(Vec.divV(Vec.sub(v, C), [rx * rx, ry * ry]));
pl.tangent = [-normalVector[1], normalVector[0]];
return pl;
});
}
export function sign(number: number) {
return number > 0 ? 1 : -1;
}
export function getPointFromBoundsWithRotation(
bounds: IBound,
point: IVec
): IVec {
const { x, y, w, h, rotate } = bounds;
if (!rotate) return point;
const cx = x + w / 2;
const cy = y + h / 2;
const m = new DOMMatrix()
.translateSelf(cx, cy)
.rotateSelf(rotate)
.translateSelf(-cx, -cy);
const p = new DOMPoint(...point).matrixTransform(m);
return [p.x, p.y];
}
export function normalizeDegAngle(angle: number) {
if (angle < 0) angle += 360;
angle %= 360;
return angle;
}
export function toDegree(radian: number) {
return (radian * 180) / Math.PI;
}
// 0 means x axis, 1 means y axis
export function isOverlap(
line1: IVec[],
line2: IVec[],
axis: 0 | 1,
strict = true
) {
const less = strict
? (a: number, b: number) => a < b
: (a: number, b: number) => a <= b;
return !(
less(
Math.max(line1[0][axis], line1[1][axis]),
Math.min(line2[0][axis], line2[1][axis])
) ||
less(
Math.max(line2[0][axis], line2[1][axis]),
Math.min(line1[0][axis], line1[1][axis])
)
);
}
export function getCenterAreaBounds(bounds: IBound, ratio: number) {
const { x, y, w, h, rotate } = bounds;
const cx = x + w / 2;
const cy = y + h / 2;
const nw = w * ratio;
const nh = h * ratio;
return {
x: cx - nw / 2,
y: cy - nh / 2,
w: nw,
h: nh,
rotate,
};
}