Socket
Socket
Sign inDemoInstall

@js-draw/math

Package Overview
Dependencies
Maintainers
1
Versions
20
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@js-draw/math - npm Package Compare versions

Comparing version 1.16.0 to 1.17.0

dist/cjs/shapes/Parameterized2DShape.d.ts

2

dist/cjs/lib.d.ts

@@ -20,3 +20,3 @@ /**

export { LineSegment2 } from './shapes/LineSegment2';
export { Path, PathCommandType, PathCommand, LinePathCommand, MoveToPathCommand, QuadraticBezierPathCommand, CubicBezierPathCommand, } from './shapes/Path';
export { Path, IntersectionResult as PathIntersectionResult, CurveIndexRecord as PathCurveIndex, PathCommandType, PathCommand, LinePathCommand, MoveToPathCommand, QuadraticBezierPathCommand, CubicBezierPathCommand, } from './shapes/Path';
export { Rect2 } from './shapes/Rect2';

@@ -23,0 +23,0 @@ export { QuadraticBezier } from './shapes/QuadraticBezier';

@@ -41,2 +41,5 @@ import LineSegment2 from './LineSegment2';

* Returns a bounding box that precisely fits the content of this shape.
*
* **Note**: This bounding box should aligned with the x/y axes. (Thus, it may be
* possible to find a tighter bounding box not axes-aligned).
*/

@@ -43,0 +46,0 @@ abstract getTightBoundingBox(): Rect2;

