mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 10:36:22 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user