UNPKG

@js-draw/math

Version:
256 lines (255 loc) 11 kB
"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;