import { Bezier } from 'bezier-js';
import { Point2, Vec2 } from '../Vec2';
import Abstract2DShape from './Abstract2DShape';
import LineSegment2 from './LineSegment2';
import Rect2 from './Rect2';
import Parameterized2DShape from './Parameterized2DShape';
/**

@@ -12,9 +12,10 @@ * A lazy-initializing wrapper around Bezier-js.

*
* Do not use this class directly. It may be removed/replaced in a future release.
* **Do not use this class directly.** It may be removed/replaced in a future release.
* @internal
*/
declare abstract class BezierJSWrapper extends Abstract2DShape {
export declare abstract class BezierJSWrapper extends Parameterized2DShape {
#private;
protected constructor(bezierJsBezier?: Bezier);
/** Returns the start, control points, and end point of this Bézier. */
abstract getPoints(): Point2[];
abstract getPoints(): readonly Point2[];
protected getBezier(): Bezier;

@@ -33,6 +34,15 @@ signedDistance(point: Point2): number;

derivativeAt(t: number): Point2;
secondDerivativeAt(t: number): Point2;
normal(t: number): Vec2;
normalAt(t: number): Vec2;
tangentAt(t: number): Vec2;
getTightBoundingBox(): Rect2;
intersectsLineSegment(line: LineSegment2): Point2[];
argIntersectsLineSegment(line: LineSegment2): number[];
splitAt(t: number): [BezierJSWrapper] | [BezierJSWrapper, BezierJSWrapper];
nearestPointTo(point: Point2): {
parameterValue: number;
point: import("../Vec3").Vec3;
};
toString(): string;
}
export default BezierJSWrapper;
"use strict";
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 __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {

@@ -13,2 +8,7 @@ if (kind === "m") throw new TypeError("Private method is not writable");

};
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) {

@@ -19,6 +19,7 @@ return (mod && mod.__esModule) ? mod : { "default": mod };

Object.defineProperty(exports, "__esModule", { value: true });
exports.BezierJSWrapper = void 0;
const bezier_js_1 = require("bezier-js");
const Vec2_1 = require("../Vec2");
const Abstract2DShape_1 = __importDefault(require("./Abstract2DShape"));
const Rect2_1 = __importDefault(require("./Rect2"));
const Parameterized2DShape_1 = __importDefault(require("./Parameterized2DShape"));
/**

@@ -30,9 +31,12 @@ * A lazy-initializing wrapper around Bezier-js.

*
* Do not use this class directly. It may be removed/replaced in a future release.
* **Do not use this class directly.** It may be removed/replaced in a future release.
* @internal
*/
class BezierJSWrapper extends Abstract2DShape_1.default {
constructor() {
super(...arguments);
class BezierJSWrapper extends Parameterized2DShape_1.default {
constructor(bezierJsBezier) {
super();
_BezierJSWrapper_bezierJs.set(this, null);
if (bezierJsBezier) {
__classPrivateFieldSet(this, _BezierJSWrapper_bezierJs, bezierJsBezier, "f");
}
}

@@ -47,3 +51,3 @@ getBezier() {

// .d: Distance
return this.getBezier().project(point.xy).d;
return this.nearestPointTo(point).point.distanceTo(point);
}

@@ -68,5 +72,14 @@ /**

}
secondDerivativeAt(t) {
return Vec2_1.Vec2.ofXY(this.getBezier().dderivative(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() {

@@ -78,5 +91,5 @@ const bbox = this.getBezier().bbox();

}
intersectsLineSegment(line) {
argIntersectsLineSegment(line) {
const bezier = this.getBezier();
const intersectionPoints = bezier.intersects(line).map(t => {
return bezier.intersects(line).map(t => {
// We're using the .intersects(line) function, which is documented

@@ -88,14 +101,118 @@ // to always return numbers. However, to satisfy the type checker (and

}
const point = Vec2_1.Vec2.ofXY(bezier.get(t));
const point = Vec2_1.Vec2.ofXY(this.at(t));
// Ensure that the intersection is on the line segment
if (point.minus(line.p1).magnitude() > line.length
|| point.minus(line.p2).magnitude() > line.length) {
if (point.distanceTo(line.p1) > line.length
|| point.distanceTo(line.p2) > line.length) {
return null;
}
return point;
return t;
}).filter(entry => entry !== null);
return intersectionPoints;
}
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);
// 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) };
}
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;
import Mat33 from '../Mat33';
import Rect2 from './Rect2';
import { Vec2, Point2 } from '../Vec2';
import Abstract2DShape from './Abstract2DShape';
import Parameterized2DShape from './Parameterized2DShape';
import Vec3 from '../Vec3';
interface IntersectionResult {

@@ -10,3 +11,3 @@ point: Point2;

/** Represents a line segment. A `LineSegment2` is immutable. */
export declare class LineSegment2 extends Abstract2DShape {
export declare class LineSegment2 extends Parameterized2DShape {
private readonly point1;

@@ -32,4 +33,5 @@ private readonly point2;

get p2(): Point2;
get center(): Point2;
/**
* Gets a point a distance `t` along this line.
* Gets a point a **distance** `t` along this line.
*

@@ -47,4 +49,16 @@ * @deprecated

at(t: number): Point2;
normalAt(_t: number): Vec2;
tangentAt(_t: number): Vec3;
splitAt(t: number): [LineSegment2] | [LineSegment2, LineSegment2];
/**
* Returns the intersection of this with another line segment.
*
* **WARNING**: The parameter value returned by this method does not range from 0 to 1 and
* is currently a length.
* This will change in a future release.
* @deprecated
*/
intersection(other: LineSegment2): IntersectionResult | null;
intersects(other: LineSegment2): boolean;
argIntersectsLineSegment(lineSegment: LineSegment2): number[];
/**

@@ -58,4 +72,8 @@ * Returns the points at which this line segment intersects the

*/
intersectsLineSegment(lineSegment: LineSegment2): import("../Vec3").Vec3[];
closestPointTo(target: Point2): import("../Vec3").Vec3;
intersectsLineSegment(lineSegment: LineSegment2): Vec3[];
closestPointTo(target: Point2): Vec3;
nearestPointTo(target: Vec3): {
point: Vec3;
parameterValue: number;
};
/**

@@ -73,3 +91,14 @@ * Returns the distance from this line segment to `target`.

toString(): string;
/**
* Returns `true` iff this is equivalent to `other`.
*
* **Options**:
* - `tolerance`: The maximum difference between endpoints. (Default: 0)
* - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
*/
eq(other: LineSegment2, options?: {
tolerance?: number;
ignoreDirection?: boolean;
}): boolean;
}
export default LineSegment2;

@@ -9,5 +9,5 @@ "use strict";

const Vec2_1 = require("../Vec2");
const Abstract2DShape_1 = __importDefault(require("./Abstract2DShape"));
const Parameterized2DShape_1 = __importDefault(require("./Parameterized2DShape"));
/** Represents a line segment. A `LineSegment2` is immutable. */
class LineSegment2 extends Abstract2DShape_1.default {
class LineSegment2 extends Parameterized2DShape_1.default {
/** Creates a new `LineSegment2` from its endpoints. */

@@ -36,4 +36,7 @@ constructor(point1, point2) {

}
get center() {
return this.point1.lerp(this.point2, 0.5);
}
/**
* Gets a point a distance `t` along this line.
* Gets a point a **distance** `t` along this line.
*

@@ -55,3 +58,27 @@ * @deprecated

}
normalAt(_t) {
return this.direction.orthog();
}
tangentAt(_t) {
return this.direction;
}
splitAt(t) {
if (t <= 0 || t >= 1) {
return [this];
}
return [
new LineSegment2(this.point1, this.at(t)),
new LineSegment2(this.at(t), this.point2),
];
}
/**
* Returns the intersection of this with another line segment.
*
* **WARNING**: The parameter value returned by this method does not range from 0 to 1 and
* is currently a length.
* This will change in a future release.
* @deprecated
*/
intersection(other) {
// TODO(v2.0.0): Make this return a `t` value from `0` to `1`.
// We want x₁(t) = x₂(t) and y₁(t) = y₂(t)

@@ -115,6 +142,6 @@ // Observe that

// Ensure the result is in this/the other segment.
const resultToP1 = resultPoint.minus(this.point1).magnitude();
const resultToP2 = resultPoint.minus(this.point2).magnitude();
const resultToP3 = resultPoint.minus(other.point1).magnitude();
const resultToP4 = resultPoint.minus(other.point2).magnitude();
const resultToP1 = resultPoint.distanceTo(this.point1);
const resultToP2 = resultPoint.distanceTo(this.point2);
const resultToP3 = resultPoint.distanceTo(other.point1);
const resultToP4 = resultPoint.distanceTo(other.point2);
if (resultToP1 > this.length

@@ -134,2 +161,9 @@ || resultToP2 > this.length

}
argIntersectsLineSegment(lineSegment) {
const intersection = this.intersection(lineSegment);
if (intersection) {
return [intersection.t / this.length];
}
return [];
}
/**

@@ -152,2 +186,5 @@ * Returns the points at which this line segment intersects the

closestPointTo(target) {
return this.nearestPointTo(target).point;
}
nearestPointTo(target) {
// Distance from P1 along this' direction.

@@ -158,9 +195,9 @@ const projectedDistFromP1 = target.minus(this.p1).dot(this.direction);

if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) {
return projection;
return { point: projection, parameterValue: projectedDistFromP1 / this.length };
}
if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) {
return this.p2;
return { point: this.p2, parameterValue: 1 };
}
else {
return this.p1;
return { point: this.p1, parameterValue: 0 };
}

@@ -188,4 +225,20 @@ }

}
/**
* Returns `true` iff this is equivalent to `other`.
*
* **Options**:
* - `tolerance`: The maximum difference between endpoints. (Default: 0)
* - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
*/
eq(other, options) {
if (!(other instanceof LineSegment2)) {
return false;
}
const tolerance = options?.tolerance;
const ignoreDirection = options?.ignoreDirection ?? true;
return ((other.p1.eq(this.p1, tolerance) && other.p2.eq(this.p2, tolerance))
|| (ignoreDirection && other.p1.eq(this.p2, tolerance) && other.p2.eq(this.p1, tolerance)));
}
}
exports.LineSegment2 = LineSegment2;
exports.default = LineSegment2;

@@ -5,3 +5,3 @@ import LineSegment2 from './LineSegment2';

import { Point2 } from '../Vec2';
import Abstract2DShape from './Abstract2DShape';
import Parameterized2DShape from './Parameterized2DShape';
export declare enum PathCommandType {

@@ -33,9 +33,20 @@ LineTo = 0,

export type PathCommand = CubicBezierPathCommand | QuadraticBezierPathCommand | MoveToPathCommand | LinePathCommand;
interface IntersectionResult {
curve: Abstract2DShape;
/** @internal @deprecated */
export interface IntersectionResult {
curve: Parameterized2DShape;
curveIndex: number;
/** Parameter value for the closest point **on** the path to the intersection. @internal @deprecated */
parameterValue?: number;
/** Point at which the intersection occured. */
point: Point2;
}
/**
* Allows indexing a particular part of a path.
*
* @see {@link Path.at} {@link Path.tangentAt}
*/
export interface CurveIndexRecord {
curveIndex: number;
parameterValue: number;
}
/**
* Represents a union of lines and curves.

@@ -62,3 +73,3 @@ */

private cachedGeometry;
get geometry(): Abstract2DShape[];
get geometry(): Parameterized2DShape[];
/**

@@ -92,6 +103,27 @@ * Iterates through the start/end points of each component in this path.

intersection(line: LineSegment2, strokeRadius?: number): IntersectionResult[];
/**
* @returns the nearest point on this path to the given `point`.
*
* @internal
* @beta
*/
nearestPointTo(point: Point2): IntersectionResult;
at(index: CurveIndexRecord): import("../Vec3").Vec3;
tangentAt(index: CurveIndexRecord): import("../Vec3").Vec3;
private static mapPathCommand;
mapPoints(mapping: (point: Point2) => Point2): Path;
transformedBy(affineTransfm: Mat33): Path;
union(other: Path | null): Path;
union(other: Path | null, options?: {
allowReverse?: boolean;
}): Path;
/**
* @returns a version of this path with the direction reversed.
*
* Example:
* ```ts,runnable,console
* import {Path} from '@js-draw/math';
* console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
* ```
*/
reversed(): Path;
private getEndPoint;

@@ -110,2 +142,4 @@ /**

closedRoughlyIntersects(rect: Rect2): boolean;
/** @returns true if all points on this are equivalent to the points on `other` */
eq(other: Path, tolerance?: number): boolean;
/**

@@ -112,0 +146,0 @@ * Returns a path that outlines `rect`.

@@ -239,3 +239,3 @@ "use strict";

// the current minimum.
if (!bbox.grownBy(minDist).containsPoint(point)) {
if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
continue;

@@ -278,3 +278,3 @@ }

const stoppingThreshold = strokeRadius / 1000;
// Returns the maximum x value explored
// Returns the maximum parameter value explored
const raymarchFrom = (startPoint,

@@ -323,5 +323,10 @@ // Direction to march in (multiplies line.direction)

point: currentPoint,
parameterValue: NaN,
parameterValue: NaN, // lastPart.nearestPointTo(currentPoint).parameterValue,
curve: lastPart,
curveIndex: this.geometry.indexOf(lastPart),
});
// Slightly increase the parameter value to prevent the same point from being
// added to the results twice.
const parameterIncrease = strokeRadius / 20 / line.length;
lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
}

@@ -359,10 +364,14 @@ return lastParameter;

}
let index = 0;
for (const part of this.geometry) {
const intersection = part.intersectsLineSegment(line);
if (intersection.length > 0) {
const intersections = part.argIntersectsLineSegment(line);
for (const intersection of intersections) {
result.push({
curve: part,
point: intersection[0],
curveIndex: index,
point: part.at(intersection),
parameterValue: intersection,
});
}
index++;
}

@@ -380,2 +389,38 @@ // If given a non-zero strokeWidth, attempt to raymarch.

}
/**
* @returns the nearest point on this path to the given `point`.
*
* @internal
* @beta
*/
nearestPointTo(point) {
// Find the closest point on this
let closestSquareDist = Infinity;
let closestPartIndex = 0;
let closestParameterValue = 0;
let closestPoint = this.startPoint;
for (let i = 0; i < this.geometry.length; i++) {
const current = this.geometry[i];
const nearestPoint = current.nearestPointTo(point);
const sqareDist = nearestPoint.point.squareDistanceTo(point);
if (i === 0 || sqareDist < closestSquareDist) {
closestPartIndex = i;
closestSquareDist = sqareDist;
closestParameterValue = nearestPoint.parameterValue;
closestPoint = nearestPoint.point;
}
}
return {
curve: this.geometry[closestPartIndex],
curveIndex: closestPartIndex,
parameterValue: closestParameterValue,
point: closestPoint,
};
}
at(index) {
return this.geometry[index.curveIndex].at(index.parameterValue);
}
tangentAt(index) {
return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
}
static mapPathCommand(part, mapping) {

@@ -424,15 +469,82 @@ switch (part.kind) {

// Creates a new path by joining [other] to the end of this path
union(other) {
union(other,
// allowReverse: true iff reversing other or this is permitted if it means
// no moveTo command is necessary when unioning the paths.
options = { allowReverse: true }) {
if (!other) {
return this;
}
return new Path(this.startPoint, [
...this.parts,
{
kind: PathCommandType.MoveTo,
point: other.startPoint,
},
...other.parts,
]);
const thisEnd = this.getEndPoint();
let newParts = [];
if (thisEnd.eq(other.startPoint)) {
newParts = this.parts.concat(other.parts);
}
else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
return other.union(this, { allowReverse: false });
}
else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
return this.union(other.reversed(), { allowReverse: false });
}
else {
newParts = [
...this.parts,
{
kind: PathCommandType.MoveTo,
point: other.startPoint,
},
...other.parts,
];
}
return new Path(this.startPoint, newParts);
}
/**
* @returns a version of this path with the direction reversed.
*
* Example:
* ```ts,runnable,console
* import {Path} from '@js-draw/math';
* console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
* ```
*/
reversed() {
const newStart = this.getEndPoint();
const newParts = [];
let lastPoint = this.startPoint;
for (const part of this.parts) {
switch (part.kind) {
case PathCommandType.LineTo:
case PathCommandType.MoveTo:
newParts.push({
kind: part.kind,
point: lastPoint,
});
lastPoint = part.point;
break;
case PathCommandType.CubicBezierTo:
newParts.push({
kind: part.kind,
controlPoint1: part.controlPoint2,
controlPoint2: part.controlPoint1,
endPoint: lastPoint,
});
lastPoint = part.endPoint;
break;
case PathCommandType.QuadraticBezierTo:
newParts.push({
kind: part.kind,
controlPoint: part.controlPoint,
endPoint: lastPoint,
});
lastPoint = part.endPoint;
break;
default:
{
const exhaustivenessCheck = part;
return exhaustivenessCheck;
}
}
}
newParts.reverse();
return new Path(newStart, newParts);
}
getEndPoint() {

@@ -528,2 +640,48 @@ if (this.parts.length === 0) {

}
/** @returns true if all points on this are equivalent to the points on `other` */
eq(other, tolerance) {
if (other.parts.length !== this.parts.length) {
return false;
}
for (let i = 0; i < this.parts.length; i++) {
const part1 = this.parts[i];
const part2 = other.parts[i];
switch (part1.kind) {
case PathCommandType.LineTo:
case PathCommandType.MoveTo:
if (part1.kind !== part2.kind) {
return false;
}
else if (!part1.point.eq(part2.point, tolerance)) {
return false;
}
break;
case PathCommandType.CubicBezierTo:
if (part1.kind !== part2.kind) {
return false;
}
else if (!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
|| !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
|| !part1.endPoint.eq(part2.endPoint, tolerance)) {
return false;
}
break;
case PathCommandType.QuadraticBezierTo:
if (part1.kind !== part2.kind) {
return false;
}
else if (!part1.controlPoint.eq(part2.controlPoint, tolerance)
|| !part1.endPoint.eq(part2.endPoint, tolerance)) {
return false;
}
break;
default:
{
const exhaustivenessCheck = part1;
return exhaustivenessCheck;
}
}
}
return true;
}
/**

@@ -530,0 +688,0 @@ * Returns a path that outlines `rect`.

import { Point2 } from '../Vec2';
import Vec3 from '../Vec3';
import Abstract2DShape from './Abstract2DShape';
import LineSegment2 from './LineSegment2';
import Parameterized2DShape from './Parameterized2DShape';
import Rect2 from './Rect2';

@@ -11,9 +11,20 @@ /**

*/
declare class PointShape2D extends Abstract2DShape {
declare class PointShape2D extends Parameterized2DShape {
readonly p: Point2;
constructor(p: Point2);
signedDistance(point: Vec3): number;
intersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): Vec3[];
argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[];
getTightBoundingBox(): Rect2;
at(_t: number): Vec3;
/**
* Returns an arbitrary unit-length vector.
*/
normalAt(_t: number): Vec3;
tangentAt(_t: number): Vec3;
splitAt(_t: number): [PointShape2D];
nearestPointTo(_point: Point2): {
point: Vec3;
parameterValue: number;
};
}
export default PointShape2D;

@@ -6,3 +6,4 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
const Abstract2DShape_1 = __importDefault(require("./Abstract2DShape"));
const Vec2_1 = require("../Vec2");
const Parameterized2DShape_1 = __importDefault(require("./Parameterized2DShape"));
const Rect2_1 = __importDefault(require("./Rect2"));

@@ -14,3 +15,3 @@ /**

*/
class PointShape2D extends Abstract2DShape_1.default {
class PointShape2D extends Parameterized2DShape_1.default {
constructor(p) {

@@ -21,7 +22,7 @@ super();

signedDistance(point) {
return this.p.minus(point).magnitude();
return this.p.distanceTo(point);
}
intersectsLineSegment(lineSegment, epsilon) {
argIntersectsLineSegment(lineSegment, epsilon) {
if (lineSegment.containsPoint(this.p, epsilon)) {
return [this.p];
return [0];
}

@@ -33,3 +34,25 @@ return [];

}
at(_t) {
return this.p;
}
/**
* Returns an arbitrary unit-length vector.
*/
normalAt(_t) {
// Return a vector that makes sense.
return Vec2_1.Vec2.unitY;
}
tangentAt(_t) {
return Vec2_1.Vec2.unitX;
}
splitAt(_t) {
return [this];
}
nearestPointTo(_point) {
return {
point: this.p,
parameterValue: 0,
};
}
}
exports.default = PointShape2D;

@@ -21,7 +21,11 @@ import { Point2, Vec2 } from '../Vec2';

private static derivativeComponentAt;
private static secondDerivativeComponentAt;
/**
* @returns the curve evaluated at `t`.
*
* `t` should be a number in `[0, 1]`.
*/
at(t: number): Point2;
derivativeAt(t: number): Point2;
secondDerivativeAt(t: number): Point2;
normal(t: number): Vec2;

@@ -28,0 +32,0 @@ /** @returns an overestimate of this shape's bounding box. */

@@ -34,6 +34,15 @@ "use strict";

}
static secondDerivativeComponentAt(t, p0, p1, p2) {
return 2 * (p0 - 2 * p1 + p2);
}
/**
* @returns the curve evaluated at `t`.
*
* `t` should be a number in `[0, 1]`.
*/
at(t) {
if (t === 0)
return this.p0;
if (t === 1)
return this.p2;
const p0 = this.p0;

@@ -50,2 +59,8 @@ const p1 = this.p1;

}
secondDerivativeAt(t) {
const p0 = this.p0;
const p1 = this.p1;
const p2 = this.p2;
return Vec2_1.Vec2.of(QuadraticBezier.secondDerivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.secondDerivativeComponentAt(t, p0.y, p1.y, p2.y));
}
normal(t) {

@@ -111,6 +126,6 @@ const tangent = this.derivativeAt(t);

const at2 = this.at(min2);
const sqrDist1 = at1.minus(point).magnitudeSquared();
const sqrDist2 = at2.minus(point).magnitudeSquared();
const sqrDist3 = this.at(0).minus(point).magnitudeSquared();
const sqrDist4 = this.at(1).minus(point).magnitudeSquared();
const sqrDist1 = at1.squareDistanceTo(point);
const sqrDist2 = at2.squareDistanceTo(point);
const sqrDist3 = this.at(0).squareDistanceTo(point);
const sqrDist4 = this.at(1).squareDistanceTo(point);
return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));

@@ -117,0 +132,0 @@ }

@@ -28,2 +28,5 @@ import LineSegment2 from './LineSegment2';

containsRect(other: Rect2): boolean;
/**
* @returns true iff this and `other` overlap
*/
intersects(other: Rect2): boolean;

@@ -30,0 +33,0 @@ intersection(other: Rect2): Rect2 | null;

@@ -47,2 +47,5 @@ "use strict";

}
/**
* @returns true iff this and `other` overlap
*/
intersects(other) {

@@ -134,3 +137,3 @@ // Project along x/y axes.

for (const point of closestEdgePoints) {
const dist = point.minus(target).length();
const dist = point.distanceTo(target);
if (closestDist === null || dist < closestDist) {

@@ -137,0 +140,0 @@ closest = point;

@@ -39,2 +39,16 @@ /**

/**
* Interpreting this vector as a point in ℝ^3, computes the square distance
* to another point, `p`.
*
* Equivalent to `.minus(p).magnitudeSquared()`.
*/
squareDistanceTo(p: Vec3): number;
/**
* Interpreting this vector as a point in ℝ³, returns the distance to the point
* `p`.
*
* Equivalent to `.minus(p).magnitude()`.
*/
distanceTo(p: Vec3): number;
/**
* Returns the entry of this with the greatest magnitude.

@@ -44,2 +58,8 @@ *

* all entries of this vector.
*
* **Example**:
* ```ts,runnable,console
* import { Vec3 } from '@js-draw/math';
* console.log(Vec3.of(-1, -10, 8).maximumEntryMagnitude()); // -> 10
* ```
*/

@@ -55,2 +75,3 @@ maximumEntryMagnitude(): number;

*
* **Example**:
* ```ts,runnable,console

@@ -57,0 +78,0 @@ * import { Vec2 } from '@js-draw/math';

@@ -62,2 +62,23 @@ "use strict";

/**
* Interpreting this vector as a point in ℝ^3, computes the square distance
* to another point, `p`.
*
* Equivalent to `.minus(p).magnitudeSquared()`.
*/
squareDistanceTo(p) {
const dx = this.x - p.x;
const dy = this.y - p.y;
const dz = this.z - p.z;
return dx * dx + dy * dy + dz * dz;
}
/**
* Interpreting this vector as a point in ℝ³, returns the distance to the point
* `p`.
*
* Equivalent to `.minus(p).magnitude()`.
*/
distanceTo(p) {
return Math.sqrt(this.squareDistanceTo(p));
}
/**
* Returns the entry of this with the greatest magnitude.

@@ -67,2 +88,8 @@ *

* all entries of this vector.
*
* **Example**:
* ```ts,runnable,console
* import { Vec3 } from '@js-draw/math';
* console.log(Vec3.of(-1, -10, 8).maximumEntryMagnitude()); // -> 10
* ```
*/

@@ -80,2 +107,3 @@ maximumEntryMagnitude() {

*
* **Example**:
* ```ts,runnable,console

@@ -82,0 +110,0 @@ * import { Vec2 } from '@js-draw/math';

@@ -20,3 +20,3 @@ /**

export { LineSegment2 } from './shapes/LineSegment2';
export { Path, PathCommandType, PathCommand, LinePathCommand, MoveToPathCommand, QuadraticBezierPathCommand, CubicBezierPathCommand, } from './shapes/Path';
export { Path, IntersectionResult as PathIntersectionResult, CurveIndexRecord as PathCurveIndex, PathCommandType, PathCommand, LinePathCommand, MoveToPathCommand, QuadraticBezierPathCommand, CubicBezierPathCommand, } from './shapes/Path';
export { Rect2 } from './shapes/Rect2';

@@ -23,0 +23,0 @@ export { QuadraticBezier } from './shapes/QuadraticBezier';

@@ -41,2 +41,5 @@ import LineSegment2 from './LineSegment2';

* Returns a bounding box that precisely fits the content of this shape.
*
* **Note**: This bounding box should aligned with the x/y axes. (Thus, it may be
* possible to find a tighter bounding box not axes-aligned).
*/

@@ -43,0 +46,0 @@ abstract getTightBoundingBox(): Rect2;

import { Bezier } from 'bezier-js';
import { Point2, Vec2 } from '../Vec2';
import Abstract2DShape from './Abstract2DShape';
import LineSegment2 from './LineSegment2';
import Rect2 from './Rect2';
import Parameterized2DShape from './Parameterized2DShape';
/**

@@ -12,9 +12,10 @@ * A lazy-initializing wrapper around Bezier-js.

*
* Do not use this class directly. It may be removed/replaced in a future release.
* **Do not use this class directly.** It may be removed/replaced in a future release.
* @internal
*/
declare abstract class BezierJSWrapper extends Abstract2DShape {
export declare abstract class BezierJSWrapper extends Parameterized2DShape {
#private;
protected constructor(bezierJsBezier?: Bezier);
/** Returns the start, control points, and end point of this Bézier. */
abstract getPoints(): Point2[];
abstract getPoints(): readonly Point2[];
protected getBezier(): Bezier;

@@ -33,6 +34,15 @@ signedDistance(point: Point2): number;

derivativeAt(t: number): Point2;
secondDerivativeAt(t: number): Point2;
normal(t: number): Vec2;
normalAt(t: number): Vec2;
tangentAt(t: number): Vec2;
getTightBoundingBox(): Rect2;
intersectsLineSegment(line: LineSegment2): Point2[];
argIntersectsLineSegment(line: LineSegment2): number[];
splitAt(t: number): [BezierJSWrapper] | [BezierJSWrapper, BezierJSWrapper];
nearestPointTo(point: Point2): {
parameterValue: number;
point: import("../Vec3").Vec3;
};
toString(): string;
}
export default BezierJSWrapper;
import Mat33 from '../Mat33';
import Rect2 from './Rect2';
import { Vec2, Point2 } from '../Vec2';
import Abstract2DShape from './Abstract2DShape';
import Parameterized2DShape from './Parameterized2DShape';
import Vec3 from '../Vec3';
interface IntersectionResult {

@@ -10,3 +11,3 @@ point: Point2;

/** Represents a line segment. A `LineSegment2` is immutable. */
export declare class LineSegment2 extends Abstract2DShape {
export declare class LineSegment2 extends Parameterized2DShape {
private readonly point1;

@@ -32,4 +33,5 @@ private readonly point2;

get p2(): Point2;
get center(): Point2;
/**
* Gets a point a distance `t` along this line.
* Gets a point a **distance** `t` along this line.
*

@@ -47,4 +49,16 @@ * @deprecated

at(t: number): Point2;
normalAt(_t: number): Vec2;
tangentAt(_t: number): Vec3;
splitAt(t: number): [LineSegment2] | [LineSegment2, LineSegment2];
/**
* Returns the intersection of this with another line segment.
*
* **WARNING**: The parameter value returned by this method does not range from 0 to 1 and
* is currently a length.
* This will change in a future release.
* @deprecated
*/
intersection(other: LineSegment2): IntersectionResult | null;
intersects(other: LineSegment2): boolean;
argIntersectsLineSegment(lineSegment: LineSegment2): number[];
/**

@@ -58,4 +72,8 @@ * Returns the points at which this line segment intersects the

*/
intersectsLineSegment(lineSegment: LineSegment2): import("../Vec3").Vec3[];
closestPointTo(target: Point2): import("../Vec3").Vec3;
intersectsLineSegment(lineSegment: LineSegment2): Vec3[];
closestPointTo(target: Point2): Vec3;
nearestPointTo(target: Vec3): {
point: Vec3;
parameterValue: number;
};
/**

@@ -73,3 +91,14 @@ * Returns the distance from this line segment to `target`.

toString(): string;
/**
* Returns `true` iff this is equivalent to `other`.
*
* **Options**:
* - `tolerance`: The maximum difference between endpoints. (Default: 0)
* - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
*/
eq(other: LineSegment2, options?: {
tolerance?: number;
ignoreDirection?: boolean;
}): boolean;
}
export default LineSegment2;

@@ -5,3 +5,3 @@ import LineSegment2 from './LineSegment2';

import { Point2 } from '../Vec2';
import Abstract2DShape from './Abstract2DShape';
import Parameterized2DShape from './Parameterized2DShape';
export declare enum PathCommandType {

@@ -33,9 +33,20 @@ LineTo = 0,

export type PathCommand = CubicBezierPathCommand | QuadraticBezierPathCommand | MoveToPathCommand | LinePathCommand;
interface IntersectionResult {
curve: Abstract2DShape;
/** @internal @deprecated */
export interface IntersectionResult {
curve: Parameterized2DShape;
curveIndex: number;
/** Parameter value for the closest point **on** the path to the intersection. @internal @deprecated */
parameterValue?: number;
/** Point at which the intersection occured. */
point: Point2;
}
/**
* Allows indexing a particular part of a path.
*
* @see {@link Path.at} {@link Path.tangentAt}
*/
export interface CurveIndexRecord {
curveIndex: number;
parameterValue: number;
}
/**
* Represents a union of lines and curves.

@@ -62,3 +73,3 @@ */

private cachedGeometry;
get geometry(): Abstract2DShape[];
get geometry(): Parameterized2DShape[];
/**

@@ -92,6 +103,27 @@ * Iterates through the start/end points of each component in this path.

intersection(line: LineSegment2, strokeRadius?: number): IntersectionResult[];
/**
* @returns the nearest point on this path to the given `point`.
*
* @internal
* @beta
*/
nearestPointTo(point: Point2): IntersectionResult;
at(index: CurveIndexRecord): import("../Vec3").Vec3;
tangentAt(index: CurveIndexRecord): import("../Vec3").Vec3;
private static mapPathCommand;
mapPoints(mapping: (point: Point2) => Point2): Path;
transformedBy(affineTransfm: Mat33): Path;
union(other: Path | null): Path;
union(other: Path | null, options?: {
allowReverse?: boolean;
}): Path;
/**
* @returns a version of this path with the direction reversed.
*
* Example:
* ```ts,runnable,console
* import {Path} from '@js-draw/math';
* console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
* ```
*/
reversed(): Path;
private getEndPoint;

@@ -110,2 +142,4 @@ /**

closedRoughlyIntersects(rect: Rect2): boolean;
/** @returns true if all points on this are equivalent to the points on `other` */
eq(other: Path, tolerance?: number): boolean;
/**

@@ -112,0 +146,0 @@ * Returns a path that outlines `rect`.

import { Point2 } from '../Vec2';
import Vec3 from '../Vec3';
import Abstract2DShape from './Abstract2DShape';
import LineSegment2 from './LineSegment2';
import Parameterized2DShape from './Parameterized2DShape';
import Rect2 from './Rect2';

@@ -11,9 +11,20 @@ /**

*/
declare class PointShape2D extends Abstract2DShape {
declare class PointShape2D extends Parameterized2DShape {
readonly p: Point2;
constructor(p: Point2);
signedDistance(point: Vec3): number;
intersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): Vec3[];
argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[];
getTightBoundingBox(): Rect2;
at(_t: number): Vec3;
/**
* Returns an arbitrary unit-length vector.
*/
normalAt(_t: number): Vec3;
tangentAt(_t: number): Vec3;
splitAt(_t: number): [PointShape2D];
nearestPointTo(_point: Point2): {
point: Vec3;
parameterValue: number;
};
}
export default PointShape2D;

@@ -21,7 +21,11 @@ import { Point2, Vec2 } from '../Vec2';

private static derivativeComponentAt;
private static secondDerivativeComponentAt;
/**
* @returns the curve evaluated at `t`.
*
* `t` should be a number in `[0, 1]`.
*/
at(t: number): Point2;
derivativeAt(t: number): Point2;
secondDerivativeAt(t: number): Point2;
normal(t: number): Vec2;

@@ -28,0 +32,0 @@ /** @returns an overestimate of this shape's bounding box. */

@@ -28,2 +28,5 @@ import LineSegment2 from './LineSegment2';

containsRect(other: Rect2): boolean;
/**
* @returns true iff this and `other` overlap
*/
intersects(other: Rect2): boolean;

@@ -30,0 +33,0 @@ intersection(other: Rect2): Rect2 | null;

@@ -39,2 +39,16 @@ /**

/**
* Interpreting this vector as a point in ℝ^3, computes the square distance
* to another point, `p`.
*
* Equivalent to `.minus(p).magnitudeSquared()`.
*/
squareDistanceTo(p: Vec3): number;
/**
* Interpreting this vector as a point in ℝ³, returns the distance to the point
* `p`.
*
* Equivalent to `.minus(p).magnitude()`.
*/
distanceTo(p: Vec3): number;
/**
* Returns the entry of this with the greatest magnitude.

@@ -44,2 +58,8 @@ *

* all entries of this vector.
*
* **Example**:
* ```ts,runnable,console
* import { Vec3 } from '@js-draw/math';
* console.log(Vec3.of(-1, -10, 8).maximumEntryMagnitude()); // -> 10
* ```
*/

@@ -55,2 +75,3 @@ maximumEntryMagnitude(): number;

*
* **Example**:
* ```ts,runnable,console

@@ -57,0 +78,0 @@ * import { Vec2 } from '@js-draw/math';

{
"name": "@js-draw/math",
"version": "1.16.0",
"version": "1.17.0",
"description": "A math library for js-draw. ",

@@ -24,4 +24,4 @@ "types": "./dist/mjs/lib.d.ts",

"dist": "npm run build && npm run dist-test",
"build": "rm -rf ./dist && mkdir dist && build-tool build",
"watch": "rm -rf ./dist/* && mkdir -p dist && build-tool watch"
"build": "rm -rf ./dist && build-tool build",
"watch": "build-tool watch"
},

@@ -32,3 +32,3 @@ "dependencies": {

"devDependencies": {
"@js-draw/build-tool": "^1.11.1",
"@js-draw/build-tool": "^1.17.0",
"@types/bezier-js": "4.1.0",

@@ -50,3 +50,3 @@ "@types/jest": "29.5.5",

],
"gitHead": "b0b6d7165d76582e1c197d0f56a10bfe6b46e2bc"
"gitHead": "d0eff585750ab5670af3acda8ddff090e8825bd3"
}

@@ -24,2 +24,4 @@ /**

IntersectionResult as PathIntersectionResult,
CurveIndexRecord as PathCurveIndex,
PathCommandType,

@@ -26,0 +28,0 @@ PathCommand,

@@ -52,2 +52,5 @@ import LineSegment2 from './LineSegment2';

* Returns a bounding box that precisely fits the content of this shape.
*
* **Note**: This bounding box should aligned with the x/y axes. (Thus, it may be
* possible to find a tighter bounding box not axes-aligned).
*/

@@ -54,0 +57,0 @@ public abstract getTightBoundingBox(): Rect2;

import { Bezier } from 'bezier-js';
import { Point2, Vec2 } from '../Vec2';
import Abstract2DShape from './Abstract2DShape';
import LineSegment2 from './LineSegment2';
import Rect2 from './Rect2';
import Parameterized2DShape from './Parameterized2DShape';

@@ -13,10 +13,20 @@ /**

*
* Do not use this class directly. It may be removed/replaced in a future release.
* **Do not use this class directly.** It may be removed/replaced in a future release.
* @internal
*/
abstract class BezierJSWrapper extends Abstract2DShape {
export abstract class BezierJSWrapper extends Parameterized2DShape {
#bezierJs: Bezier|null = null;
protected constructor(
bezierJsBezier?: Bezier
) {
super();
if (bezierJsBezier) {
this.#bezierJs = bezierJsBezier;
}
}
/** Returns the start, control points, and end point of this Bézier. */
public abstract getPoints(): Point2[];
public abstract getPoints(): readonly Point2[];

@@ -32,3 +42,3 @@ protected getBezier() {

// .d: Distance
return this.getBezier().project(point.xy).d!;
return this.nearestPointTo(point).point.distanceTo(point);
}

@@ -49,3 +59,3 @@

*/
public at(t: number): Point2 {
public override at(t: number): Point2 {
return Vec2.ofXY(this.getBezier().get(t));

@@ -58,2 +68,6 @@ }

public secondDerivativeAt(t: number): Point2 {
return Vec2.ofXY((this.getBezier() as any).dderivative(t));
}
public normal(t: number): Vec2 {

@@ -63,2 +77,10 @@ return Vec2.ofXY(this.getBezier().normal(t));

public override normalAt(t: number): Vec2 {
return this.normal(t);
}
public override tangentAt(t: number): Vec2 {
return this.derivativeAt(t).normalized();
}
public override getTightBoundingBox(): Rect2 {

@@ -72,6 +94,6 @@ const bbox = this.getBezier().bbox();

public override intersectsLineSegment(line: LineSegment2): Point2[] {
public override argIntersectsLineSegment(line: LineSegment2): number[] {
const bezier = this.getBezier();
const intersectionPoints = bezier.intersects(line).map(t => {
return bezier.intersects(line).map(t => {
// We're using the .intersects(line) function, which is documented

@@ -84,17 +106,135 @@ // to always return numbers. However, to satisfy the type checker (and

const point = Vec2.ofXY(bezier.get(t));
const point = Vec2.ofXY(this.at(t));
// Ensure that the intersection is on the line segment
if (point.minus(line.p1).magnitude() > line.length
|| point.minus(line.p2).magnitude() > line.length) {
if (point.distanceTo(line.p1) > line.length
|| point.distanceTo(line.p2) > line.length) {
return null;
}
return point;
}).filter(entry => entry !== null) as Point2[];
return t;
}).filter(entry => entry !== null) as number[];
}
return intersectionPoints;
public override splitAt(t: number): [BezierJSWrapper] | [BezierJSWrapper, BezierJSWrapper] {
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.ofXY(point)), split.left),
new BezierJSWrapperImpl(split.right.points.map(point => Vec2.ofXY(point)), split.right),
];
}
public override nearestPointTo(point: Point2) {
// 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: number) => 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: number) => {
// 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: number) => {
// 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);
// 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) };
}
public override toString() {
return `Bézier(${this.getPoints().map(point => point.toString()).join(', ')})`;
}
}
/**
* Private concrete implementation of `BezierJSWrapper`, used by methods above that need to return a wrapper
* around a `Bezier`.
*/
class BezierJSWrapperImpl extends BezierJSWrapper {
public constructor(private controlPoints: readonly Point2[], curve?: Bezier) {
super(curve);
}
public override getPoints() {
return this.controlPoints;
}
}
export default BezierJSWrapper;

@@ -31,3 +31,3 @@ import LineSegment2 from './LineSegment2';

// t=10 implies 10 units along he line from (10, 10) to (-10, 10)
// t=10 implies 10 units along the line from (10, 10) to (-10, 10)
expect(line1.intersection(line2)?.t).toBe(10);

@@ -100,2 +100,36 @@

});
it.each([
{ from: Vec2.of(0, 0), to: Vec2.of(2, 2) },
{ from: Vec2.of(100, 0), to: Vec2.of(2, 2) },
])('should be able to split a line segment between %j', ({ from, to }) => {
const midpoint = from.lerp(to, 0.5);
const lineSegment = new LineSegment2(from, to);
// Halving
//
expect(lineSegment.at(0.5)).objEq(midpoint);
const [ firstHalf, secondHalf ] = lineSegment.splitAt(0.5);
if (!secondHalf) {
throw new Error('Splitting a line segment in half should yield two line segments.');
}
expect(firstHalf.p2).objEq(midpoint);
expect(firstHalf.p1).objEq(from);
expect(secondHalf.p2).objEq(to);
expect(secondHalf.p1).objEq(midpoint);
// Before start/end
expect(lineSegment.splitAt(0)[0]).objEq(lineSegment);
expect(lineSegment.splitAt(0)).toHaveLength(1);
expect(lineSegment.splitAt(1)).toHaveLength(1);
expect(lineSegment.splitAt(2)).toHaveLength(1);
});
it('equivalence check should allow ignoring direction', () => {
expect(new LineSegment2(Vec2.zero, Vec2.unitX)).objEq(new LineSegment2(Vec2.zero, Vec2.unitX));
expect(new LineSegment2(Vec2.zero, Vec2.unitX)).objEq(new LineSegment2(Vec2.unitX, Vec2.zero));
expect(new LineSegment2(Vec2.zero, Vec2.unitX)).not.objEq(new LineSegment2(Vec2.unitX, Vec2.zero), { ignoreDirection: false });
});
});
import Mat33 from '../Mat33';
import Rect2 from './Rect2';
import { Vec2, Point2 } from '../Vec2';
import Abstract2DShape from './Abstract2DShape';
import Parameterized2DShape from './Parameterized2DShape';
import Vec3 from '../Vec3';

@@ -12,3 +13,3 @@ interface IntersectionResult {

/** Represents a line segment. A `LineSegment2` is immutable. */
export class LineSegment2 extends Abstract2DShape {
export class LineSegment2 extends Parameterized2DShape {
// invariant: ||direction|| = 1

@@ -62,4 +63,8 @@

public get center(): Point2 {
return this.point1.lerp(this.point2, 0.5);
}
/**
* Gets a point a distance `t` along this line.
* Gets a point a **distance** `t` along this line.
*

@@ -79,7 +84,36 @@ * @deprecated

*/
public at(t: number): Point2 {
public override at(t: number): Point2 {
return this.get(t * this.length);
}
public override normalAt(_t: number): Vec2 {
return this.direction.orthog();
}
public override tangentAt(_t: number): Vec3 {
return this.direction;
}
public splitAt(t: number): [LineSegment2]|[LineSegment2,LineSegment2] {
if (t <= 0 || t >= 1) {
return [this];
}
return [
new LineSegment2(this.point1, this.at(t)),
new LineSegment2(this.at(t), this.point2),
];
}
/**
* Returns the intersection of this with another line segment.
*
* **WARNING**: The parameter value returned by this method does not range from 0 to 1 and
* is currently a length.
* This will change in a future release.
* @deprecated
*/
public intersection(other: LineSegment2): IntersectionResult|null {
// TODO(v2.0.0): Make this return a `t` value from `0` to `1`.
// We want x₁(t) = x₂(t) and y₁(t) = y₂(t)

@@ -152,6 +186,6 @@ // Observe that

// Ensure the result is in this/the other segment.
const resultToP1 = resultPoint.minus(this.point1).magnitude();
const resultToP2 = resultPoint.minus(this.point2).magnitude();
const resultToP3 = resultPoint.minus(other.point1).magnitude();
const resultToP4 = resultPoint.minus(other.point2).magnitude();
const resultToP1 = resultPoint.distanceTo(this.point1);
const resultToP2 = resultPoint.distanceTo(this.point2);
const resultToP3 = resultPoint.distanceTo(other.point1);
const resultToP4 = resultPoint.distanceTo(other.point2);
if (resultToP1 > this.length

@@ -174,2 +208,11 @@ || resultToP2 > this.length

public override argIntersectsLineSegment(lineSegment: LineSegment2) {
const intersection = this.intersection(lineSegment);
if (intersection) {
return [ intersection.t / this.length ];
}
return [];
}
/**

@@ -194,2 +237,6 @@ * Returns the points at which this line segment intersects the

public closestPointTo(target: Point2) {
return this.nearestPointTo(target).point;
}
public override nearestPointTo(target: Vec3): { point: Vec3; parameterValue: number; } {
// Distance from P1 along this' direction.

@@ -202,9 +249,9 @@ const projectedDistFromP1 = target.minus(this.p1).dot(this.direction);

if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) {
return projection;
return { point: projection, parameterValue: projectedDistFromP1 / this.length };
}
if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) {
return this.p2;
return { point: this.p2, parameterValue: 1 };
} else {
return this.p1;
return { point: this.p1, parameterValue: 0 };
}

@@ -238,3 +285,24 @@ }

}
/**
* Returns `true` iff this is equivalent to `other`.
*
* **Options**:
* - `tolerance`: The maximum difference between endpoints. (Default: 0)
* - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
*/
public eq(other: LineSegment2, options?: { tolerance?: number, ignoreDirection?: boolean }) {
if (!(other instanceof LineSegment2)) {
return false;
}
const tolerance = options?.tolerance;
const ignoreDirection = options?.ignoreDirection ?? true;
return (
(other.p1.eq(this.p1, tolerance) && other.p2.eq(this.p2, tolerance))
|| (ignoreDirection && other.p1.eq(this.p2, tolerance) && other.p2.eq(this.p1, tolerance))
);
}
}
export default LineSegment2;

@@ -63,2 +63,20 @@ import LineSegment2 from './LineSegment2';

it.each([
[ 'm0,0 L1,1', 'M0,0 L1,1', true ],
[ 'm0,0 L1,1', 'M1,1 L0,0', false ],
[ 'm0,0 L1,1 Q2,3 4,5', 'M1,1 L0,0', false ],
[ 'm0,0 L1,1 Q2,3 4,5', 'M1,1 L0,0 Q2,3 4,5', false ],
[ 'm0,0 L1,1 Q2,3 4,5', 'M0,0 L1,1 Q2,3 4,5', true ],
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', true ],
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9Z', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', false ],
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9Z', false ],
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9.01', false ],
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5 6,7.01 8,9', false ],
[ 'm0,0 L1,1 Q2,3 4,5 C4,5 6,7 8,9', 'M0,0 L1,1 Q2,3 4,5 C4,5.01 6,7 8,9', false ],
])('.eq should check equality', (path1Str, path2Str, shouldEqual) => {
expect(Path.fromString(path1Str)).objEq(Path.fromString(path1Str));
expect(Path.fromString(path2Str)).objEq(Path.fromString(path2Str));
expect(Path.fromString(path1Str).eq(Path.fromString(path2Str))).toBe(shouldEqual);
});
describe('intersection', () => {

@@ -183,3 +201,3 @@ it('should give all intersections for a path made up of lines', () => {

it('should give all intersections for a Bézier stroked path', () => {
it('should correctly report intersections for a simple Bézier curve path', () => {
const lineStart = Vec2.zero;

@@ -201,3 +219,3 @@ const path = new Path(lineStart, [

);
expect(intersections.length).toBe(0);
expect(intersections).toHaveLength(0);

@@ -208,4 +226,27 @@ // Should be an intersection when exiting/entering the edge of the stroke

);
expect(intersections.length).toBe(1);
expect(intersections).toHaveLength(1);
});
it('should correctly report intersections near the cap of a line-like Bézier', () => {
const path = Path.fromString('M0,0Q14,0 27,0');
expect(
path.intersection(
new LineSegment2(Vec2.of(0, -100), Vec2.of(0, 100)),
10,
),
// Should have intersections, despite being at the cap of the Bézier
// curve.
).toHaveLength(2);
});
it.each([
[new LineSegment2(Vec2.of(43.5,-12.5), Vec2.of(40.5,24.5)), 0],
// TODO: The below case is failing. It seems to be a Bezier-js bug though...
// (The Bézier.js method returns an empty array).
//[new LineSegment2(Vec2.of(35.5,19.5), Vec2.of(38.5,-17.5)), 0],
])('should correctly report positive intersections with a line-like Bézier', (line, strokeRadius) => {
const bezier = Path.fromString('M0,0 Q50,0 100,0');
expect(bezier.intersection(line, strokeRadius).length).toBeGreaterThan(0);
});
});

@@ -313,2 +354,21 @@

});
it.each([
[ 'm0,0 L1,1', 'M1,1 L0,0' ],
[ 'm0,0 L1,1', 'M1,1 L0,0' ],
[ 'M0,0 L1,1 Q2,2 3,3', 'M3,3 Q2,2 1,1 L0,0' ],
[ 'M0,0 L1,1 Q4,2 5,3 C12,13 10,9 8,7', 'M8,7 C 10,9 12,13 5,3 Q 4,2 1,1 L 0,0' ],
])('.reversed should reverse paths', (original, expected) => {
expect(Path.fromString(original).reversed()).objEq(Path.fromString(expected));
expect(Path.fromString(expected).reversed()).objEq(Path.fromString(original));
expect(Path.fromString(original).reversed().reversed()).objEq(Path.fromString(original));
});
it.each([
[ 'm0,0 l1,0', Vec2.of(0, 0), Vec2.of(0, 0) ],
[ 'm0,0 l1,0', Vec2.of(0.5, 0), Vec2.of(0.5, 0) ],
[ 'm0,0 Q1,0 1,2', Vec2.of(1, 0), Vec2.of(0.6236, 0.299) ],
])('.nearestPointTo should return the closest point on a path to the given parameter (case %#)', (path, point, expectedClosest) => {
expect(Path.fromString(path).nearestPointTo(point).point).objEq(expectedClosest, 0.002);
});
});

@@ -5,3 +5,2 @@ import LineSegment2 from './LineSegment2';

import { Point2, Vec2 } from '../Vec2';
import Abstract2DShape from './Abstract2DShape';
import CubicBezier from './CubicBezier';

@@ -12,2 +11,3 @@ import QuadraticBezier from './QuadraticBezier';

import toStringOfSamePrecision from '../rounding/toStringOfSamePrecision';
import Parameterized2DShape from './Parameterized2DShape';

@@ -46,10 +46,12 @@ export enum PathCommandType {

interface IntersectionResult {
export interface IntersectionResult {
// @internal
curve: Abstract2DShape;
curve: Parameterized2DShape;
// @internal
curveIndex: number;
/** @internal @deprecated */
/** Parameter value for the closest point **on** the path to the intersection. @internal @deprecated */
parameterValue?: number;
// Point at which the intersection occured.
/** Point at which the intersection occured. */
point: Point2;

@@ -59,2 +61,12 @@ }

/**
* Allows indexing a particular part of a path.
*
* @see {@link Path.at} {@link Path.tangentAt}
*/
export interface CurveIndexRecord {
curveIndex: number;
parameterValue: number;
}
/**
* Represents a union of lines and curves.

@@ -104,6 +116,6 @@ */

private cachedGeometry: Abstract2DShape[]|null = null;
private cachedGeometry: Parameterized2DShape[]|null = null;
// Lazy-loads and returns this path's geometry
public get geometry(): Abstract2DShape[] {
public get geometry(): Parameterized2DShape[] {
if (this.cachedGeometry) {

@@ -114,3 +126,3 @@ return this.cachedGeometry;

let startPoint = this.startPoint;
const geometry: Abstract2DShape[] = [];
const geometry: Parameterized2DShape[] = [];

@@ -279,3 +291,3 @@ for (const part of this.parts) {

type DistanceFunctionRecord = {
part: Abstract2DShape,
part: Parameterized2DShape,
bbox: Rect2,

@@ -319,5 +331,5 @@ distFn: DistanceFunction,

// line could intersect are considered.
const sdf = (point: Point2): [Abstract2DShape|null, number] => {
const sdf = (point: Point2): [Parameterized2DShape|null, number] => {
let minDist = Infinity;
let minDistPart: Abstract2DShape|null = null;
let minDistPart: Parameterized2DShape|null = null;

@@ -349,3 +361,3 @@ const uncheckedDistFunctions: DistanceFunctionRecord[] = [];

// the current minimum.
if (!bbox.grownBy(minDist).containsPoint(point)) {
if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
continue;

@@ -400,3 +412,3 @@ }

// Returns the maximum x value explored
// Returns the maximum parameter value explored
const raymarchFrom = (

@@ -459,5 +471,11 @@ startPoint: Point2,

point: currentPoint,
parameterValue: NaN,
parameterValue: NaN,// lastPart.nearestPointTo(currentPoint).parameterValue,
curve: lastPart,
curveIndex: this.geometry.indexOf(lastPart),
});
// Slightly increase the parameter value to prevent the same point from being
// added to the results twice.
const parameterIncrease = strokeRadius / 20 / line.length;
lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
}

@@ -503,11 +521,16 @@

let index = 0;
for (const part of this.geometry) {
const intersection = part.intersectsLineSegment(line);
const intersections = part.argIntersectsLineSegment(line);
if (intersection.length > 0) {
for (const intersection of intersections) {
result.push({
curve: part,
point: intersection[0],
curveIndex: index,
point: part.at(intersection),
parameterValue: intersection,
});
}
index ++;
}

@@ -528,2 +551,43 @@

/**
* @returns the nearest point on this path to the given `point`.
*
* @internal
* @beta
*/
public nearestPointTo(point: Point2): IntersectionResult {
// Find the closest point on this
let closestSquareDist = Infinity;
let closestPartIndex = 0;
let closestParameterValue = 0;
let closestPoint: Point2 = this.startPoint;
for (let i = 0; i < this.geometry.length; i++) {
const current = this.geometry[i];
const nearestPoint = current.nearestPointTo(point);
const sqareDist = nearestPoint.point.squareDistanceTo(point);
if (i === 0 || sqareDist < closestSquareDist) {
closestPartIndex = i;
closestSquareDist = sqareDist;
closestParameterValue = nearestPoint.parameterValue;
closestPoint = nearestPoint.point;
}
}
return {
curve: this.geometry[closestPartIndex],
curveIndex: closestPartIndex,
parameterValue: closestParameterValue,
point: closestPoint,
};
}
public at(index: CurveIndexRecord) {
return this.geometry[index.curveIndex].at(index.parameterValue);
}
public tangentAt(index: CurveIndexRecord) {
return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
}
private static mapPathCommand(part: PathCommand, mapping: (point: Point2)=> Point2): PathCommand {

@@ -579,3 +643,9 @@ switch (part.kind) {

// Creates a new path by joining [other] to the end of this path
public union(other: Path|null): Path {
public union(
other: Path|null,
// allowReverse: true iff reversing other or this is permitted if it means
// no moveTo command is necessary when unioning the paths.
options: { allowReverse?: boolean } = { allowReverse: true },
): Path {
if (!other) {

@@ -585,10 +655,73 @@ return this;

return new Path(this.startPoint, [
...this.parts,
const thisEnd = this.getEndPoint();
let newParts: Readonly<PathCommand>[] = [];
if (thisEnd.eq(other.startPoint)) {
newParts = this.parts.concat(other.parts);
} else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
return other.union(this, { allowReverse: false });
} else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
return this.union(other.reversed(), { allowReverse: false });
} else {
newParts = [
...this.parts,
{
kind: PathCommandType.MoveTo,
point: other.startPoint,
},
...other.parts,
];
}
return new Path(this.startPoint, newParts);
}
/**
* @returns a version of this path with the direction reversed.
*
* Example:
* ```ts,runnable,console
* import {Path} from '@js-draw/math';
* console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
* ```
*/
public reversed() {
const newStart = this.getEndPoint();
const newParts: Readonly<PathCommand>[] = [];
let lastPoint: Point2 = this.startPoint;
for (const part of this.parts) {
switch (part.kind) {
case PathCommandType.LineTo:
case PathCommandType.MoveTo:
newParts.push({
kind: part.kind,
point: lastPoint,
});
lastPoint = part.point;
break;
case PathCommandType.CubicBezierTo:
newParts.push({
kind: part.kind,
controlPoint1: part.controlPoint2,
controlPoint2: part.controlPoint1,
endPoint: lastPoint,
});
lastPoint = part.endPoint;
break;
case PathCommandType.QuadraticBezierTo:
newParts.push({
kind: part.kind,
controlPoint: part.controlPoint,
endPoint: lastPoint,
});
lastPoint = part.endPoint;
break;
default:
{
kind: PathCommandType.MoveTo,
point: other.startPoint,
},
...other.parts,
]);
const exhaustivenessCheck: never = part;
return exhaustivenessCheck;
}
}
}
newParts.reverse();
return new Path(newStart, newParts);
}

@@ -700,2 +833,53 @@

/** @returns true if all points on this are equivalent to the points on `other` */
public eq(other: Path, tolerance?: number) {
if (other.parts.length !== this.parts.length) {
return false;
}
for (let i = 0; i < this.parts.length; i++) {
const part1 = this.parts[i];
const part2 = other.parts[i];
switch (part1.kind) {
case PathCommandType.LineTo:
case PathCommandType.MoveTo:
if (part1.kind !== part2.kind) {
return false;
} else if(!part1.point.eq(part2.point, tolerance)) {
return false;
}
break;
case PathCommandType.CubicBezierTo:
if (part1.kind !== part2.kind) {
return false;
} else if (
!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
|| !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
|| !part1.endPoint.eq(part2.endPoint, tolerance)
) {
return false;
}
break;
case PathCommandType.QuadraticBezierTo:
if (part1.kind !== part2.kind) {
return false;
} else if (
!part1.controlPoint.eq(part2.controlPoint, tolerance)
|| !part1.endPoint.eq(part2.endPoint, tolerance)
) {
return false;
}
break;
default:
{
const exhaustivenessCheck: never = part1;
return exhaustivenessCheck;
}
}
}
return true;
}
/**

@@ -702,0 +886,0 @@ * Returns a path that outlines `rect`.

@@ -1,5 +0,5 @@

import { Point2 } from '../Vec2';
import { Point2, Vec2 } from '../Vec2';
import Vec3 from '../Vec3';
import Abstract2DShape from './Abstract2DShape';
import LineSegment2 from './LineSegment2';
import Parameterized2DShape from './Parameterized2DShape';
import Rect2 from './Rect2';

@@ -12,3 +12,3 @@

*/
class PointShape2D extends Abstract2DShape {
class PointShape2D extends Parameterized2DShape {
public constructor(public readonly p: Point2) {

@@ -19,8 +19,8 @@ super();

public override signedDistance(point: Vec3): number {
return this.p.minus(point).magnitude();
return this.p.distanceTo(point);
}
public override intersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): Vec3[] {
public override argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[] {
if (lineSegment.containsPoint(this.p, epsilon)) {
return [ this.p ];
return [ 0 ];
}

@@ -33,4 +33,31 @@ return [ ];

}
public override at(_t: number) {
return this.p;
}
/**
* Returns an arbitrary unit-length vector.
*/
public override normalAt(_t: number) {
// Return a vector that makes sense.
return Vec2.unitY;
}
public override tangentAt(_t: number): Vec3 {
return Vec2.unitX;
}
public override splitAt(_t: number): [PointShape2D] {
return [this];
}
public override nearestPointTo(_point: Point2) {
return {
point: this.p,
parameterValue: 0,
};
}
}
export default PointShape2D;

@@ -5,9 +5,8 @@ import { Vec2 } from '../Vec2';

describe('QuadraticBezier', () => {
it('approxmiateDistance should approximately return the distance to the curve', () => {
const curves = [
new QuadraticBezier(Vec2.zero, Vec2.of(10, 0), Vec2.of(20, 0)),
new QuadraticBezier(Vec2.of(-10, 0), Vec2.of(2, 10), Vec2.of(20, 0)),
new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(20, 60)),
new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(-20, 60)),
];
test.each([
new QuadraticBezier(Vec2.zero, Vec2.of(10, 0), Vec2.of(20, 0)),
new QuadraticBezier(Vec2.of(-10, 0), Vec2.of(2, 10), Vec2.of(20, 0)),
new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(20, 60)),
new QuadraticBezier(Vec2.of(0, 0), Vec2.of(4, -10), Vec2.of(-20, 60)),
])('approxmiateDistance should approximately return the distance to the curve (%s)', (curve) => {
const testPoints = [

@@ -22,9 +21,46 @@ Vec2.of(1, 1),

for (const point of testPoints) {
const actualDist = curve.distance(point);
const approxDist = curve.approximateDistance(point);
expect(approxDist).toBeGreaterThan(actualDist * 0.6 - 0.25);
expect(approxDist).toBeLessThan(actualDist * 1.5 + 2.6);
}
});
test.each([
[ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.zero, 0 ],
[ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitY, 1 ],
[ new QuadraticBezier(Vec2.zero, Vec2.of(0.5, 0), Vec2.of(1, 0)), Vec2.of(0.4, 0), 0.4],
[ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.of(0, 1)), Vec2.of(0, 0.4), 0.4],
[ new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY), Vec2.unitX, 0.42514 ],
// Should not return an out-of-range parameter
[ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, -1000), 0 ],
[ new QuadraticBezier(Vec2.zero, Vec2.of(0, 0.5), Vec2.unitY), Vec2.of(0, 1000), 1 ],
])('nearestPointTo should return the nearest point and parameter value on %s to %s', (bezier, point, expectedParameter) => {
const nearest = bezier.nearestPointTo(point);
expect(nearest.parameterValue).toBeCloseTo(expectedParameter, 0.0001);
expect(nearest.point).objEq(bezier.at(nearest.parameterValue));
});
test('.normalAt should return a unit normal vector at the given parameter value', () => {
const curves = [
new QuadraticBezier(Vec2.zero, Vec2.unitY, Vec2.unitY.times(2)),
new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY),
new QuadraticBezier(Vec2.zero, Vec2.unitX, Vec2.unitY.times(-2)),
new QuadraticBezier(Vec2.of(2, 3), Vec2.of(4, 5.1), Vec2.of(6, 7)),
new QuadraticBezier(Vec2.of(2, 3), Vec2.of(100, 1000), Vec2.unitY.times(-2)),
];
for (const curve of curves) {
for (const point of testPoints) {
const actualDist = curve.distance(point);
const approxDist = curve.approximateDistance(point);
for (let t = 0; t < 1; t += 0.1) {
const normal = curve.normalAt(t);
expect(normal.length()).toBe(1);
expect(approxDist).toBeGreaterThan(actualDist * 0.6 - 0.25);
expect(approxDist).toBeLessThan(actualDist * 1.5 + 2.6);
const tangentApprox = curve.at(t + 0.001).minus(curve.at(t - 0.001));
// The tangent vector should be perpindicular to the normal
expect(tangentApprox.dot(normal)).toBeCloseTo(0);
}

@@ -31,0 +67,0 @@ }

@@ -33,6 +33,15 @@ import { Point2, Vec2 } from '../Vec2';

private static secondDerivativeComponentAt(t: number, p0: number, p1: number, p2: number) {
return 2 * (p0 - 2 * p1 + p2);
}
/**
* @returns the curve evaluated at `t`.
*
* `t` should be a number in `[0, 1]`.
*/
public override at(t: number): Point2 {
if (t === 0) return this.p0;
if (t === 1) return this.p2;
const p0 = this.p0;

@@ -57,2 +66,12 @@ const p1 = this.p1;

public override secondDerivativeAt(t: number): Point2 {
const p0 = this.p0;
const p1 = this.p1;
const p2 = this.p2;
return Vec2.of(
QuadraticBezier.secondDerivativeComponentAt(t, p0.x, p1.x, p2.x),
QuadraticBezier.secondDerivativeComponentAt(t, p0.y, p1.y, p2.y),
);
}
public override normal(t: number): Vec2 {

@@ -131,8 +150,7 @@ const tangent = this.derivativeAt(t);

const at2 = this.at(min2);
const sqrDist1 = at1.minus(point).magnitudeSquared();
const sqrDist2 = at2.minus(point).magnitudeSquared();
const sqrDist3 = this.at(0).minus(point).magnitudeSquared();
const sqrDist4 = this.at(1).minus(point).magnitudeSquared();
const sqrDist1 = at1.squareDistanceTo(point);
const sqrDist2 = at2.squareDistanceTo(point);
const sqrDist3 = this.at(0).squareDistanceTo(point);
const sqrDist4 = this.at(1).squareDistanceTo(point);
return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));

@@ -139,0 +157,0 @@ }

@@ -70,2 +70,5 @@ import LineSegment2 from './LineSegment2';

/**
* @returns true iff this and `other` overlap
*/
public intersects(other: Rect2): boolean {

@@ -185,3 +188,3 @@ // Project along x/y axes.

for (const point of closestEdgePoints) {
const dist = point.minus(target).length();
const dist = point.distanceTo(target);
if (closestDist === null || dist < closestDist) {

@@ -188,0 +191,0 @@ closest = point;

@@ -5,3 +5,3 @@

describe('Vec3', () => {
it('.xy should contain the x and y components', () => {
test('.xy should contain the x and y components', () => {
const vec = Vec3.of(1, 2, 3);

@@ -14,3 +14,3 @@ expect(vec.xy).toMatchObject({

it('should be combinable with other vectors via .zip', () => {
test('should be combinable with other vectors via .zip', () => {
const vec1 = Vec3.unitX;

@@ -22,3 +22,3 @@ const vec2 = Vec3.unitY;

it('.cross should obey the right hand rule', () => {
test('.cross should obey the right hand rule', () => {
const vec1 = Vec3.unitX;

@@ -30,3 +30,3 @@ const vec2 = Vec3.unitY;

it('.orthog should return an orthogonal vector', () => {
test('.orthog should return an orthogonal vector', () => {
expect(Vec3.unitZ.orthog().dot(Vec3.unitZ)).toBe(0);

@@ -38,7 +38,7 @@

it('.minus should return the difference between two vectors', () => {
test('.minus should return the difference between two vectors', () => {
expect(Vec3.of(1, 2, 3).minus(Vec3.of(4, 5, 6))).objEq(Vec3.of(1 - 4, 2 - 5, 3 - 6));
});
it('.orthog should return a unit vector', () => {
test('.orthog should return a unit vector', () => {
expect(Vec3.zero.orthog().magnitude()).toBe(1);

@@ -50,3 +50,3 @@ expect(Vec3.unitZ.orthog().magnitude()).toBe(1);

it('.normalizedOrZero should normalize the given vector or return zero', () => {
test('.normalizedOrZero should normalize the given vector or return zero', () => {
expect(Vec3.zero.normalizedOrZero()).objEq(Vec3.zero);

@@ -57,2 +57,21 @@ expect(Vec3.unitX.normalizedOrZero()).objEq(Vec3.unitX);

});
test.each([
{ from: Vec3.of(1, 1, 1), to: Vec3.of(1, 2, 1), expected: 1 },
{ from: Vec3.of(1, 1, 1), to: Vec3.of(1, 2, 2), expected: 2 },
{ from: Vec3.of(1, 1, 1), to: Vec3.of(2, 2, 2), expected: 3 },
{ from: Vec3.of(1, 1, 1), to: Vec3.of(0, 1, 1), expected: 1 },
{ from: Vec3.of(1, 1, 1), to: Vec3.of(0, 1, 0), expected: 2 },
{ from: Vec3.of(1, 1, 1), to: Vec3.of(0, 0, 0), expected: 3 },
{ from: Vec3.of(-1, -10, 0), to: Vec3.of(1, 2, 0), expected: 148 },
])(
'.squareDistanceTo and .distanceTo should return correct square and euclidean distances (%j)',
({ from , to, expected }) => {
expect(from.squareDistanceTo(to)).toBe(expected);
expect(to.squareDistanceTo(from)).toBe(expected);
expect(to.distanceTo(from)).toBeCloseTo(Math.sqrt(expected));
expect(to.minus(from).magnitudeSquared()).toBe(expected);
expect(from.minus(to).magnitudeSquared()).toBe(expected);
},
);
});

@@ -67,2 +67,25 @@

/**
* Interpreting this vector as a point in ℝ^3, computes the square distance
* to another point, `p`.
*
* Equivalent to `.minus(p).magnitudeSquared()`.
*/
public squareDistanceTo(p: Vec3) {
const dx = this.x - p.x;
const dy = this.y - p.y;
const dz = this.z - p.z;
return dx * dx + dy * dy + dz * dz;
}
/**
* Interpreting this vector as a point in ℝ³, returns the distance to the point
* `p`.
*
* Equivalent to `.minus(p).magnitude()`.
*/
public distanceTo(p: Vec3) {
return Math.sqrt(this.squareDistanceTo(p));
}
/**
* Returns the entry of this with the greatest magnitude.

@@ -72,2 +95,8 @@ *

* all entries of this vector.
*
* **Example**:
* ```ts,runnable,console
* import { Vec3 } from '@js-draw/math';
* console.log(Vec3.of(-1, -10, 8).maximumEntryMagnitude()); // -> 10
* ```
*/

@@ -86,2 +115,3 @@ public maximumEntryMagnitude(): number {

*
* **Example**:
* ```ts,runnable,console

@@ -88,0 +118,0 @@ * import { Vec2 } from '@js-draw/math';

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc