@js-draw/math
Version:
A math library for js-draw.
256 lines (255 loc) • 11 kB
JavaScript
"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _BezierJSWrapper_bezierJs;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BezierJSWrapper = void 0;
const bezier_js_1 = require("bezier-js");
const Vec2_1 = require("../Vec2");
const LineSegment2_1 = __importDefault(require("./LineSegment2"));
const Rect2_1 = __importDefault(require("./Rect2"));
const Parameterized2DShape_1 = __importDefault(require("./Parameterized2DShape"));
/**
* A lazy-initializing wrapper around Bezier-js.
*
* Subclasses may override `at`, `derivativeAt`, and `normal` with functions
* that do not initialize a `bezier-js` `Bezier`.
*
* **Do not use this class directly.** It may be removed/replaced in a future release.
* @internal
*/
class BezierJSWrapper extends Parameterized2DShape_1.default {
constructor(bezierJsBezier) {
super();
_BezierJSWrapper_bezierJs.set(this, null);
if (bezierJsBezier) {
__classPrivateFieldSet(this, _BezierJSWrapper_bezierJs, bezierJsBezier, "f");
}
}
getBezier() {
if (!__classPrivateFieldGet(this, _BezierJSWrapper_bezierJs, "f")) {
__classPrivateFieldSet(this, _BezierJSWrapper_bezierJs, new bezier_js_1.Bezier(this.getPoints().map((p) => p.xy)), "f");
}
return __classPrivateFieldGet(this, _BezierJSWrapper_bezierJs, "f");
}
signedDistance(point) {
// .d: Distance
return this.nearestPointTo(point).point.distanceTo(point);
}
/**
* @returns the (more) exact distance from `point` to this.
*
* @see {@link approximateDistance}
*/
distance(point) {
// A Bézier curve has no interior, thus, signed distance is the same as distance.
return this.signedDistance(point);
}
/**
* @returns the curve evaluated at `t`.
*/
at(t) {
return Vec2_1.Vec2.ofXY(this.getBezier().get(t));
}
/** @returns the curve's directional derivative at `t`. */
derivativeAt(t) {
return Vec2_1.Vec2.ofXY(this.getBezier().derivative(t));
}
secondDerivativeAt(t) {
return Vec2_1.Vec2.ofXY(this.getBezier().dderivative(t));
}
/** @returns the [normal vector](https://en.wikipedia.org/wiki/Normal_(geometry)) to this curve at `t`. */
normal(t) {
return Vec2_1.Vec2.ofXY(this.getBezier().normal(t));
}
normalAt(t) {
return this.normal(t);
}
tangentAt(t) {
return this.derivativeAt(t).normalized();
}
getTightBoundingBox() {
const bbox = this.getBezier().bbox();
const width = bbox.x.max - bbox.x.min;
const height = bbox.y.max - bbox.y.min;
return new Rect2_1.default(bbox.x.min, bbox.y.min, width, height);
}
argIntersectsLineSegment(line) {
// Bezier-js has a bug when all control points of a Bezier curve lie on
// a line. Our solution involves converting the Bezier into a line, then
// finding the parameter value that produced the intersection.
//
// TODO: This is unnecessarily slow. A better solution would be to fix
// the bug upstream.
const asLine = LineSegment2_1.default.ofSmallestContainingPoints(this.getPoints());
if (asLine) {
const intersection = asLine.intersectsLineSegment(line);
return intersection.map((p) => this.nearestPointTo(p).parameterValue);
}
const bezier = this.getBezier();
return bezier
.intersects(line)
.map((t) => {
// We're using the .intersects(line) function, which is documented
// to always return numbers. However, to satisfy the type checker (and
// possibly improperly-defined types),
if (typeof t === 'string') {
t = parseFloat(t);
}
const point = Vec2_1.Vec2.ofXY(this.at(t));
// Ensure that the intersection is on the line segment
if (point.distanceTo(line.p1) > line.length || point.distanceTo(line.p2) > line.length) {
return null;
}
return t;
})
.filter((entry) => entry !== null);
}
splitAt(t) {
if (t <= 0 || t >= 1) {
return [this];
}
const bezier = this.getBezier();
const split = bezier.split(t);
return [
new BezierJSWrapperImpl(split.left.points.map((point) => Vec2_1.Vec2.ofXY(point)), split.left),
new BezierJSWrapperImpl(split.right.points.map((point) => Vec2_1.Vec2.ofXY(point)), split.right),
];
}
nearestPointTo(point) {
// One implementation could be similar to this:
// const projection = this.getBezier().project(point);
// return {
// point: Vec2.ofXY(projection),
// parameterValue: projection.t!,
// };
// However, Bezier-js is rather impercise (and relies on a lookup table).
// Thus, we instead use Newton's Method:
// We want to find t such that f(t) = |B(t) - p|² is minimized.
// Expanding,
// f(t) = (Bₓ(t) - pₓ)² + (Bᵧ(t) - pᵧ)²
// ⇒ f'(t) = Dₜ(Bₓ(t) - pₓ)² + Dₜ(Bᵧ(t) - pᵧ)²
// ⇒ f'(t) = 2(Bₓ(t) - pₓ)(Bₓ'(t)) + 2(Bᵧ(t) - pᵧ)(Bᵧ'(t))
// = 2Bₓ(t)Bₓ'(t) - 2pₓBₓ'(t) + 2Bᵧ(t)Bᵧ'(t) - 2pᵧBᵧ'(t)
// ⇒ f''(t)= 2Bₓ'(t)Bₓ'(t) + 2Bₓ(t)Bₓ''(t) - 2pₓBₓ''(t) + 2Bᵧ'(t)Bᵧ'(t)
// + 2Bᵧ(t)Bᵧ''(t) - 2pᵧBᵧ''(t)
// Because f'(t) = 0 at relative extrema, we can use Newton's Method
// to improve on an initial guess.
const sqrDistAt = (t) => point.squareDistanceTo(this.at(t));
const yIntercept = sqrDistAt(0);
let t = 0;
let minSqrDist = yIntercept;
// Start by testing a few points:
const pointsToTest = 4;
for (let i = 0; i < pointsToTest; i++) {
const testT = i / (pointsToTest - 1);
const testMinSqrDist = sqrDistAt(testT);
if (testMinSqrDist < minSqrDist) {
t = testT;
minSqrDist = testMinSqrDist;
}
}
// To use Newton's Method, we need to evaluate the second derivative of the distance
// function:
const secondDerivativeAt = (t) => {
// f''(t) = 2Bₓ'(t)Bₓ'(t) + 2Bₓ(t)Bₓ''(t) - 2pₓBₓ''(t)
// + 2Bᵧ'(t)Bᵧ'(t) + 2Bᵧ(t)Bᵧ''(t) - 2pᵧBᵧ''(t)
const b = this.at(t);
const bPrime = this.derivativeAt(t);
const bPrimePrime = this.secondDerivativeAt(t);
return (2 * bPrime.x * bPrime.x +
2 * b.x * bPrimePrime.x -
2 * point.x * bPrimePrime.x +
2 * bPrime.y * bPrime.y +
2 * b.y * bPrimePrime.y -
2 * point.y * bPrimePrime.y);
};
// Because we're zeroing f'(t), we also need to be able to compute it:
const derivativeAt = (t) => {
// f'(t) = 2Bₓ(t)Bₓ'(t) - 2pₓBₓ'(t) + 2Bᵧ(t)Bᵧ'(t) - 2pᵧBᵧ'(t)
const b = this.at(t);
const bPrime = this.derivativeAt(t);
return (2 * b.x * bPrime.x - 2 * point.x * bPrime.x + 2 * b.y * bPrime.y - 2 * point.y * bPrime.y);
};
const iterate = () => {
const slope = secondDerivativeAt(t);
if (slope === 0)
return;
// We intersect a line through the point on f'(t) at t with the x-axis:
// y = m(x - x₀) + y₀
// ⇒ x - x₀ = (y - y₀) / m
// ⇒ x = (y - y₀) / m + x₀
//
// Thus, when zeroed,
// tN = (0 - f'(t)) / m + t
const newT = (0 - derivativeAt(t)) / slope + t;
//const distDiff = sqrDistAt(newT) - sqrDistAt(t);
//console.assert(distDiff <= 0, `${-distDiff} >= 0`);
t = newT;
if (t > 1) {
t = 1;
}
else if (t < 0) {
t = 0;
}
};
for (let i = 0; i < 12; i++) {
iterate();
}
return { parameterValue: t, point: this.at(t) };
}
intersectsBezier(other) {
const intersections = this.getBezier().intersects(other.getBezier());
if (!intersections || intersections.length === 0) {
return [];
}
const result = [];
for (const intersection of intersections) {
// From http://pomax.github.io/bezierjs/#intersect-curve,
// .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(intersection);
if (!match) {
throw new Error(`Incorrect format returned by .intersects: ${intersections} should be array of "number/number"!`);
}
const t = parseFloat(match[1]);
result.push({
parameterValue: t,
point: this.at(t),
});
}
return result;
}
toString() {
return `Bézier(${this.getPoints()
.map((point) => point.toString())
.join(', ')})`;
}
}
exports.BezierJSWrapper = BezierJSWrapper;
_BezierJSWrapper_bezierJs = new WeakMap();
/**
* Private concrete implementation of `BezierJSWrapper`, used by methods above that need to return a wrapper
* around a `Bezier`.
*/
class BezierJSWrapperImpl extends BezierJSWrapper {
constructor(controlPoints, curve) {
super(curve);
this.controlPoints = controlPoints;
}
getPoints() {
return this.controlPoints;
}
}
exports.default = BezierJSWrapper;