mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
refactor(editor): add gfx entry in bs global package (#10612)
This commit is contained in:
@@ -1,175 +0,0 @@
|
||||
import { Bound, getIBoundFromPoints, type IBound } from './model/bound.js';
|
||||
import { type IVec } from './model/vec.js';
|
||||
|
||||
function getExpandedBound(a: IBound, b: IBound): IBound {
|
||||
const minX = Math.min(a.x, b.x);
|
||||
const minY = Math.min(a.y, b.y);
|
||||
const maxX = Math.max(a.x + a.w, b.x + b.w);
|
||||
const maxY = Math.max(a.y + a.h, b.y + b.h);
|
||||
const width = Math.abs(maxX - minX);
|
||||
const height = Math.abs(maxY - minY);
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
w: width,
|
||||
h: height,
|
||||
};
|
||||
}
|
||||
|
||||
export function getPointsFromBoundWithRotation(
|
||||
bounds: IBound,
|
||||
getPoints: (bounds: IBound) => IVec[] = ({ x, y, w, h }: IBound) => [
|
||||
// left-top
|
||||
[x, y],
|
||||
// right-top
|
||||
[x + w, y],
|
||||
// right-bottom
|
||||
[x + w, y + h],
|
||||
// left-bottom
|
||||
[x, y + h],
|
||||
],
|
||||
resPadding: [number, number] = [0, 0]
|
||||
): IVec[] {
|
||||
const { rotate } = bounds;
|
||||
let points = getPoints({
|
||||
x: bounds.x - resPadding[1],
|
||||
y: bounds.y - resPadding[0],
|
||||
w: bounds.w + resPadding[1] * 2,
|
||||
h: bounds.h + resPadding[0] * 2,
|
||||
});
|
||||
|
||||
if (rotate) {
|
||||
const { x, y, w, h } = bounds;
|
||||
const cx = x + w / 2;
|
||||
const cy = y + h / 2;
|
||||
|
||||
const m = new DOMMatrix()
|
||||
.translateSelf(cx, cy)
|
||||
.rotateSelf(rotate)
|
||||
.translateSelf(-cx, -cy);
|
||||
|
||||
points = points.map(point => {
|
||||
const { x, y } = new DOMPoint(...point).matrixTransform(m);
|
||||
return [x, y];
|
||||
});
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
export function getQuadBoundWithRotation(bounds: IBound): DOMRect {
|
||||
const { x, y, w, h, rotate } = bounds;
|
||||
const rect = new DOMRect(x, y, w, h);
|
||||
|
||||
if (!rotate) return rect;
|
||||
|
||||
return new DOMQuad(
|
||||
...getPointsFromBoundWithRotation(bounds).map(
|
||||
point => new DOMPoint(...point)
|
||||
)
|
||||
).getBounds();
|
||||
}
|
||||
|
||||
export function getBoundWithRotation(bound: IBound): IBound {
|
||||
const { x, y, width: w, height: h } = getQuadBoundWithRotation(bound);
|
||||
return { x, y, w, h };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the common bound of the given bounds.
|
||||
* The rotation of the bounds is not considered.
|
||||
* @param bounds
|
||||
* @returns
|
||||
*/
|
||||
export function getCommonBound(bounds: IBound[]): Bound | null {
|
||||
if (!bounds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bounds.length === 1) {
|
||||
const { x, y, w, h } = bounds[0];
|
||||
return new Bound(x, y, w, h);
|
||||
}
|
||||
|
||||
let result = bounds[0];
|
||||
|
||||
for (let i = 1; i < bounds.length; i++) {
|
||||
result = getExpandedBound(result, bounds[i]);
|
||||
}
|
||||
|
||||
return new Bound(result.x, result.y, result.w, result.h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `getCommonBound`, but considers the rotation of the bounds.
|
||||
* @returns
|
||||
*/
|
||||
export function getCommonBoundWithRotation(bounds: IBound[]): Bound {
|
||||
if (bounds.length === 0) {
|
||||
return new Bound(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
return bounds.reduce(
|
||||
(pre, bound) => {
|
||||
return pre.unite(
|
||||
bound instanceof Bound ? bound : Bound.from(getBoundWithRotation(bound))
|
||||
);
|
||||
},
|
||||
Bound.from(getBoundWithRotation(bounds[0]))
|
||||
);
|
||||
}
|
||||
|
||||
export function getBoundFromPoints(points: IVec[]) {
|
||||
return Bound.from(getIBoundFromPoints(points));
|
||||
}
|
||||
|
||||
export function inflateBound(bound: IBound, delta: number) {
|
||||
const half = delta / 2;
|
||||
|
||||
const newBound = new Bound(
|
||||
bound.x - half,
|
||||
bound.y - half,
|
||||
bound.w + delta,
|
||||
bound.h + delta
|
||||
);
|
||||
|
||||
if (newBound.w <= 0 || newBound.h <= 0) {
|
||||
throw new Error('Invalid delta range or bound size.');
|
||||
}
|
||||
|
||||
return newBound;
|
||||
}
|
||||
|
||||
export function transformPointsToNewBound<T extends { x: number; y: number }>(
|
||||
points: T[],
|
||||
oldBound: IBound,
|
||||
oldMargin: number,
|
||||
newBound: IBound,
|
||||
newMargin: number
|
||||
) {
|
||||
const wholeOldMargin = oldMargin * 2;
|
||||
const wholeNewMargin = newMargin * 2;
|
||||
const oldW = Math.max(oldBound.w - wholeOldMargin, 1);
|
||||
const oldH = Math.max(oldBound.h - wholeOldMargin, 1);
|
||||
const newW = Math.max(newBound.w - wholeNewMargin, 1);
|
||||
const newH = Math.max(newBound.h - wholeNewMargin, 1);
|
||||
|
||||
const transformedPoints = points.map(p => {
|
||||
return {
|
||||
...p,
|
||||
x: newW * ((p.x - oldMargin) / oldW) + newMargin,
|
||||
y: newH * ((p.y - oldMargin) / oldH) + newMargin,
|
||||
} as T;
|
||||
});
|
||||
|
||||
return {
|
||||
points: transformedPoints,
|
||||
bound: new Bound(
|
||||
newBound.x,
|
||||
newBound.y,
|
||||
newW + wholeNewMargin,
|
||||
newH + wholeNewMargin
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -1,404 +0,0 @@
|
||||
// control coords are not relative to start or end
|
||||
import { assertExists } from './assert.js';
|
||||
import { CURVETIME_EPSILON, isZero } from './math.js';
|
||||
import { Bound, type IVec, PointLocation, Vec } from './model/index.js';
|
||||
|
||||
export type BezierCurveParameters = [
|
||||
start: IVec,
|
||||
control1: IVec,
|
||||
control2: IVec,
|
||||
end: IVec,
|
||||
];
|
||||
|
||||
function evaluate(
|
||||
v: BezierCurveParameters,
|
||||
t: number,
|
||||
type: number,
|
||||
normalized: boolean
|
||||
): IVec | null {
|
||||
if (t == null || t < 0 || t > 1) return null;
|
||||
const x0 = v[0][0],
|
||||
y0 = v[0][1],
|
||||
x3 = v[3][0],
|
||||
y3 = v[3][1];
|
||||
let x1 = v[1][0],
|
||||
y1 = v[1][1],
|
||||
x2 = v[2][0],
|
||||
y2 = v[2][1];
|
||||
|
||||
if (isZero(x1 - x0) && isZero(y1 - y0)) {
|
||||
x1 = x0;
|
||||
y1 = y0;
|
||||
}
|
||||
if (isZero(x2 - x3) && isZero(y2 - y3)) {
|
||||
x2 = x3;
|
||||
y2 = y3;
|
||||
}
|
||||
// Calculate the polynomial coefficients.
|
||||
const cx = 3 * (x1 - x0),
|
||||
bx = 3 * (x2 - x1) - cx,
|
||||
ax = x3 - x0 - cx - bx,
|
||||
cy = 3 * (y1 - y0),
|
||||
by = 3 * (y2 - y1) - cy,
|
||||
ay = y3 - y0 - cy - by;
|
||||
let x, y;
|
||||
if (type === 0) {
|
||||
// type === 0: getPoint()
|
||||
x = t === 0 ? x0 : t === 1 ? x3 : ((ax * t + bx) * t + cx) * t + x0;
|
||||
y = t === 0 ? y0 : t === 1 ? y3 : ((ay * t + by) * t + cy) * t + y0;
|
||||
} else {
|
||||
// type === 1: getTangent()
|
||||
// type === 2: getNormal()
|
||||
// type === 3: getCurvature()
|
||||
const tMin = CURVETIME_EPSILON,
|
||||
tMax = 1 - tMin;
|
||||
if (t < tMin) {
|
||||
x = cx;
|
||||
y = cy;
|
||||
} else if (t > tMax) {
|
||||
x = 3 * (x3 - x2);
|
||||
y = 3 * (y3 - y2);
|
||||
} else {
|
||||
x = (3 * ax * t + 2 * bx) * t + cx;
|
||||
y = (3 * ay * t + 2 * by) * t + cy;
|
||||
}
|
||||
if (normalized) {
|
||||
if (x === 0 && y === 0 && (t < tMin || t > tMax)) {
|
||||
x = x2 - x1;
|
||||
y = y2 - y1;
|
||||
}
|
||||
const len = Math.sqrt(x * x + y * y);
|
||||
if (len) {
|
||||
x /= len;
|
||||
y /= len;
|
||||
}
|
||||
}
|
||||
if (type === 3) {
|
||||
const x2 = 6 * ax * t + 2 * bx,
|
||||
y2 = 6 * ay * t + 2 * by,
|
||||
d = Math.pow(x * x + y * y, 3 / 2);
|
||||
x = d !== 0 ? (x * y2 - y * x2) / d : 0;
|
||||
y = 0;
|
||||
}
|
||||
}
|
||||
return type === 2 ? [y, -x] : [x, y];
|
||||
}
|
||||
|
||||
export function getBezierPoint(values: BezierCurveParameters, t: number) {
|
||||
return evaluate(values, t, 0, false);
|
||||
}
|
||||
|
||||
export function getBezierTangent(values: BezierCurveParameters, t: number) {
|
||||
return evaluate(values, t, 1, true);
|
||||
}
|
||||
|
||||
export function getBezierNormal(values: BezierCurveParameters, t: number) {
|
||||
return evaluate(values, t, 2, true);
|
||||
}
|
||||
|
||||
export function getBezierCurvature(values: BezierCurveParameters, t: number) {
|
||||
return evaluate(values, t, 3, false)?.[0];
|
||||
}
|
||||
|
||||
export function getBezierNearestTime(
|
||||
values: BezierCurveParameters,
|
||||
point: IVec
|
||||
) {
|
||||
const count = 100;
|
||||
let minDist = Infinity,
|
||||
minT = 0;
|
||||
|
||||
function refine(t: number) {
|
||||
if (t >= 0 && t <= 1) {
|
||||
const tmpPoint = getBezierPoint(values, t);
|
||||
assertExists(tmpPoint);
|
||||
const dist = Vec.dist2(point, tmpPoint);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
minT = t;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i <= count; i++) refine(i / count);
|
||||
|
||||
let step = 1 / (count * 2);
|
||||
while (step > CURVETIME_EPSILON) {
|
||||
if (!refine(minT - step) && !refine(minT + step)) step /= 2;
|
||||
}
|
||||
return minT;
|
||||
}
|
||||
|
||||
export function getBezierNearestPoint(
|
||||
values: BezierCurveParameters,
|
||||
point: IVec
|
||||
) {
|
||||
const t = getBezierNearestTime(values, point);
|
||||
const pointOnCurve = getBezierPoint(values, t);
|
||||
assertExists(pointOnCurve);
|
||||
return pointOnCurve;
|
||||
}
|
||||
|
||||
export function getBezierParameters(
|
||||
points: PointLocation[]
|
||||
): BezierCurveParameters {
|
||||
// Fallback for degenerate Bezier curve (all points are at the same position)
|
||||
if (points.length === 1) {
|
||||
const point = points[0];
|
||||
return [point, point, point, point];
|
||||
}
|
||||
|
||||
return [points[0], points[0].absOut, points[1].absIn, points[1]];
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/2587751/an-algorithm-to-find-bounding-box-of-closed-bezier-curves
|
||||
export function getBezierCurveBoundingBox(values: BezierCurveParameters) {
|
||||
const [start, controlPoint1, controlPoint2, end] = values;
|
||||
|
||||
const [x0, y0] = start;
|
||||
const [x1, y1] = controlPoint1;
|
||||
const [x2, y2] = controlPoint2;
|
||||
const [x3, y3] = end;
|
||||
|
||||
const points = []; // local extremes
|
||||
const tvalues = []; // t values of local extremes
|
||||
const bounds: [number[], number[]] = [[], []];
|
||||
|
||||
let a;
|
||||
let b;
|
||||
let c;
|
||||
let t;
|
||||
let t1;
|
||||
let t2;
|
||||
let b2ac;
|
||||
let sqrtb2ac;
|
||||
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
if (i === 0) {
|
||||
b = 6 * x0 - 12 * x1 + 6 * x2;
|
||||
a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
|
||||
c = 3 * x1 - 3 * x0;
|
||||
} else {
|
||||
b = 6 * y0 - 12 * y1 + 6 * y2;
|
||||
a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
|
||||
c = 3 * y1 - 3 * y0;
|
||||
}
|
||||
|
||||
if (Math.abs(a) < 1e-12) {
|
||||
if (Math.abs(b) < 1e-12) {
|
||||
continue;
|
||||
}
|
||||
|
||||
t = -c / b;
|
||||
if (t > 0 && t < 1) tvalues.push(t);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
b2ac = b * b - 4 * c * a;
|
||||
sqrtb2ac = Math.sqrt(b2ac);
|
||||
|
||||
if (b2ac < 0) continue;
|
||||
|
||||
t1 = (-b + sqrtb2ac) / (2 * a);
|
||||
if (t1 > 0 && t1 < 1) tvalues.push(t1);
|
||||
|
||||
t2 = (-b - sqrtb2ac) / (2 * a);
|
||||
if (t2 > 0 && t2 < 1) tvalues.push(t2);
|
||||
}
|
||||
|
||||
let x;
|
||||
let y;
|
||||
let mt;
|
||||
let j = tvalues.length;
|
||||
const jlen = j;
|
||||
|
||||
while (j) {
|
||||
j -= 1;
|
||||
t = tvalues[j];
|
||||
mt = 1 - t;
|
||||
|
||||
x =
|
||||
mt * mt * mt * x0 +
|
||||
3 * mt * mt * t * x1 +
|
||||
3 * mt * t * t * x2 +
|
||||
t * t * t * x3;
|
||||
bounds[0][j] = x;
|
||||
|
||||
y =
|
||||
mt * mt * mt * y0 +
|
||||
3 * mt * mt * t * y1 +
|
||||
3 * mt * t * t * y2 +
|
||||
t * t * t * y3;
|
||||
|
||||
bounds[1][j] = y;
|
||||
points[j] = { X: x, Y: y };
|
||||
}
|
||||
|
||||
tvalues[jlen] = 0;
|
||||
tvalues[jlen + 1] = 1;
|
||||
|
||||
points[jlen] = { X: x0, Y: y0 };
|
||||
points[jlen + 1] = { X: x3, Y: y3 };
|
||||
|
||||
bounds[0][jlen] = x0;
|
||||
bounds[1][jlen] = y0;
|
||||
|
||||
bounds[0][jlen + 1] = x3;
|
||||
bounds[1][jlen + 1] = y3;
|
||||
|
||||
tvalues.length = jlen + 2;
|
||||
bounds[0].length = jlen + 2;
|
||||
bounds[1].length = jlen + 2;
|
||||
points.length = jlen + 2;
|
||||
|
||||
const left = Math.min.apply(null, bounds[0]);
|
||||
const top = Math.min.apply(null, bounds[1]);
|
||||
const right = Math.max.apply(null, bounds[0]);
|
||||
const bottom = Math.max.apply(null, bounds[1]);
|
||||
|
||||
return new Bound(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
// https://pomax.github.io/bezierjs/#intersect-line
|
||||
// MIT Licence
|
||||
|
||||
// cube root function yielding real roots
|
||||
function crt(v: number) {
|
||||
return v < 0 ? -Math.pow(-v, 1 / 3) : Math.pow(v, 1 / 3);
|
||||
}
|
||||
|
||||
function align(points: BezierCurveParameters, [start, end]: IVec[]) {
|
||||
const tx = start[0],
|
||||
ty = start[1],
|
||||
a = -Math.atan2(end[1] - ty, end[0] - tx),
|
||||
d = function ([x, y]: IVec) {
|
||||
return [
|
||||
(x - tx) * Math.cos(a) - (y - ty) * Math.sin(a),
|
||||
(x - tx) * Math.sin(a) + (y - ty) * Math.cos(a),
|
||||
];
|
||||
};
|
||||
return points.map(d);
|
||||
}
|
||||
|
||||
function between(v: number, min: number, max: number) {
|
||||
return (
|
||||
(min <= v && v <= max) || approximately(v, min) || approximately(v, max)
|
||||
);
|
||||
}
|
||||
|
||||
function approximately(
|
||||
a: number,
|
||||
b: number,
|
||||
precision?: number,
|
||||
epsilon = 0.000001
|
||||
) {
|
||||
return Math.abs(a - b) <= (precision || epsilon);
|
||||
}
|
||||
|
||||
function roots(points: BezierCurveParameters, line: IVec[]) {
|
||||
const order = points.length - 1;
|
||||
const aligned = align(points, line);
|
||||
const reduce = function (t: number) {
|
||||
return 0 <= t && t <= 1;
|
||||
};
|
||||
|
||||
if (order === 2) {
|
||||
const a = aligned[0][1],
|
||||
b = aligned[1][1],
|
||||
c = aligned[2][1],
|
||||
d = a - 2 * b + c;
|
||||
if (d !== 0) {
|
||||
const m1 = -Math.sqrt(b * b - a * c),
|
||||
m2 = -a + b,
|
||||
v1 = -(m1 + m2) / d,
|
||||
v2 = -(-m1 + m2) / d;
|
||||
return [v1, v2].filter(reduce);
|
||||
} else if (b !== c && d === 0) {
|
||||
return [(2 * b - c) / (2 * b - 2 * c)].filter(reduce);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm
|
||||
const pa = aligned[0][1],
|
||||
pb = aligned[1][1],
|
||||
pc = aligned[2][1],
|
||||
pd = aligned[3][1];
|
||||
|
||||
const d = -pa + 3 * pb - 3 * pc + pd;
|
||||
let a = 3 * pa - 6 * pb + 3 * pc,
|
||||
b = -3 * pa + 3 * pb,
|
||||
c = pa;
|
||||
|
||||
if (approximately(d, 0)) {
|
||||
// this is not a cubic curve.
|
||||
if (approximately(a, 0)) {
|
||||
// in fact, this is not a quadratic curve either.
|
||||
if (approximately(b, 0)) {
|
||||
// in fact in fact, there are no solutions.
|
||||
return [];
|
||||
}
|
||||
// linear solution:
|
||||
return [-c / b].filter(reduce);
|
||||
}
|
||||
// quadratic solution:
|
||||
const q = Math.sqrt(b * b - 4 * a * c),
|
||||
a2 = 2 * a;
|
||||
return [(q - b) / a2, (-b - q) / a2].filter(reduce);
|
||||
}
|
||||
|
||||
// at this point, we know we need a cubic solution:
|
||||
|
||||
a /= d;
|
||||
b /= d;
|
||||
c /= d;
|
||||
|
||||
const p = (3 * b - a * a) / 3,
|
||||
p3 = p / 3,
|
||||
q = (2 * a * a * a - 9 * a * b + 27 * c) / 27,
|
||||
q2 = q / 2,
|
||||
discriminant = q2 * q2 + p3 * p3 * p3;
|
||||
|
||||
let u1, v1, x1, x2, x3;
|
||||
if (discriminant < 0) {
|
||||
const mp3 = -p / 3,
|
||||
mp33 = mp3 * mp3 * mp3,
|
||||
r = Math.sqrt(mp33),
|
||||
t = -q / (2 * r),
|
||||
cosphi = t < -1 ? -1 : t > 1 ? 1 : t,
|
||||
phi = Math.acos(cosphi),
|
||||
crtr = crt(r),
|
||||
t1 = 2 * crtr;
|
||||
x1 = t1 * Math.cos(phi / 3) - a / 3;
|
||||
x2 = t1 * Math.cos((phi + Math.PI * 2) / 3) - a / 3;
|
||||
x3 = t1 * Math.cos((phi + 2 * Math.PI * 2) / 3) - a / 3;
|
||||
return [x1, x2, x3].filter(reduce);
|
||||
} else if (discriminant === 0) {
|
||||
u1 = q2 < 0 ? crt(-q2) : -crt(q2);
|
||||
x1 = 2 * u1 - a / 3;
|
||||
x2 = -u1 - a / 3;
|
||||
return [x1, x2].filter(reduce);
|
||||
} else {
|
||||
const sd = Math.sqrt(discriminant);
|
||||
u1 = crt(-q2 + sd);
|
||||
v1 = crt(q2 + sd);
|
||||
return [u1 - v1 - a / 3].filter(reduce);
|
||||
}
|
||||
}
|
||||
|
||||
export function curveIntersects(path: PointLocation[], line: [IVec, IVec]) {
|
||||
const { minX, maxX, minY, maxY } = Bound.fromPoints(line);
|
||||
const points = getBezierParameters(path);
|
||||
const intersectedPoints = roots(points, line)
|
||||
.map(t => getBezierPoint(points, t))
|
||||
.filter(point =>
|
||||
point
|
||||
? between(point[0], minX, maxX) && between(point[1], minY, maxY)
|
||||
: false
|
||||
)
|
||||
.map(point => new PointLocation(point!));
|
||||
return intersectedPoints.length > 0 ? intersectedPoints : null;
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
export * from './assert.js';
|
||||
export * from './bound.js';
|
||||
export * from './crypto.js';
|
||||
export * from './curve.js';
|
||||
export * from './disposable.js';
|
||||
export * from './function.js';
|
||||
export * from './iterable.js';
|
||||
export * from './logger.js';
|
||||
export * from './math.js';
|
||||
export * from './model/index.js';
|
||||
export * from './perfect-freehand/index.js';
|
||||
export * from './polyline.js';
|
||||
export * from './signal-watcher.js';
|
||||
export * from './slot.js';
|
||||
export * from './types.js';
|
||||
export * from './with-disposable.js';
|
||||
export type { SerializedXYWH, XYWH } from './xywh.js';
|
||||
export { deserializeXYWH, serializeXYWH } from './xywh.js';
|
||||
|
||||
@@ -1,538 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
import { EPSILON, lineIntersects, polygonPointDistance } from '../math.js';
|
||||
import type { SerializedXYWH, XYWH } from '../xywh.js';
|
||||
import { deserializeXYWH, serializeXYWH } from '../xywh.js';
|
||||
import { type IVec, Vec } from './vec.js';
|
||||
|
||||
export function getIBoundFromPoints(
|
||||
points: IVec[],
|
||||
rotation = 0
|
||||
): IBound & {
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
minX: number;
|
||||
minY: number;
|
||||
} {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
if (points.length < 1) {
|
||||
minX = 0;
|
||||
minY = 0;
|
||||
maxX = 1;
|
||||
maxY = 1;
|
||||
} else {
|
||||
for (const [x, y] of points) {
|
||||
minX = Math.min(x, minX);
|
||||
minY = Math.min(y, minY);
|
||||
maxX = Math.max(x, maxX);
|
||||
maxY = Math.max(y, maxY);
|
||||
}
|
||||
}
|
||||
|
||||
if (rotation !== 0) {
|
||||
return getIBoundFromPoints(
|
||||
points.map(pt =>
|
||||
Vec.rotWith(pt, [(minX + maxX) / 2, (minY + maxY) / 2], rotation)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
x: minX,
|
||||
y: minY,
|
||||
w: maxX - minX,
|
||||
h: maxY - minY,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the x, y, width, and height of a block that can be easily accessed.
|
||||
*/
|
||||
export interface IBound {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
rotate?: number;
|
||||
}
|
||||
|
||||
export class Bound implements IBound {
|
||||
h: number;
|
||||
|
||||
w: number;
|
||||
|
||||
x: number;
|
||||
|
||||
y: number;
|
||||
|
||||
get bl(): IVec {
|
||||
return [this.x, this.y + this.h];
|
||||
}
|
||||
|
||||
get br(): IVec {
|
||||
return [this.x + this.w, this.y + this.h];
|
||||
}
|
||||
|
||||
get center(): IVec {
|
||||
return [this.x + this.w / 2, this.y + this.h / 2];
|
||||
}
|
||||
|
||||
set center([cx, cy]: IVec) {
|
||||
const [px, py] = this.center;
|
||||
this.x += cx - px;
|
||||
this.y += cy - py;
|
||||
}
|
||||
|
||||
get horizontalLine(): IVec[] {
|
||||
return [
|
||||
[this.x, this.y + this.h / 2],
|
||||
[this.x + this.w, this.y + this.h / 2],
|
||||
];
|
||||
}
|
||||
|
||||
get leftLine(): IVec[] {
|
||||
return [
|
||||
[this.x, this.y],
|
||||
[this.x, this.y + this.h],
|
||||
];
|
||||
}
|
||||
|
||||
get lowerLine(): IVec[] {
|
||||
return [
|
||||
[this.x, this.y + this.h],
|
||||
[this.x + this.w, this.y + this.h],
|
||||
];
|
||||
}
|
||||
|
||||
get maxX() {
|
||||
return this.x + this.w;
|
||||
}
|
||||
|
||||
get maxY() {
|
||||
return this.y + this.h;
|
||||
}
|
||||
|
||||
get midPoints(): IVec[] {
|
||||
return [
|
||||
[this.x + this.w / 2, this.y],
|
||||
[this.x + this.w, this.y + this.h / 2],
|
||||
[this.x + this.w / 2, this.y + this.h],
|
||||
[this.x, this.y + this.h / 2],
|
||||
];
|
||||
}
|
||||
|
||||
get minX() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
get minY() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
get points(): IVec[] {
|
||||
return [
|
||||
[this.x, this.y],
|
||||
[this.x + this.w, this.y],
|
||||
[this.x + this.w, this.y + this.h],
|
||||
[this.x, this.y + this.h],
|
||||
];
|
||||
}
|
||||
|
||||
get rightLine(): IVec[] {
|
||||
return [
|
||||
[this.x + this.w, this.y],
|
||||
[this.x + this.w, this.y + this.h],
|
||||
];
|
||||
}
|
||||
|
||||
get tl(): IVec {
|
||||
return [this.x, this.y];
|
||||
}
|
||||
|
||||
get tr(): IVec {
|
||||
return [this.x + this.w, this.y];
|
||||
}
|
||||
|
||||
get upperLine(): IVec[] {
|
||||
return [
|
||||
[this.x, this.y],
|
||||
[this.x + this.w, this.y],
|
||||
];
|
||||
}
|
||||
|
||||
get verticalLine(): IVec[] {
|
||||
return [
|
||||
[this.x + this.w / 2, this.y],
|
||||
[this.x + this.w / 2, this.y + this.h],
|
||||
];
|
||||
}
|
||||
|
||||
constructor(x = 0, y = 0, w = 0, h = 0) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.w = w;
|
||||
this.h = h;
|
||||
}
|
||||
|
||||
static deserialize(s: string) {
|
||||
const [x, y, w, h] = deserializeXYWH(s);
|
||||
return new Bound(x, y, w, h);
|
||||
}
|
||||
|
||||
static from(arg1: IBound) {
|
||||
return new Bound(arg1.x, arg1.y, arg1.w, arg1.h);
|
||||
}
|
||||
|
||||
static fromCenter(center: IVec, width: number, height: number) {
|
||||
const [x, y] = center;
|
||||
return new Bound(x - width / 2, y - height / 2, width, height);
|
||||
}
|
||||
|
||||
static fromDOMRect({ left, top, width, height }: DOMRect) {
|
||||
return new Bound(left, top, width, height);
|
||||
}
|
||||
|
||||
static fromPoints(points: IVec[]) {
|
||||
return Bound.from(getIBoundFromPoints(points));
|
||||
}
|
||||
|
||||
static fromXYWH(xywh: XYWH) {
|
||||
return new Bound(xywh[0], xywh[1], xywh[2], xywh[3]);
|
||||
}
|
||||
|
||||
static serialize(bound: IBound) {
|
||||
return serializeXYWH(bound.x, bound.y, bound.w, bound.h);
|
||||
}
|
||||
|
||||
clone(): Bound {
|
||||
return new Bound(this.x, this.y, this.w, this.h);
|
||||
}
|
||||
|
||||
contains(bound: Bound) {
|
||||
return (
|
||||
bound.x >= this.x &&
|
||||
bound.y >= this.y &&
|
||||
bound.maxX <= this.maxX &&
|
||||
bound.maxY <= this.maxY
|
||||
);
|
||||
}
|
||||
|
||||
containsPoint([x, y]: IVec): boolean {
|
||||
const { minX, minY, maxX, maxY } = this;
|
||||
return minX <= x && x <= maxX && minY <= y && y <= maxY;
|
||||
}
|
||||
|
||||
expand(margin: [number, number]): Bound;
|
||||
expand(left: number, top?: number, right?: number, bottom?: number): Bound;
|
||||
expand(
|
||||
left: number | [number, number],
|
||||
top?: number,
|
||||
right?: number,
|
||||
bottom?: number
|
||||
) {
|
||||
if (Array.isArray(left)) {
|
||||
const [x, y] = left;
|
||||
return new Bound(this.x - x, this.y - y, this.w + x * 2, this.h + y * 2);
|
||||
}
|
||||
|
||||
top ??= left;
|
||||
right ??= left;
|
||||
bottom ??= top;
|
||||
|
||||
return new Bound(
|
||||
this.x - left,
|
||||
this.y - top,
|
||||
this.w + left + right,
|
||||
this.h + top + bottom
|
||||
);
|
||||
}
|
||||
|
||||
getRelativePoint([x, y]: IVec): IVec {
|
||||
return [this.x + x * this.w, this.y + y * this.h];
|
||||
}
|
||||
|
||||
getVerticesAndMidpoints() {
|
||||
return [...this.points, ...this.midPoints];
|
||||
}
|
||||
|
||||
horizontalDistance(bound: Bound) {
|
||||
return Math.min(
|
||||
Math.abs(this.minX - bound.maxX),
|
||||
Math.abs(this.maxX - bound.minX)
|
||||
);
|
||||
}
|
||||
|
||||
include(point: IVec) {
|
||||
const x1 = Math.min(this.x, point[0]),
|
||||
y1 = Math.min(this.y, point[1]),
|
||||
x2 = Math.max(this.maxX, point[0]),
|
||||
y2 = Math.max(this.maxY, point[1]);
|
||||
return new Bound(x1, y1, x2 - x1, y2 - y1);
|
||||
}
|
||||
|
||||
intersectLine(sp: IVec, ep: IVec, infinite = false) {
|
||||
const rst: IVec[] = [];
|
||||
(
|
||||
[
|
||||
[this.tl, this.tr],
|
||||
[this.tl, this.bl],
|
||||
[this.tr, this.br],
|
||||
[this.bl, this.br],
|
||||
] as IVec[][]
|
||||
).forEach(([p1, p2]) => {
|
||||
const p = lineIntersects(sp, ep, p1, p2, infinite);
|
||||
if (p) rst.push(p);
|
||||
});
|
||||
return rst.length === 0 ? null : rst;
|
||||
}
|
||||
|
||||
isHorizontalCross(bound: Bound) {
|
||||
return !(this.maxY < bound.minY || this.minY > bound.maxY);
|
||||
}
|
||||
|
||||
isIntersectWithBound(bound: Bound, epsilon = EPSILON) {
|
||||
return (
|
||||
bound.maxX > this.minX - epsilon &&
|
||||
bound.maxY > this.minY - epsilon &&
|
||||
bound.minX < this.maxX + epsilon &&
|
||||
bound.minY < this.maxY + epsilon &&
|
||||
!this.contains(bound) &&
|
||||
!bound.contains(this)
|
||||
);
|
||||
}
|
||||
|
||||
isOverlapWithBound(bound: Bound, epsilon = EPSILON) {
|
||||
return (
|
||||
bound.maxX > this.minX - epsilon &&
|
||||
bound.maxY > this.minY - epsilon &&
|
||||
bound.minX < this.maxX + epsilon &&
|
||||
bound.minY < this.maxY + epsilon
|
||||
);
|
||||
}
|
||||
|
||||
isPointInBound([x, y]: IVec, tolerance = 0.01) {
|
||||
return (
|
||||
x > this.minX + tolerance &&
|
||||
x < this.maxX - tolerance &&
|
||||
y > this.minY + tolerance &&
|
||||
y < this.maxY - tolerance
|
||||
);
|
||||
}
|
||||
|
||||
isPointNearBound([x, y]: IVec, tolerance = 0.01) {
|
||||
return polygonPointDistance(this.points, [x, y]) < tolerance;
|
||||
}
|
||||
|
||||
isVerticalCross(bound: Bound) {
|
||||
return !(this.maxX < bound.minX || this.minX > bound.maxX);
|
||||
}
|
||||
|
||||
moveDelta(dx: number, dy: number) {
|
||||
return new Bound(this.x + dx, this.y + dy, this.w, this.h);
|
||||
}
|
||||
|
||||
serialize(): SerializedXYWH {
|
||||
return serializeXYWH(this.x, this.y, this.w, this.h);
|
||||
}
|
||||
|
||||
toRelative([x, y]: IVec): IVec {
|
||||
return [(x - this.x) / this.w, (y - this.y) / this.h];
|
||||
}
|
||||
|
||||
toXYWH(): XYWH {
|
||||
return [this.x, this.y, this.w, this.h];
|
||||
}
|
||||
|
||||
unite(bound: Bound) {
|
||||
const x1 = Math.min(this.x, bound.x),
|
||||
y1 = Math.min(this.y, bound.y),
|
||||
x2 = Math.max(this.maxX, bound.maxX),
|
||||
y2 = Math.max(this.maxY, bound.maxY);
|
||||
return new Bound(x1, y1, x2 - x1, y2 - y1);
|
||||
}
|
||||
|
||||
verticalDistance(bound: Bound) {
|
||||
return Math.min(
|
||||
Math.abs(this.minY - bound.maxY),
|
||||
Math.abs(this.maxY - bound.minY)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './bound.js';
|
||||
export * from './point.js';
|
||||
export * from './point-location.js';
|
||||
export * from './vec.js';
|
||||
@@ -1,94 +0,0 @@
|
||||
import { type IVec, Vec } from './vec.js';
|
||||
|
||||
/**
|
||||
* PointLocation is an implementation of IVec with in/out vectors and tangent.
|
||||
* This is useful when dealing with path.
|
||||
*/
|
||||
export class PointLocation extends Array<number> implements IVec {
|
||||
_in: IVec = [0, 0];
|
||||
|
||||
_out: IVec = [0, 0];
|
||||
|
||||
// the tangent belongs to the point on the element outline
|
||||
_tangent: IVec = [0, 0];
|
||||
|
||||
[0]: number;
|
||||
|
||||
[1]: number;
|
||||
|
||||
get absIn() {
|
||||
return Vec.add(this, this._in);
|
||||
}
|
||||
|
||||
get absOut() {
|
||||
return Vec.add(this, this._out);
|
||||
}
|
||||
|
||||
get in() {
|
||||
return this._in;
|
||||
}
|
||||
|
||||
set in(value: IVec) {
|
||||
this._in = value;
|
||||
}
|
||||
|
||||
override get length() {
|
||||
return super.length as 2;
|
||||
}
|
||||
|
||||
get out() {
|
||||
return this._out;
|
||||
}
|
||||
|
||||
set out(value: IVec) {
|
||||
this._out = value;
|
||||
}
|
||||
|
||||
get tangent() {
|
||||
return this._tangent;
|
||||
}
|
||||
|
||||
set tangent(value: IVec) {
|
||||
this._tangent = value;
|
||||
}
|
||||
|
||||
constructor(
|
||||
point: IVec = [0, 0],
|
||||
tangent: IVec = [0, 0],
|
||||
inVec: IVec = [0, 0],
|
||||
outVec: IVec = [0, 0]
|
||||
) {
|
||||
super(2);
|
||||
this[0] = point[0];
|
||||
this[1] = point[1];
|
||||
this._tangent = tangent;
|
||||
this._in = inVec;
|
||||
this._out = outVec;
|
||||
}
|
||||
|
||||
static fromVec(vec: IVec) {
|
||||
const point = new PointLocation();
|
||||
point[0] = vec[0];
|
||||
point[1] = vec[1];
|
||||
return point;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new PointLocation(
|
||||
this as unknown as IVec,
|
||||
this._tangent,
|
||||
this._in,
|
||||
this._out
|
||||
);
|
||||
}
|
||||
|
||||
setVec(vec: IVec) {
|
||||
this[0] = vec[0];
|
||||
this[1] = vec[1];
|
||||
return this;
|
||||
}
|
||||
|
||||
toVec(): IVec {
|
||||
return [this[0], this[1]];
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import { clamp } from '../math.js';
|
||||
|
||||
export interface IPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export class Point {
|
||||
x: number;
|
||||
|
||||
y: number;
|
||||
|
||||
constructor(x = 0, y = 0) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict a value to a certain interval.
|
||||
*/
|
||||
static clamp(p: Point, min: Point, max: Point) {
|
||||
return new Point(clamp(p.x, min.x, max.x), clamp(p.y, min.y, max.y));
|
||||
}
|
||||
|
||||
static from(point: IPoint | number[] | number, y?: number) {
|
||||
if (Array.isArray(point)) {
|
||||
return new Point(point[0], point[1]);
|
||||
}
|
||||
if (typeof point === 'number') {
|
||||
return new Point(point, y ?? point);
|
||||
}
|
||||
return new Point(point.x, point.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares and returns the maximum of two points.
|
||||
*/
|
||||
static max(a: Point, b: Point) {
|
||||
return new Point(Math.max(a.x, b.x), Math.max(a.y, b.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares and returns the minimum of two points.
|
||||
*/
|
||||
static min(a: Point, b: Point) {
|
||||
return new Point(Math.min(a.x, b.x), Math.min(a.y, b.y));
|
||||
}
|
||||
|
||||
add(point: IPoint): Point {
|
||||
return new Point(this.x + point.x, this.y + point.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the point.
|
||||
*/
|
||||
clone() {
|
||||
return new Point(this.x, this.y);
|
||||
}
|
||||
|
||||
cross(point: IPoint): number {
|
||||
return this.x * point.y - this.y * point.x;
|
||||
}
|
||||
|
||||
equals({ x, y }: Point) {
|
||||
return this.x === x && this.y === y;
|
||||
}
|
||||
|
||||
lerp(point: IPoint, t: number): Point {
|
||||
return new Point(
|
||||
this.x + (point.x - this.x) * t,
|
||||
this.y + (point.y - this.y) * t
|
||||
);
|
||||
}
|
||||
|
||||
scale(factor: number): Point {
|
||||
return new Point(this.x * factor, this.y * factor);
|
||||
}
|
||||
|
||||
set(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
subtract(point: IPoint): Point {
|
||||
return new Point(this.x - point.x, this.y - point.y);
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return [this.x, this.y];
|
||||
}
|
||||
}
|
||||
|
||||
export class Rect {
|
||||
// `[right, bottom]`
|
||||
max: Point;
|
||||
|
||||
// `[left, top]`
|
||||
min: Point;
|
||||
|
||||
get bottom() {
|
||||
return this.max.y;
|
||||
}
|
||||
|
||||
set bottom(y: number) {
|
||||
this.max.y = y;
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.max.y - this.min.y;
|
||||
}
|
||||
|
||||
set height(h: number) {
|
||||
this.max.y = this.min.y + h;
|
||||
}
|
||||
|
||||
get left() {
|
||||
return this.min.x;
|
||||
}
|
||||
|
||||
set left(x: number) {
|
||||
this.min.x = x;
|
||||
}
|
||||
|
||||
get right() {
|
||||
return this.max.x;
|
||||
}
|
||||
|
||||
set right(x: number) {
|
||||
this.max.x = x;
|
||||
}
|
||||
|
||||
get top() {
|
||||
return this.min.y;
|
||||
}
|
||||
|
||||
set top(y: number) {
|
||||
this.min.y = y;
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this.max.x - this.min.x;
|
||||
}
|
||||
|
||||
set width(w: number) {
|
||||
this.max.x = this.min.x + w;
|
||||
}
|
||||
|
||||
constructor(left: number, top: number, right: number, bottom: number) {
|
||||
const [minX, maxX] = left <= right ? [left, right] : [right, left];
|
||||
const [minY, maxY] = top <= bottom ? [top, bottom] : [bottom, top];
|
||||
this.min = new Point(minX, minY);
|
||||
this.max = new Point(maxX, maxY);
|
||||
}
|
||||
|
||||
static fromDOM(dom: Element) {
|
||||
return Rect.fromDOMRect(dom.getBoundingClientRect());
|
||||
}
|
||||
|
||||
static fromDOMRect({ left, top, right, bottom }: DOMRect) {
|
||||
return Rect.fromLTRB(left, top, right, bottom);
|
||||
}
|
||||
|
||||
static fromLTRB(left: number, top: number, right: number, bottom: number) {
|
||||
return new Rect(left, top, right, bottom);
|
||||
}
|
||||
|
||||
static fromLWTH(left: number, width: number, top: number, height: number) {
|
||||
return new Rect(left, top, left + width, top + height);
|
||||
}
|
||||
|
||||
static fromPoint(point: Point) {
|
||||
return Rect.fromPoints(point.clone(), point);
|
||||
}
|
||||
|
||||
static fromPoints(start: Point, end: Point) {
|
||||
const width = Math.abs(end.x - start.x);
|
||||
const height = Math.abs(end.y - start.y);
|
||||
const left = Math.min(end.x, start.x);
|
||||
const top = Math.min(end.y, start.y);
|
||||
return Rect.fromLWTH(left, width, top, height);
|
||||
}
|
||||
|
||||
static fromXY(x: number, y: number) {
|
||||
return Rect.fromPoint(new Point(x, y));
|
||||
}
|
||||
|
||||
center() {
|
||||
return new Point(
|
||||
(this.left + this.right) / 2,
|
||||
(this.top + this.bottom) / 2
|
||||
);
|
||||
}
|
||||
|
||||
clamp(p: Point) {
|
||||
return Point.clamp(p, this.min, this.max);
|
||||
}
|
||||
|
||||
clone() {
|
||||
const { left, top, right, bottom } = this;
|
||||
return new Rect(left, top, right, bottom);
|
||||
}
|
||||
|
||||
contains({ min, max }: Rect) {
|
||||
return this.isPointIn(min) && this.isPointIn(max);
|
||||
}
|
||||
|
||||
equals({ min, max }: Rect) {
|
||||
return this.min.equals(min) && this.max.equals(max);
|
||||
}
|
||||
|
||||
extend_with(point: Point) {
|
||||
this.min = Point.min(this.min, point);
|
||||
this.max = Point.max(this.max, point);
|
||||
}
|
||||
|
||||
extend_with_x(x: number) {
|
||||
this.min.x = Math.min(this.min.x, x);
|
||||
this.max.x = Math.max(this.max.x, x);
|
||||
}
|
||||
|
||||
extend_with_y(y: number) {
|
||||
this.min.y = Math.min(this.min.y, y);
|
||||
this.max.y = Math.max(this.max.y, y);
|
||||
}
|
||||
|
||||
intersect(other: Rect) {
|
||||
return Rect.fromPoints(
|
||||
Point.max(this.min, other.min),
|
||||
Point.min(this.max, other.max)
|
||||
);
|
||||
}
|
||||
|
||||
intersects({ left, top, right, bottom }: Rect) {
|
||||
return (
|
||||
this.left <= right &&
|
||||
left <= this.right &&
|
||||
this.top <= bottom &&
|
||||
top <= this.bottom
|
||||
);
|
||||
}
|
||||
|
||||
isPointDown({ x, y }: Point) {
|
||||
return this.bottom < y && this.left <= x && this.right >= x;
|
||||
}
|
||||
|
||||
isPointIn({ x, y }: Point) {
|
||||
return (
|
||||
this.left <= x && x <= this.right && this.top <= y && y <= this.bottom
|
||||
);
|
||||
}
|
||||
|
||||
isPointLeft({ x, y }: Point) {
|
||||
return x < this.left && this.top <= y && this.bottom >= y;
|
||||
}
|
||||
|
||||
isPointRight({ x, y }: Point) {
|
||||
return x > this.right && this.top <= y && this.bottom >= y;
|
||||
}
|
||||
|
||||
isPointUp({ x, y }: Point) {
|
||||
return y < this.top && this.left <= x && this.right >= x;
|
||||
}
|
||||
|
||||
toDOMRect() {
|
||||
const { left, top, width, height } = this;
|
||||
return new DOMRect(left, top, width, height);
|
||||
}
|
||||
}
|
||||
@@ -1,599 +0,0 @@
|
||||
// Inlined from https://raw.githubusercontent.com/tldraw/tldraw/24cad6959f59f93e20e556d018c391fd89d4ecca/packages/vec/src/index.ts
|
||||
// Credits to tldraw
|
||||
|
||||
export type IVec = [number, number];
|
||||
|
||||
export type IVec3 = [number, number, number];
|
||||
|
||||
export class Vec {
|
||||
/**
|
||||
* Absolute value of a vector.
|
||||
* @param A
|
||||
* @returns
|
||||
*/
|
||||
static abs = (A: number[]): number[] => {
|
||||
return [Math.abs(A[0]), Math.abs(A[1])];
|
||||
};
|
||||
|
||||
/**
|
||||
* Add vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static add = (A: number[], B: number[]): IVec => {
|
||||
return [A[0] + B[0], A[1] + B[1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* Add scalar to vector.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static addScalar = (A: number[], n: number): IVec => {
|
||||
return [A[0] + n, A[1] + n];
|
||||
};
|
||||
|
||||
/**
|
||||
* Angle between vector A and vector B in radians
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static ang = (A: number[], B: number[]): number => {
|
||||
return Math.atan2(Vec.cpr(A, B), Vec.dpr(A, B));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the angle between the three vectors A, B, and C.
|
||||
* @param p1
|
||||
* @param pc
|
||||
* @param p2
|
||||
*/
|
||||
static ang3 = (p1: IVec, pc: IVec, p2: IVec): number => {
|
||||
// this,
|
||||
const v1 = Vec.vec(pc, p1);
|
||||
const v2 = Vec.vec(pc, p2);
|
||||
return Vec.ang(v1, v2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Angle between vector A and vector B in radians
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static angle = (A: IVec, B: IVec): number => {
|
||||
return Math.atan2(B[1] - A[1], B[0] - A[0]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get whether p1 is left of p2, relative to pc.
|
||||
* @param p1
|
||||
* @param pc
|
||||
* @param p2
|
||||
*/
|
||||
static clockwise = (p1: number[], pc: number[], p2: number[]): boolean => {
|
||||
return Vec.isLeft(p1, pc, p2) > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cross product (outer product) | A X B |
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static cpr = (A: number[], B: number[]): number => {
|
||||
return A[0] * B[1] - B[0] * A[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Dist length from A to B
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static dist = (A: number[], B: number[]): number => {
|
||||
return Math.hypot(A[1] - B[1], A[0] - B[0]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Dist length from A to B squared.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static dist2 = (A: IVec, B: IVec): number => {
|
||||
return Vec.len2(Vec.sub(A, B));
|
||||
};
|
||||
|
||||
/**
|
||||
* Distance between a point and the nearest point on a bounding box.
|
||||
* @param bounds The bounding box.
|
||||
* @param P The point
|
||||
* @returns
|
||||
*/
|
||||
static distanceToBounds = (
|
||||
bounds: {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
},
|
||||
P: number[]
|
||||
): number => {
|
||||
return Vec.dist(P, Vec.nearestPointOnBounds(bounds, P));
|
||||
};
|
||||
|
||||
/**
|
||||
* Distance between a point and the nearest point on a line segment between A and B
|
||||
* @param A The start of the line segment
|
||||
* @param B The end of the line segment
|
||||
* @param P The off-line point
|
||||
* @param clamp Whether to clamp the point between A and B.
|
||||
* @returns
|
||||
*/
|
||||
static distanceToLineSegment = (
|
||||
A: IVec,
|
||||
B: IVec,
|
||||
P: IVec,
|
||||
clamp = true
|
||||
): number => {
|
||||
return Vec.dist(P, Vec.nearestPointOnLineSegment(A, B, P, clamp));
|
||||
};
|
||||
|
||||
/**
|
||||
* Distance between a point and a line with a known unit vector that passes through a point.
|
||||
* @param A Any point on the line
|
||||
* @param u The unit vector for the line.
|
||||
* @param P A point not on the line to test.
|
||||
* @returns
|
||||
*/
|
||||
static distanceToLineThroughPoint = (A: IVec, u: IVec, P: IVec): number => {
|
||||
return Vec.dist(P, Vec.nearestPointOnLineThroughPoint(A, u, P));
|
||||
};
|
||||
|
||||
/**
|
||||
* Vector division by scalar.
|
||||
* @param A
|
||||
* @param n
|
||||
*/
|
||||
static div = (A: IVec, n: number): IVec => {
|
||||
return [A[0] / n, A[1] / n];
|
||||
};
|
||||
|
||||
/**
|
||||
* Vector division by vector.
|
||||
* @param A
|
||||
* @param n
|
||||
*/
|
||||
static divV = (A: IVec, B: IVec): IVec => {
|
||||
return [A[0] / B[0], A[1] / B[1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* Dot product
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static dpr = (A: number[], B: number[]): number => {
|
||||
return A[0] * B[0] + A[1] * B[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* A faster, though less accurate method for testing distances. Maybe faster?
|
||||
* @param A
|
||||
* @param B
|
||||
* @returns
|
||||
*/
|
||||
static fastDist = (A: number[], B: number[]): number[] => {
|
||||
const V = [B[0] - A[0], B[1] - A[1]];
|
||||
const aV = [Math.abs(V[0]), Math.abs(V[1])];
|
||||
let r = 1 / Math.max(aV[0], aV[1]);
|
||||
r = r * (1.29289 - (aV[0] + aV[1]) * r * 0.29289);
|
||||
return [V[0] * r, V[1] * r];
|
||||
};
|
||||
|
||||
/**
|
||||
* Interpolate from A to B when curVAL goes fromVAL: number[] => to
|
||||
* @param A
|
||||
* @param B
|
||||
* @param from Starting value
|
||||
* @param to Ending value
|
||||
* @param s Strength
|
||||
*/
|
||||
static int = (A: IVec, B: IVec, from: number, to: number, s = 1): IVec => {
|
||||
const t = (Vec.clamp(from, to) - from) / (to - from);
|
||||
return Vec.add(Vec.mul(A, 1 - t), Vec.mul(B, s));
|
||||
};
|
||||
|
||||
/**
|
||||
* Check of two vectors are identical.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static isEqual = (A: number[], B: number[]): boolean => {
|
||||
return A[0] === B[0] && A[1] === B[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get whether p1 is left of p2, relative to pc.
|
||||
* @param p1
|
||||
* @param pc
|
||||
* @param p2
|
||||
*/
|
||||
static isLeft = (p1: number[], pc: number[], p2: number[]): number => {
|
||||
// isLeft: >0 for counterclockwise
|
||||
// =0 for none (degenerate)
|
||||
// <0 for clockwise
|
||||
return (
|
||||
(pc[0] - p1[0]) * (p2[1] - p1[1]) - (p2[0] - p1[0]) * (pc[1] - p1[1])
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Length of the vector
|
||||
* @param A
|
||||
*/
|
||||
static len = (A: number[]): number => {
|
||||
return Math.hypot(A[0], A[1]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Length of the vector squared
|
||||
* @param A
|
||||
*/
|
||||
static len2 = (A: number[]): number => {
|
||||
return A[0] * A[0] + A[1] * A[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Interpolate vector A to B with a scalar t
|
||||
* @param A
|
||||
* @param B
|
||||
* @param t scalar
|
||||
*/
|
||||
static lrp = (A: IVec, B: IVec, t: number): IVec => {
|
||||
return Vec.add(A, Vec.mul(Vec.sub(B, A), t));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a vector comprised of the maximum of two or more vectors.
|
||||
*/
|
||||
static max = (...v: number[][]) => {
|
||||
return [Math.max(...v.map(a => a[0])), Math.max(...v.map(a => a[1]))];
|
||||
};
|
||||
|
||||
/**
|
||||
* Mean between two vectors or mid vector between two vectors
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static med = (A: IVec, B: IVec): IVec => {
|
||||
return Vec.mul(Vec.add(A, B), 0.5);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a vector comprised of the minimum of two or more vectors.
|
||||
*/
|
||||
static min = (...v: number[][]) => {
|
||||
return [Math.min(...v.map(a => a[0])), Math.min(...v.map(a => a[1]))];
|
||||
};
|
||||
|
||||
/**
|
||||
* Vector multiplication by scalar
|
||||
* @param A
|
||||
* @param n
|
||||
*/
|
||||
static mul = (A: IVec, n: number): IVec => {
|
||||
return [A[0] * n, A[1] * n];
|
||||
};
|
||||
|
||||
/**
|
||||
* Multiple two vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static mulV = (A: IVec, B: IVec): IVec => {
|
||||
return [A[0] * B[0], A[1] * B[1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the nearest point on a bounding box to a point P.
|
||||
* @param bounds The bounding box
|
||||
* @param P The point point
|
||||
* @returns
|
||||
*/
|
||||
static nearestPointOnBounds = (
|
||||
bounds: {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
},
|
||||
P: number[]
|
||||
): number[] => {
|
||||
return [
|
||||
Vec.clamp(P[0], bounds.minX, bounds.maxX),
|
||||
Vec.clamp(P[1], bounds.minY, bounds.maxY),
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the nearest point on a line segment between A and B
|
||||
* @param A The start of the line segment
|
||||
* @param B The end of the line segment
|
||||
* @param P The off-line point
|
||||
* @param clamp Whether to clamp the point between A and B.
|
||||
* @returns
|
||||
*/
|
||||
static nearestPointOnLineSegment = (
|
||||
A: IVec,
|
||||
B: IVec,
|
||||
P: IVec,
|
||||
clamp = true
|
||||
): IVec => {
|
||||
const u = Vec.uni(Vec.sub(B, A));
|
||||
const C = Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u)));
|
||||
|
||||
if (clamp) {
|
||||
if (C[0] < Math.min(A[0], B[0])) return A[0] < B[0] ? A : B;
|
||||
if (C[0] > Math.max(A[0], B[0])) return A[0] > B[0] ? A : B;
|
||||
if (C[1] < Math.min(A[1], B[1])) return A[1] < B[1] ? A : B;
|
||||
if (C[1] > Math.max(A[1], B[1])) return A[1] > B[1] ? A : B;
|
||||
}
|
||||
|
||||
return C;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the nearest point on a line with a known unit vector that passes through point A
|
||||
* @param A Any point on the line
|
||||
* @param u The unit vector for the line.
|
||||
* @param P A point not on the line to test.
|
||||
* @returns
|
||||
*/
|
||||
static nearestPointOnLineThroughPoint = (A: IVec, u: IVec, P: IVec): IVec => {
|
||||
return Vec.add(A, Vec.mul(u, Vec.pry(Vec.sub(P, A), u)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Negate a vector.
|
||||
* @param A
|
||||
*/
|
||||
static neg = (A: number[]): number[] => {
|
||||
return [-A[0], -A[1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get normalized / unit vector.
|
||||
* @param A
|
||||
*/
|
||||
static normalize = (A: IVec): IVec => {
|
||||
return Vec.uni(A);
|
||||
};
|
||||
|
||||
/**
|
||||
* Push a point A towards point B by a given distance.
|
||||
* @param A
|
||||
* @param B
|
||||
* @param d
|
||||
* @returns
|
||||
*/
|
||||
static nudge = (A: IVec, B: IVec, d: number): number[] => {
|
||||
if (Vec.isEqual(A, B)) return A;
|
||||
return Vec.add(A, Vec.mul(Vec.uni(Vec.sub(B, A)), d));
|
||||
};
|
||||
|
||||
/**
|
||||
* Push a point in a given angle by a given distance.
|
||||
* @param A
|
||||
* @param B
|
||||
* @param d
|
||||
*/
|
||||
static nudgeAtAngle = (A: number[], a: number, d: number): number[] => {
|
||||
return [Math.cos(a) * d + A[0], Math.sin(a) * d + A[1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* Perpendicular rotation of a vector A
|
||||
* @param A
|
||||
*/
|
||||
static per = (A: IVec): IVec => {
|
||||
return [A[1], -A[0]];
|
||||
};
|
||||
|
||||
static pointOffset = (A: IVec, B: IVec, offset: number): IVec => {
|
||||
let u = Vec.uni(Vec.sub(B, A));
|
||||
if (Vec.isEqual(A, B)) u = A;
|
||||
return Vec.add(A, Vec.mul(u, offset));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an array of points between two points.
|
||||
* @param A The first point.
|
||||
* @param B The second point.
|
||||
* @param steps The number of points to return.
|
||||
*/
|
||||
static pointsBetween = (A: IVec, B: IVec, steps = 6): number[][] => {
|
||||
return Array.from({ length: steps }).map((_, i) => {
|
||||
const t = i / (steps - 1);
|
||||
const k = Math.min(1, 0.5 + Math.abs(0.5 - t));
|
||||
return [...Vec.lrp(A, B, t), k];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Project A over B
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static pry = (A: number[], B: number[]): number => {
|
||||
return Vec.dpr(A, B) / Vec.len(B);
|
||||
};
|
||||
|
||||
static rescale = (a: number[], n: number): number[] => {
|
||||
const l = Vec.len(a);
|
||||
return [(n * a[0]) / l, (n * a[1]) / l];
|
||||
};
|
||||
|
||||
/**
|
||||
* Vector rotation by r (radians)
|
||||
* @param A
|
||||
* @param r rotation in radians
|
||||
*/
|
||||
static rot = (A: number[], r = 0): IVec => {
|
||||
return [
|
||||
A[0] * Math.cos(r) - A[1] * Math.sin(r),
|
||||
A[0] * Math.sin(r) + A[1] * Math.cos(r),
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Rotate a vector around another vector by r (radians)
|
||||
* @param A vector
|
||||
* @param C center
|
||||
* @param r rotation in radians
|
||||
*/
|
||||
static rotWith = (A: IVec, C: IVec, r = 0): IVec => {
|
||||
if (r === 0) return A;
|
||||
|
||||
const s = Math.sin(r);
|
||||
const c = Math.cos(r);
|
||||
|
||||
const px = A[0] - C[0];
|
||||
const py = A[1] - C[1];
|
||||
|
||||
const nx = px * c - py * s;
|
||||
const ny = px * s + py * c;
|
||||
|
||||
return [nx + C[0], ny + C[1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the slope between two points.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static slope = (A: number[], B: number[]) => {
|
||||
if (A[0] === B[0]) return NaN;
|
||||
return (A[1] - B[1]) / (A[0] - B[0]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Subtract vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static sub = (A: IVec, B: IVec): IVec => {
|
||||
return [A[0] - B[0], A[1] - B[1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* Subtract scalar from vector.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static subScalar = (A: IVec, n: number): IVec => {
|
||||
return [A[0] - n, A[1] - n];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the tangent between two vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
* @returns
|
||||
*/
|
||||
static tangent = (A: IVec, B: IVec): IVec => {
|
||||
return Vec.uni(Vec.sub(A, B));
|
||||
};
|
||||
|
||||
/**
|
||||
* Round a vector to two decimal places.
|
||||
* @param a
|
||||
*/
|
||||
static toFixed = (a: number[]): number[] => {
|
||||
return a.map(v => Math.round(v * 100) / 100);
|
||||
};
|
||||
|
||||
static toPoint = (v: IVec) => {
|
||||
return {
|
||||
x: v[0],
|
||||
y: v[1],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Round a vector to a precision length.
|
||||
* @param a
|
||||
* @param n
|
||||
*/
|
||||
static toPrecision = (a: number[], n = 4): number[] => {
|
||||
return [+a[0].toPrecision(n), +a[1].toPrecision(n)];
|
||||
};
|
||||
|
||||
static toVec = (v: { x: number; y: number }): IVec => [v.x, v.y];
|
||||
|
||||
/**
|
||||
* Get normalized / unit vector.
|
||||
* @param A
|
||||
*/
|
||||
static uni = (A: IVec): IVec => {
|
||||
return Vec.div(A, Vec.len(A));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the vector from vectors A to B.
|
||||
* @param A
|
||||
* @param B
|
||||
*/
|
||||
static vec = (A: IVec, B: IVec): IVec => {
|
||||
// A, B as vectors get the vector from A to B
|
||||
return [B[0] - A[0], B[1] - A[1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* Clamp a value into a range.
|
||||
* @param n
|
||||
* @param min
|
||||
*/
|
||||
static clamp(n: number, min: number): number;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
static clamp(n: number, min: number, max: number): number;
|
||||
|
||||
static clamp(n: number, min: number, max?: number): number {
|
||||
return Math.max(min, max !== undefined ? Math.min(n, max) : n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a value into a range.
|
||||
* @param n
|
||||
* @param min
|
||||
*/
|
||||
static clampV(A: number[], min: number): number[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
static clampV(A: number[], min: number, max: number): number[];
|
||||
|
||||
static clampV(A: number[], min: number, max?: number): number[] {
|
||||
return A.map(n =>
|
||||
max !== undefined ? Vec.clamp(n, min, max) : Vec.clamp(n, min)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross (for point in polygon)
|
||||
*
|
||||
*/
|
||||
static cross(x: number[], y: number[], z: number[]): number {
|
||||
return (y[0] - x[0]) * (z[1] - x[1]) - (z[0] - x[0]) * (y[1] - x[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap vector to nearest step.
|
||||
* @param A
|
||||
* @param step
|
||||
* @example
|
||||
* ```ts
|
||||
* Vec.snap([10.5, 28], 10) // [10, 30]
|
||||
* ```
|
||||
*/
|
||||
static snap(a: number[], step = 1) {
|
||||
return [Math.round(a[0] / step) * step, Math.round(a[1] / step) * step];
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Stephen Ruiz Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { IVec, IVec3 } from '../model/index.js';
|
||||
import { getStroke } from './get-stroke.js';
|
||||
|
||||
export function getSolidStrokePoints(
|
||||
points: (IVec | IVec3)[],
|
||||
lineWidth: number
|
||||
) {
|
||||
return getStroke(points, {
|
||||
size: lineWidth,
|
||||
thinning: 0.6,
|
||||
streamline: 0.5,
|
||||
smoothing: 0.5,
|
||||
easing: t => Math.sin((t * Math.PI) / 2),
|
||||
simulatePressure: points[0]?.length === 2,
|
||||
});
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
import type { IVec } from '../model/index.js';
|
||||
import { getStrokeRadius } from './get-stroke-radius.js';
|
||||
import type { StrokeOptions, StrokePoint } from './types.js';
|
||||
import {
|
||||
add,
|
||||
dist2,
|
||||
dpr,
|
||||
lrp,
|
||||
mul,
|
||||
neg,
|
||||
per,
|
||||
prj,
|
||||
rotAround,
|
||||
sub,
|
||||
uni,
|
||||
} from './vec.js';
|
||||
|
||||
const { min, PI } = Math;
|
||||
|
||||
// This is the rate of change for simulated pressure. It could be an option.
|
||||
const RATE_OF_PRESSURE_CHANGE = 0.275;
|
||||
|
||||
// Browser strokes seem to be off if PI is regular, a tiny offset seems to fix it
|
||||
const FIXED_PI = PI + 0.0001;
|
||||
|
||||
/**
|
||||
* ## getStrokeOutlinePoints
|
||||
* @description Get an array of points (as `[x, y]`) representing the outline of a stroke.
|
||||
* @param points An array of StrokePoints as returned from `getStrokePoints`.
|
||||
* @param options (optional) An object with options.
|
||||
* @param options.size The base size (diameter) of the stroke.
|
||||
* @param options.thinning The effect of pressure on the stroke's size.
|
||||
* @param options.smoothing How much to soften the stroke's edges.
|
||||
* @param options.easing An easing function to apply to each point's pressure.
|
||||
* @param options.simulatePressure Whether to simulate pressure based on velocity.
|
||||
* @param options.start Cap, taper and easing for the start of the line.
|
||||
* @param options.end Cap, taper and easing for the end of the line.
|
||||
* @param options.last Whether to handle the points as a completed stroke.
|
||||
*/
|
||||
export function getStrokeOutlinePoints(
|
||||
points: StrokePoint[],
|
||||
options: Partial<StrokeOptions> = {} as Partial<StrokeOptions>
|
||||
): IVec[] {
|
||||
const {
|
||||
size = 16,
|
||||
smoothing = 0.5,
|
||||
thinning = 0.5,
|
||||
simulatePressure = true,
|
||||
easing = t => t,
|
||||
start = {},
|
||||
end = {},
|
||||
last: isComplete = false,
|
||||
} = options;
|
||||
|
||||
const { cap: capStart = true, easing: taperStartEase = t => t * (2 - t) } =
|
||||
start;
|
||||
|
||||
const { cap: capEnd = true, easing: taperEndEase = t => --t * t * t + 1 } =
|
||||
end;
|
||||
|
||||
// We can't do anything with an empty array or a stroke with negative size.
|
||||
if (points.length === 0 || size <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// The total length of the line
|
||||
const totalLength = points[points.length - 1].runningLength;
|
||||
|
||||
const taperStart =
|
||||
start.taper === false
|
||||
? 0
|
||||
: start.taper === true
|
||||
? Math.max(size, totalLength)
|
||||
: (start.taper as number);
|
||||
|
||||
const taperEnd =
|
||||
end.taper === false
|
||||
? 0
|
||||
: end.taper === true
|
||||
? Math.max(size, totalLength)
|
||||
: (end.taper as number);
|
||||
|
||||
// The minimum allowed distance between points (squared)
|
||||
const minDistance = Math.pow(size * smoothing, 2);
|
||||
|
||||
// Our collected left and right points
|
||||
const leftPts: IVec[] = [];
|
||||
const rightPts: IVec[] = [];
|
||||
|
||||
// Previous pressure (start with average of first five pressures,
|
||||
// in order to prevent fat starts for every line. Drawn lines
|
||||
// almost always start slow!
|
||||
let prevPressure = points.slice(0, 10).reduce((acc, curr) => {
|
||||
let pressure = curr.pressure;
|
||||
|
||||
if (simulatePressure) {
|
||||
// Speed of change - how fast should the the pressure changing?
|
||||
const sp = min(1, curr.distance / size);
|
||||
// Rate of change - how much of a change is there?
|
||||
const rp = min(1, 1 - sp);
|
||||
// Accelerate the pressure
|
||||
pressure = min(1, acc + (rp - acc) * (sp * RATE_OF_PRESSURE_CHANGE));
|
||||
}
|
||||
|
||||
return (acc + pressure) / 2;
|
||||
}, points[0].pressure);
|
||||
|
||||
// The current radius
|
||||
let radius = getStrokeRadius(
|
||||
size,
|
||||
thinning,
|
||||
points[points.length - 1].pressure,
|
||||
easing
|
||||
);
|
||||
|
||||
// The radius of the first saved point
|
||||
let firstRadius: number | undefined = undefined;
|
||||
|
||||
// Previous vector
|
||||
let prevVector = points[0].vector;
|
||||
|
||||
// Previous left and right points
|
||||
let pl = points[0].point;
|
||||
let pr = pl;
|
||||
|
||||
// Temporary left and right points
|
||||
let tl = pl;
|
||||
let tr = pr;
|
||||
|
||||
// Keep track of whether the previous point is a sharp corner
|
||||
// ... so that we don't detect the same corner twice
|
||||
let isPrevPointSharpCorner = false;
|
||||
|
||||
// let short = true
|
||||
|
||||
/*
|
||||
Find the outline's left and right points
|
||||
|
||||
Iterating through the points and populate the rightPts and leftPts arrays,
|
||||
skipping the first and last pointsm, which will get caps later on.
|
||||
*/
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
let { pressure } = points[i];
|
||||
const { point, vector, distance, runningLength } = points[i];
|
||||
|
||||
// Removes noise from the end of the line
|
||||
if (i < points.length - 1 && totalLength - runningLength < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
Calculate the radius
|
||||
|
||||
If not thinning, the current point's radius will be half the size; or
|
||||
otherwise, the size will be based on the current (real or simulated)
|
||||
pressure.
|
||||
*/
|
||||
|
||||
if (thinning) {
|
||||
if (simulatePressure) {
|
||||
// If we're simulating pressure, then do so based on the distance
|
||||
// between the current point and the previous point, and the size
|
||||
// of the stroke. Otherwise, use the input pressure.
|
||||
const sp = min(1, distance / size);
|
||||
const rp = min(1, 1 - sp);
|
||||
pressure = min(
|
||||
1,
|
||||
prevPressure + (rp - prevPressure) * (sp * RATE_OF_PRESSURE_CHANGE)
|
||||
);
|
||||
}
|
||||
|
||||
radius = getStrokeRadius(size, thinning, pressure, easing);
|
||||
} else {
|
||||
radius = size / 2;
|
||||
}
|
||||
|
||||
if (firstRadius === undefined) {
|
||||
firstRadius = radius;
|
||||
}
|
||||
|
||||
/*
|
||||
Apply tapering
|
||||
|
||||
If the current length is within the taper distance at either the
|
||||
start or the end, calculate the taper strengths. Apply the smaller
|
||||
of the two taper strengths to the radius.
|
||||
*/
|
||||
|
||||
const ts =
|
||||
runningLength < taperStart
|
||||
? taperStartEase(runningLength / taperStart)
|
||||
: 1;
|
||||
|
||||
const te =
|
||||
totalLength - runningLength < taperEnd
|
||||
? taperEndEase((totalLength - runningLength) / taperEnd)
|
||||
: 1;
|
||||
|
||||
radius = Math.max(0.01, radius * Math.min(ts, te));
|
||||
|
||||
/* Add points to left and right */
|
||||
|
||||
/*
|
||||
Handle sharp corners
|
||||
|
||||
Find the difference (dot product) between the current and next vector.
|
||||
If the next vector is at more than a right angle to the current vector,
|
||||
draw a cap at the current point.
|
||||
*/
|
||||
|
||||
const nextVector = (i < points.length - 1 ? points[i + 1] : points[i])
|
||||
.vector;
|
||||
const nextDpr = i < points.length - 1 ? dpr(vector, nextVector) : 1.0;
|
||||
const prevDpr = dpr(vector, prevVector);
|
||||
|
||||
const isPointSharpCorner = prevDpr < 0 && !isPrevPointSharpCorner;
|
||||
const isNextPointSharpCorner = nextDpr !== null && nextDpr < 0;
|
||||
|
||||
if (isPointSharpCorner || isNextPointSharpCorner) {
|
||||
// It's a sharp corner. Draw a rounded cap and move on to the next point
|
||||
// Considering saving these and drawing them later? So that we can avoid
|
||||
// crossing future points.
|
||||
|
||||
const offset = mul(per(prevVector), radius);
|
||||
|
||||
for (let step = 1 / 13, t = 0; t <= 1; t += step) {
|
||||
tl = rotAround(sub(point, offset), point, FIXED_PI * t);
|
||||
leftPts.push(tl);
|
||||
|
||||
tr = rotAround(add(point, offset), point, FIXED_PI * -t);
|
||||
rightPts.push(tr);
|
||||
}
|
||||
|
||||
pl = tl;
|
||||
pr = tr;
|
||||
|
||||
if (isNextPointSharpCorner) {
|
||||
isPrevPointSharpCorner = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
isPrevPointSharpCorner = false;
|
||||
|
||||
// Handle the last point
|
||||
if (i === points.length - 1) {
|
||||
const offset = mul(per(vector), radius);
|
||||
leftPts.push(sub(point, offset));
|
||||
rightPts.push(add(point, offset));
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
Add regular points
|
||||
|
||||
Project points to either side of the current point, using the
|
||||
calculated size as a distance. If a point's distance to the
|
||||
previous point on that side greater than the minimum distance
|
||||
(or if the corner is kinda sharp), add the points to the side's
|
||||
points array.
|
||||
*/
|
||||
|
||||
const offset = mul(per(lrp(nextVector, vector, nextDpr)), radius);
|
||||
|
||||
tl = sub(point, offset);
|
||||
|
||||
if (i <= 1 || dist2(pl, tl) > minDistance) {
|
||||
leftPts.push(tl);
|
||||
pl = tl;
|
||||
}
|
||||
|
||||
tr = add(point, offset);
|
||||
|
||||
if (i <= 1 || dist2(pr, tr) > minDistance) {
|
||||
rightPts.push(tr);
|
||||
pr = tr;
|
||||
}
|
||||
|
||||
// Set variables for next iteration
|
||||
prevPressure = pressure;
|
||||
prevVector = vector;
|
||||
}
|
||||
|
||||
/*
|
||||
Drawing caps
|
||||
|
||||
Now that we have our points on either side of the line, we need to
|
||||
draw caps at the start and end. Tapered lines don't have caps, but
|
||||
may have dots for very short lines.
|
||||
*/
|
||||
|
||||
const firstPoint = points[0].point.slice(0, 2) as IVec;
|
||||
|
||||
const lastPoint =
|
||||
points.length > 1
|
||||
? (points[points.length - 1].point.slice(0, 2) as IVec)
|
||||
: add(points[0].point, [1, 1]);
|
||||
|
||||
const startCap: IVec[] = [];
|
||||
|
||||
const endCap: IVec[] = [];
|
||||
|
||||
/*
|
||||
Draw a dot for very short or completed strokes
|
||||
|
||||
If the line is too short to gather left or right points and if the line is
|
||||
not tapered on either side, draw a dot. If the line is tapered, then only
|
||||
draw a dot if the line is both very short and complete. If we draw a dot,
|
||||
we can just return those points.
|
||||
*/
|
||||
|
||||
if (points.length === 1) {
|
||||
if (!(taperStart || taperEnd) || isComplete) {
|
||||
const start = prj(
|
||||
firstPoint,
|
||||
uni(per(sub(firstPoint, lastPoint))),
|
||||
-(firstRadius || radius)
|
||||
);
|
||||
const dotPts: IVec[] = [];
|
||||
for (let step = 1 / 13, t = step; t <= 1; t += step) {
|
||||
dotPts.push(rotAround(start, firstPoint, FIXED_PI * 2 * t));
|
||||
}
|
||||
return dotPts;
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
Draw a start cap
|
||||
|
||||
Unless the line has a tapered start, or unless the line has a tapered end
|
||||
and the line is very short, draw a start cap around the first point. Use
|
||||
the distance between the second left and right point for the cap's radius.
|
||||
Finally remove the first left and right points. :psyduck:
|
||||
*/
|
||||
|
||||
if (taperStart || (taperEnd && points.length === 1)) {
|
||||
// The start point is tapered, noop
|
||||
} else if (capStart) {
|
||||
// Draw the round cap - add thirteen points rotating the right point around the start point to the left point
|
||||
for (let step = 1 / 13, t = step; t <= 1; t += step) {
|
||||
const pt = rotAround(rightPts[0], firstPoint, FIXED_PI * t);
|
||||
startCap.push(pt);
|
||||
}
|
||||
} else {
|
||||
// Draw the flat cap - add a point to the left and right of the start point
|
||||
const cornersVector = sub(leftPts[0], rightPts[0]);
|
||||
const offsetA = mul(cornersVector, 0.5);
|
||||
const offsetB = mul(cornersVector, 0.51);
|
||||
|
||||
startCap.push(
|
||||
sub(firstPoint, offsetA),
|
||||
sub(firstPoint, offsetB),
|
||||
add(firstPoint, offsetB),
|
||||
add(firstPoint, offsetA)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
Draw an end cap
|
||||
|
||||
If the line does not have a tapered end, and unless the line has a tapered
|
||||
start and the line is very short, draw a cap around the last point. Finally,
|
||||
remove the last left and right points. Otherwise, add the last point. Note
|
||||
that This cap is a full-turn-and-a-half: this prevents incorrect caps on
|
||||
sharp end turns.
|
||||
*/
|
||||
|
||||
const direction = per(neg(points[points.length - 1].vector));
|
||||
|
||||
if (taperEnd || (taperStart && points.length === 1)) {
|
||||
// Tapered end - push the last point to the line
|
||||
endCap.push(lastPoint);
|
||||
} else if (capEnd) {
|
||||
// Draw the round end cap
|
||||
const start = prj(lastPoint, direction, radius);
|
||||
for (let step = 1 / 29, t = step; t < 1; t += step) {
|
||||
endCap.push(rotAround(start, lastPoint, FIXED_PI * 3 * t));
|
||||
}
|
||||
} else {
|
||||
// Draw the flat end cap
|
||||
|
||||
endCap.push(
|
||||
add(lastPoint, mul(direction, radius)),
|
||||
add(lastPoint, mul(direction, radius * 0.99)),
|
||||
sub(lastPoint, mul(direction, radius * 0.99)),
|
||||
sub(lastPoint, mul(direction, radius))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Return the points in the correct winding order: begin on the left side, then
|
||||
continue around the end cap, then come back along the right side, and finally
|
||||
complete the start cap.
|
||||
*/
|
||||
|
||||
return leftPts.concat(endCap, rightPts.reverse(), startCap);
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import type { IVec, IVec3 } from '../model/index.js';
|
||||
import type { StrokeOptions, StrokePoint } from './types.js';
|
||||
import { add, dist, isEqual, lrp, sub, uni } from './vec.js';
|
||||
|
||||
/**
|
||||
* ## getStrokePoints
|
||||
* @description Get an array of points as objects with an adjusted point, pressure, vector, distance, and runningLength.
|
||||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional in both cases.
|
||||
* @param options (optional) An object with options.
|
||||
* @param options.size The base size (diameter) of the stroke.
|
||||
* @param options.thinning The effect of pressure on the stroke's size.
|
||||
* @param options.smoothing How much to soften the stroke's edges.
|
||||
* @param options.easing An easing function to apply to each point's pressure.
|
||||
* @param options.simulatePressure Whether to simulate pressure based on velocity.
|
||||
* @param options.start Cap, taper and easing for the start of the line.
|
||||
* @param options.end Cap, taper and easing for the end of the line.
|
||||
* @param options.last Whether to handle the points as a completed stroke.
|
||||
*/
|
||||
export function getStrokePoints<
|
||||
T extends IVec | IVec3,
|
||||
K extends { x: number; y: number; pressure?: number },
|
||||
>(points: (T | K)[], options = {} as StrokeOptions): StrokePoint[] {
|
||||
const { streamline = 0.5, size = 16, last: isComplete = false } = options;
|
||||
|
||||
// If we don't have any points, return an empty array.
|
||||
if (points.length === 0) return [];
|
||||
|
||||
// Find the interpolation level between points.
|
||||
const t = 0.15 + (1 - streamline) * 0.85;
|
||||
|
||||
// Whatever the input is, make sure that the points are in number[][].
|
||||
let pts: (IVec3 | IVec)[] = Array.isArray(points[0])
|
||||
? (points as T[])
|
||||
: (points as K[]).map(
|
||||
({ x, y, pressure = 0.5 }) => [x, y, pressure] as IVec3
|
||||
);
|
||||
|
||||
// Add extra points between the two, to help avoid "dash" lines
|
||||
// for strokes with tapered start and ends. Don't mutate the
|
||||
// input array!
|
||||
if (pts.length === 2) {
|
||||
const last = pts[1];
|
||||
pts = pts.slice(0, -1);
|
||||
for (let i = 1; i < 5; i++) {
|
||||
pts.push(lrp(pts[0] as IVec, last as IVec, i / 4));
|
||||
}
|
||||
}
|
||||
|
||||
// If there's only one point, add another point at a 1pt offset.
|
||||
// Don't mutate the input array!
|
||||
if (pts.length === 1) {
|
||||
pts = [
|
||||
...pts,
|
||||
[...add(pts[0] as IVec, [1, 1]), ...pts[0].slice(2)] as IVec,
|
||||
];
|
||||
}
|
||||
|
||||
// The strokePoints array will hold the points for the stroke.
|
||||
// Start it out with the first point, which needs no adjustment.
|
||||
const strokePoints: StrokePoint[] = [
|
||||
{
|
||||
point: [pts[0][0], pts[0][1]],
|
||||
pressure: (pts[0][2] ?? -1) >= 0 ? pts[0][2]! : 0.25,
|
||||
vector: [1, 1],
|
||||
distance: 0,
|
||||
runningLength: 0,
|
||||
},
|
||||
];
|
||||
|
||||
// A flag to see whether we've already reached out minimum length
|
||||
let hasReachedMinimumLength = false;
|
||||
|
||||
// We use the runningLength to keep track of the total distance
|
||||
let runningLength = 0;
|
||||
|
||||
// We're set this to the latest point, so we can use it to calculate
|
||||
// the distance and vector of the next point.
|
||||
let prev = strokePoints[0];
|
||||
|
||||
const max = pts.length - 1;
|
||||
|
||||
// Iterate through all of the points, creating StrokePoints.
|
||||
for (let i = 1; i < pts.length; i++) {
|
||||
const point =
|
||||
isComplete && i === max
|
||||
? // If we're at the last point, and `options.last` is true,
|
||||
// then add the actual input point.
|
||||
(pts[i].slice(0, 2) as IVec)
|
||||
: // Otherwise, using the t calculated from the streamline
|
||||
// option, interpolate a new point between the previous
|
||||
// point the current point.
|
||||
lrp(prev.point, pts[i] as IVec, t);
|
||||
|
||||
// If the new point is the same as the previous point, skip ahead.
|
||||
if (isEqual(prev.point, point)) continue;
|
||||
|
||||
// How far is the new point from the previous point?
|
||||
const distance = dist(point, prev.point);
|
||||
|
||||
// Add this distance to the total "running length" of the line.
|
||||
runningLength += distance;
|
||||
|
||||
// At the start of the line, we wait until the new point is a
|
||||
// certain distance away from the original point, to avoid noise
|
||||
if (i < max && !hasReachedMinimumLength) {
|
||||
if (runningLength < size) continue;
|
||||
hasReachedMinimumLength = true;
|
||||
// TODO: Backfill the missing points so that tapering works correctly.
|
||||
}
|
||||
// Create a new strokepoint (it will be the new "previous" one).
|
||||
prev = {
|
||||
// The adjusted point
|
||||
point,
|
||||
// The input pressure (or .5 if not specified)
|
||||
pressure: (pts[i][2] ?? -1) >= 0 ? pts[i][2]! : 0.5,
|
||||
// The vector from the current point to the previous point
|
||||
vector: uni(sub(prev.point, point)),
|
||||
// The distance between the current point and the previous point
|
||||
distance,
|
||||
// The total distance so far
|
||||
runningLength,
|
||||
};
|
||||
|
||||
// Push it to the strokePoints array.
|
||||
strokePoints.push(prev);
|
||||
}
|
||||
|
||||
// Set the vector of the first point to be the same as the second point.
|
||||
strokePoints[0].vector = strokePoints[1]?.vector || [0, 0];
|
||||
|
||||
return strokePoints;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Compute a radius based on the pressure.
|
||||
* @param size
|
||||
* @param thinning
|
||||
* @param pressure
|
||||
* @param easing
|
||||
* @internal
|
||||
*/
|
||||
export function getStrokeRadius(
|
||||
size: number,
|
||||
thinning: number,
|
||||
pressure: number,
|
||||
easing: (t: number) => number = t => t
|
||||
) {
|
||||
return size * easing(0.5 - thinning * (0.5 - pressure));
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { IVec, IVec3 } from '../model/index.js';
|
||||
import { getStrokeOutlinePoints } from './get-stroke-outline-points.js';
|
||||
import { getStrokePoints } from './get-stroke-points.js';
|
||||
import type { StrokeOptions } from './types.js';
|
||||
|
||||
/**
|
||||
* ## getStroke
|
||||
* @description Get an array of points describing a polygon that surrounds the input points.
|
||||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional in both cases.
|
||||
* @param options (optional) An object with options.
|
||||
* @param options.size The base size (diameter) of the stroke.
|
||||
* @param options.thinning The effect of pressure on the stroke's size.
|
||||
* @param options.smoothing How much to soften the stroke's edges.
|
||||
* @param options.easing An easing function to apply to each point's pressure.
|
||||
* @param options.simulatePressure Whether to simulate pressure based on velocity.
|
||||
* @param options.start Cap, taper and easing for the start of the line.
|
||||
* @param options.end Cap, taper and easing for the end of the line.
|
||||
* @param options.last Whether to handle the points as a completed stroke.
|
||||
*/
|
||||
|
||||
export function getStroke(
|
||||
points: (IVec | IVec3 | { x: number; y: number; pressure?: number })[],
|
||||
options: StrokeOptions = {} as StrokeOptions
|
||||
) {
|
||||
return getStrokeOutlinePoints(getStrokePoints(points, options), options);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export * from './get-solid-stroke-points.js';
|
||||
export * from './get-stroke.js';
|
||||
export * from './get-stroke-outline-points.js';
|
||||
export * from './get-stroke-points.js';
|
||||
export * from './get-stroke-radius.js';
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { IVec } from '../model/index.js';
|
||||
|
||||
/**
|
||||
* The options object for `getStroke` or `getStrokePoints`.
|
||||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional in both cases.
|
||||
* @param options (optional) An object with options.
|
||||
* @param options.size The base size (diameter) of the stroke.
|
||||
* @param options.thinning The effect of pressure on the stroke's size.
|
||||
* @param options.smoothing How much to soften the stroke's edges.
|
||||
* @param options.easing An easing function to apply to each point's pressure.
|
||||
* @param options.simulatePressure Whether to simulate pressure based on velocity.
|
||||
* @param options.start Cap, taper and easing for the start of the line.
|
||||
* @param options.end Cap, taper and easing for the end of the line.
|
||||
* @param options.last Whether to handle the points as a completed stroke.
|
||||
*/
|
||||
export interface StrokeOptions {
|
||||
size?: number;
|
||||
thinning?: number;
|
||||
smoothing?: number;
|
||||
streamline?: number;
|
||||
easing?: (pressure: number) => number;
|
||||
simulatePressure?: boolean;
|
||||
start?: {
|
||||
cap?: boolean;
|
||||
taper?: number | boolean;
|
||||
easing?: (distance: number) => number;
|
||||
};
|
||||
end?: {
|
||||
cap?: boolean;
|
||||
taper?: number | boolean;
|
||||
easing?: (distance: number) => number;
|
||||
};
|
||||
// Whether to handle the points as a completed stroke.
|
||||
last?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The points returned by `getStrokePoints`, and the input for `getStrokeOutlinePoints`.
|
||||
*/
|
||||
export interface StrokePoint {
|
||||
point: IVec;
|
||||
pressure: number;
|
||||
distance: number;
|
||||
vector: IVec;
|
||||
runningLength: number;
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import type { IVec } from '../model/index.js';
|
||||
|
||||
/**
|
||||
* Negate a vector.
|
||||
* @param A
|
||||
* @internal
|
||||
*/
|
||||
export function neg(A: IVec): IVec {
|
||||
return [-A[0], -A[1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
* @internal
|
||||
*/
|
||||
export function add(A: IVec, B: IVec): IVec {
|
||||
return [A[0] + B[0], A[1] + B[1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract vectors.
|
||||
* @param A
|
||||
* @param B
|
||||
* @internal
|
||||
*/
|
||||
export function sub(A: IVec, B: IVec): IVec {
|
||||
return [A[0] - B[0], A[1] - B[1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector multiplication by scalar
|
||||
* @param A
|
||||
* @param n
|
||||
* @internal
|
||||
*/
|
||||
export function mul(A: IVec, n: number): IVec {
|
||||
return [A[0] * n, A[1] * n];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector division by scalar.
|
||||
* @param A
|
||||
* @param n
|
||||
* @internal
|
||||
*/
|
||||
export function div(A: IVec, n: number): IVec {
|
||||
return [A[0] / n, A[1] / n];
|
||||
}
|
||||
|
||||
/**
|
||||
* Perpendicular rotation of a vector A
|
||||
* @param A
|
||||
* @internal
|
||||
*/
|
||||
export function per(A: IVec): IVec {
|
||||
return [A[1], -A[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dot product
|
||||
* @param A
|
||||
* @param B
|
||||
* @internal
|
||||
*/
|
||||
export function dpr(A: IVec, B: IVec) {
|
||||
return A[0] * B[0] + A[1] * B[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether two vectors are equal.
|
||||
* @param A
|
||||
* @param B
|
||||
* @internal
|
||||
*/
|
||||
export function isEqual(A: IVec, B: IVec) {
|
||||
return A[0] === B[0] && A[1] === B[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the vector
|
||||
* @param A
|
||||
* @internal
|
||||
*/
|
||||
export function len(A: IVec) {
|
||||
return Math.hypot(A[0], A[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the vector squared
|
||||
* @param A
|
||||
* @internal
|
||||
*/
|
||||
export function len2(A: IVec) {
|
||||
return A[0] * A[0] + A[1] * A[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dist length from A to B squared.
|
||||
* @param A
|
||||
* @param B
|
||||
* @internal
|
||||
*/
|
||||
export function dist2(A: IVec, B: IVec) {
|
||||
return len2(sub(A, B));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get normalized / unit vector.
|
||||
* @param A
|
||||
* @internal
|
||||
*/
|
||||
export function uni(A: IVec) {
|
||||
return div(A, len(A));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dist length from A to B
|
||||
* @param A
|
||||
* @param B
|
||||
* @internal
|
||||
*/
|
||||
export function dist(A: IVec, B: IVec) {
|
||||
return Math.hypot(A[1] - B[1], A[0] - B[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mean between two vectors or mid vector between two vectors
|
||||
* @param A
|
||||
* @param B
|
||||
* @internal
|
||||
*/
|
||||
export function med(A: IVec, B: IVec) {
|
||||
return mul(add(A, B), 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate a vector around another vector by r (radians)
|
||||
* @param A vector
|
||||
* @param C center
|
||||
* @param r rotation in radians
|
||||
* @internal
|
||||
*/
|
||||
export function rotAround(A: IVec, C: IVec, r: number): IVec {
|
||||
const s = Math.sin(r);
|
||||
const c = Math.cos(r);
|
||||
|
||||
const px = A[0] - C[0];
|
||||
const py = A[1] - C[1];
|
||||
|
||||
const nx = px * c - py * s;
|
||||
const ny = px * s + py * c;
|
||||
|
||||
return [nx + C[0], ny + C[1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate vector A to B with a scalar t
|
||||
* @param A
|
||||
* @param B
|
||||
* @param t scalar
|
||||
* @internal
|
||||
*/
|
||||
export function lrp(A: IVec, B: IVec, t: number) {
|
||||
return add(A, mul(sub(B, A), t));
|
||||
}
|
||||
|
||||
/**
|
||||
* Project a point A in the direction B by a scalar c
|
||||
* @param A
|
||||
* @param B
|
||||
* @param c
|
||||
* @internal
|
||||
*/
|
||||
export function prj(A: IVec, B: IVec, c: number) {
|
||||
return add(A, mul(B, c));
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import { type IVec, Vec } from './model/index.js';
|
||||
|
||||
export class Polyline {
|
||||
static len(points: IVec[]) {
|
||||
const n = points.length;
|
||||
|
||||
if (n < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
let len = 0;
|
||||
let curr: IVec;
|
||||
let prev = points[0];
|
||||
|
||||
while (++i < n) {
|
||||
curr = points[i];
|
||||
len += Vec.dist(prev, curr);
|
||||
prev = curr;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
static lenAtPoint(points: IVec[], point: IVec) {
|
||||
const n = points.length;
|
||||
let len = n;
|
||||
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
const a = points[i];
|
||||
const b = points[i + 1];
|
||||
|
||||
// start
|
||||
if (a[0] === point[0] && a[1] === point[1]) {
|
||||
return len;
|
||||
}
|
||||
|
||||
const aa = Vec.angle(a, point);
|
||||
const ba = Vec.angle(b, point);
|
||||
|
||||
if ((aa + ba) % Math.PI === 0) {
|
||||
len += Vec.dist(a, point);
|
||||
return len;
|
||||
}
|
||||
|
||||
len += Vec.dist(a, b);
|
||||
|
||||
// end
|
||||
if (b[0] === point[0] && b[1] === point[1]) {
|
||||
return len;
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
static nearestPoint(points: IVec[], point: IVec): IVec {
|
||||
const n = points.length;
|
||||
const r: IVec = [0, 0];
|
||||
let len = Infinity;
|
||||
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
const a = points[i];
|
||||
const b = points[i + 1];
|
||||
const p = Vec.nearestPointOnLineSegment(a, b, point, true);
|
||||
const d = Vec.dist(p, point);
|
||||
if (d < len) {
|
||||
len = d;
|
||||
r[0] = p[0];
|
||||
r[1] = p[1];
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
static pointAt(points: IVec[], ratio: number) {
|
||||
const n = points.length;
|
||||
|
||||
if (n === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (n === 1) {
|
||||
return points[0];
|
||||
}
|
||||
|
||||
if (ratio <= 0) {
|
||||
return points[0];
|
||||
}
|
||||
|
||||
if (ratio >= 1) {
|
||||
return points[n - 1];
|
||||
}
|
||||
|
||||
const total = Polyline.len(points);
|
||||
const len = total * ratio;
|
||||
return Polyline.pointAtLen(points, len);
|
||||
}
|
||||
|
||||
static pointAtLen(points: IVec[], len: number): IVec | null {
|
||||
const n = points.length;
|
||||
|
||||
if (n === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (n === 1) {
|
||||
return points[0];
|
||||
}
|
||||
|
||||
let fromStart = true;
|
||||
if (len < 0) {
|
||||
fromStart = false;
|
||||
len = -len;
|
||||
}
|
||||
|
||||
let tmp = 0;
|
||||
for (let j = 0, k = n - 1; j < k; j++) {
|
||||
const i = fromStart ? j : k - 1 - j;
|
||||
const a = points[i];
|
||||
const b = points[i + 1];
|
||||
const d = Vec.dist(a, b);
|
||||
|
||||
if (len <= tmp + d) {
|
||||
const t = ((fromStart ? 1 : -1) * (len - tmp)) / d;
|
||||
return Vec.lrp(a, b, t) as IVec;
|
||||
}
|
||||
|
||||
tmp += d;
|
||||
}
|
||||
|
||||
const lastPoint = fromStart ? points[n - 1] : points[0];
|
||||
return lastPoint;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* XYWH represents the x, y, width, and height of an element or block.
|
||||
*/
|
||||
export type XYWH = [number, number, number, number];
|
||||
|
||||
/**
|
||||
* SerializedXYWH is a string that represents the x, y, width, and height of a block.
|
||||
*/
|
||||
export type SerializedXYWH = `[${number},${number},${number},${number}]`;
|
||||
|
||||
export function serializeXYWH(
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number
|
||||
): SerializedXYWH {
|
||||
return `[${x},${y},${w},${h}]`;
|
||||
}
|
||||
|
||||
export function deserializeXYWH(xywh: string): XYWH {
|
||||
try {
|
||||
return JSON.parse(xywh) as XYWH;
|
||||
} catch (e) {
|
||||
console.error('Failed to deserialize xywh', xywh);
|
||||
console.error(e);
|
||||
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user