@js-draw/math 1.16.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Vec3.d.ts +21 -0
 - package/dist/cjs/Vec3.js +28 -0
 - package/dist/cjs/lib.d.ts +1 -1
 - package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
 - package/dist/cjs/shapes/BezierJSWrapper.d.ts +15 -5
 - package/dist/cjs/shapes/BezierJSWrapper.js +135 -18
 - package/dist/cjs/shapes/LineSegment2.d.ts +34 -5
 - package/dist/cjs/shapes/LineSegment2.js +63 -10
 - package/dist/cjs/shapes/Parameterized2DShape.d.ts +31 -0
 - package/dist/cjs/shapes/Parameterized2DShape.js +15 -0
 - package/dist/cjs/shapes/Path.d.ts +40 -6
 - package/dist/cjs/shapes/Path.js +173 -15
 - package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
 - package/dist/cjs/shapes/PointShape2D.js +28 -5
 - package/dist/cjs/shapes/QuadraticBezier.d.ts +4 -0
 - package/dist/cjs/shapes/QuadraticBezier.js +19 -4
 - package/dist/cjs/shapes/Rect2.d.ts +3 -0
 - package/dist/cjs/shapes/Rect2.js +4 -1
 - package/dist/mjs/Vec3.d.ts +21 -0
 - package/dist/mjs/Vec3.mjs +28 -0
 - package/dist/mjs/lib.d.ts +1 -1
 - package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
 - package/dist/mjs/shapes/BezierJSWrapper.d.ts +15 -5
 - package/dist/mjs/shapes/BezierJSWrapper.mjs +133 -18
 - package/dist/mjs/shapes/LineSegment2.d.ts +34 -5
 - package/dist/mjs/shapes/LineSegment2.mjs +63 -10
 - package/dist/mjs/shapes/Parameterized2DShape.d.ts +31 -0
 - package/dist/mjs/shapes/Parameterized2DShape.mjs +8 -0
 - package/dist/mjs/shapes/Path.d.ts +40 -6
 - package/dist/mjs/shapes/Path.mjs +173 -15
 - package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
 - package/dist/mjs/shapes/PointShape2D.mjs +28 -5
 - package/dist/mjs/shapes/QuadraticBezier.d.ts +4 -0
 - package/dist/mjs/shapes/QuadraticBezier.mjs +19 -4
 - package/dist/mjs/shapes/Rect2.d.ts +3 -0
 - package/dist/mjs/shapes/Rect2.mjs +4 -1
 - package/package.json +5 -5
 - package/src/Vec3.test.ts +26 -7
 - package/src/Vec3.ts +30 -0
 - package/src/lib.ts +2 -0
 - package/src/shapes/Abstract2DShape.ts +3 -0
 - package/src/shapes/BezierJSWrapper.ts +154 -14
 - package/src/shapes/LineSegment2.test.ts +35 -1
 - package/src/shapes/LineSegment2.ts +79 -11
 - package/src/shapes/Parameterized2DShape.ts +39 -0
 - package/src/shapes/Path.test.ts +63 -3
 - package/src/shapes/Path.ts +209 -25
 - package/src/shapes/PointShape2D.ts +33 -6
 - package/src/shapes/QuadraticBezier.test.ts +48 -12
 - package/src/shapes/QuadraticBezier.ts +23 -5
 - package/src/shapes/Rect2.ts +4 -1
 
| 
         @@ -1,8 +1,8 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            import { Bezier } from 'bezier-js';
         
     | 
| 
       2 
2 
     | 
    
         
             
            import { Point2, Vec2 } from '../Vec2';
         
     | 
| 
       3 
     | 
    
         
            -
            import Abstract2DShape from './Abstract2DShape';
         
     | 
| 
       4 
3 
     | 
    
         
             
            import LineSegment2 from './LineSegment2';
         
     | 
| 
       5 
4 
     | 
    
         
             
            import Rect2 from './Rect2';
         
     | 
| 
      
 5 
     | 
    
         
            +
            import Parameterized2DShape from './Parameterized2DShape';
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
       7 
7 
     | 
    
         
             
            /**
         
     | 
| 
       8 
8 
     | 
    
         
             
             * A lazy-initializing wrapper around Bezier-js.
         
     | 
| 
         @@ -10,14 +10,24 @@ import Rect2 from './Rect2'; 
     | 
|
| 
       10 
10 
     | 
    
         
             
             * Subclasses may override `at`, `derivativeAt`, and `normal` with functions
         
     | 
| 
       11 
11 
     | 
    
         
             
             * that do not initialize a `bezier-js` `Bezier`.
         
     | 
| 
       12 
12 
     | 
    
         
             
             *
         
     | 
| 
       13 
     | 
    
         
            -
             * Do not use this class directly 
     | 
| 
      
 13 
     | 
    
         
            +
             * **Do not use this class directly.** It may be removed/replaced in a future release.
         
     | 
| 
       14 
14 
     | 
    
         
             
             * @internal
         
     | 
| 
       15 
15 
     | 
    
         
             
             */
         
     | 
| 
       16 
     | 
    
         
            -
            abstract class BezierJSWrapper extends  
     | 
| 
      
 16 
     | 
    
         
            +
            export abstract class BezierJSWrapper extends Parameterized2DShape {
         
     | 
| 
       17 
17 
     | 
    
         
             
            	#bezierJs: Bezier|null = null;
         
     | 
| 
       18 
18 
     | 
    
         | 
| 
      
 19 
     | 
    
         
            +
            	protected constructor(
         
     | 
| 
      
 20 
     | 
    
         
            +
            		bezierJsBezier?: Bezier
         
     | 
| 
      
 21 
     | 
    
         
            +
            	) {
         
     | 
| 
      
 22 
     | 
    
         
            +
            		super();
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            		if (bezierJsBezier) {
         
     | 
| 
      
 25 
     | 
    
         
            +
            			this.#bezierJs = bezierJsBezier;
         
     | 
| 
      
 26 
     | 
    
         
            +
            		}
         
     | 
| 
      
 27 
     | 
    
         
            +
            	}
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
       19 
29 
     | 
    
         
             
            	/** Returns the start, control points, and end point of this Bézier. */
         
     | 
| 
       20 
     | 
    
         
            -
            	public abstract getPoints(): Point2[];
         
     | 
| 
      
 30 
     | 
    
         
            +
            	public abstract getPoints(): readonly Point2[];
         
     | 
| 
       21 
31 
     | 
    
         | 
| 
       22 
32 
     | 
    
         
             
            	protected getBezier() {
         
     | 
| 
       23 
33 
     | 
    
         
             
            		if (!this.#bezierJs) {
         
     | 
| 
         @@ -28,7 +38,7 @@ abstract class BezierJSWrapper extends Abstract2DShape { 
     | 
|
| 
       28 
38 
     | 
    
         | 
| 
       29 
39 
     | 
    
         
             
            	public override signedDistance(point: Point2): number {
         
     | 
| 
       30 
40 
     | 
    
         
             
            		// .d: Distance
         
     | 
| 
       31 
     | 
    
         
            -
            		return this. 
     | 
| 
      
 41 
     | 
    
         
            +
            		return this.nearestPointTo(point).point.distanceTo(point);
         
     | 
| 
       32 
42 
     | 
    
         
             
            	}
         
     | 
| 
       33 
43 
     | 
    
         | 
| 
       34 
44 
     | 
    
         
             
            	/**
         
     | 
| 
         @@ -44,7 +54,7 @@ abstract class BezierJSWrapper extends Abstract2DShape { 
     | 
|
| 
       44 
54 
     | 
    
         
             
            	/**
         
     | 
| 
       45 
55 
     | 
    
         
             
            	 * @returns the curve evaluated at `t`.
         
     | 
| 
       46 
56 
     | 
    
         
             
            	 */
         
     | 
| 
       47 
     | 
    
         
            -
            	public at(t: number): Point2 {
         
     | 
| 
      
 57 
     | 
    
         
            +
            	public override at(t: number): Point2 {
         
     | 
| 
       48 
58 
     | 
    
         
             
            		return Vec2.ofXY(this.getBezier().get(t));
         
     | 
| 
       49 
59 
     | 
    
         
             
            	}
         
     | 
| 
       50 
60 
     | 
    
         | 
| 
         @@ -52,10 +62,22 @@ abstract class BezierJSWrapper extends Abstract2DShape { 
     | 
|
| 
       52 
62 
     | 
    
         
             
            		return Vec2.ofXY(this.getBezier().derivative(t));
         
     | 
| 
       53 
63 
     | 
    
         
             
            	}
         
     | 
| 
       54 
64 
     | 
    
         | 
| 
      
 65 
     | 
    
         
            +
            	public secondDerivativeAt(t: number): Point2 {
         
     | 
| 
      
 66 
     | 
    
         
            +
            		return Vec2.ofXY((this.getBezier() as any).dderivative(t));
         
     | 
| 
      
 67 
     | 
    
         
            +
            	}
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
       55 
69 
     | 
    
         
             
            	public normal(t: number): Vec2 {
         
     | 
| 
       56 
70 
     | 
    
         
             
            		return Vec2.ofXY(this.getBezier().normal(t));
         
     | 
| 
       57 
71 
     | 
    
         
             
            	}
         
     | 
| 
       58 
72 
     | 
    
         | 
| 
      
 73 
     | 
    
         
            +
            	public override normalAt(t: number): Vec2 {
         
     | 
| 
      
 74 
     | 
    
         
            +
            		return this.normal(t);
         
     | 
| 
      
 75 
     | 
    
         
            +
            	}
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
            	public override tangentAt(t: number): Vec2 {
         
     | 
| 
      
 78 
     | 
    
         
            +
            		return this.derivativeAt(t).normalized();
         
     | 
| 
      
 79 
     | 
    
         
            +
            	}
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
       59 
81 
     | 
    
         
             
            	public override getTightBoundingBox(): Rect2 {
         
     | 
| 
       60 
82 
     | 
    
         
             
            		const bbox = this.getBezier().bbox();
         
     | 
| 
       61 
83 
     | 
    
         
             
            		const width = bbox.x.max - bbox.x.min;
         
     | 
| 
         @@ -64,10 +86,10 @@ abstract class BezierJSWrapper extends Abstract2DShape { 
     | 
|
| 
       64 
86 
     | 
    
         
             
            		return new Rect2(bbox.x.min, bbox.y.min, width, height);
         
     | 
| 
       65 
87 
     | 
    
         
             
            	}
         
     | 
| 
       66 
88 
     | 
    
         | 
| 
       67 
     | 
    
         
            -
            	public override  
     | 
| 
      
 89 
     | 
    
         
            +
            	public override argIntersectsLineSegment(line: LineSegment2): number[] {
         
     | 
| 
       68 
90 
     | 
    
         
             
            		const bezier = this.getBezier();
         
     | 
| 
       69 
91 
     | 
    
         | 
| 
       70 
     | 
    
         
            -
            		 
     | 
| 
      
 92 
     | 
    
         
            +
            		return bezier.intersects(line).map(t => {
         
     | 
| 
       71 
93 
     | 
    
         
             
            			// We're using the .intersects(line) function, which is documented
         
     | 
| 
       72 
94 
     | 
    
         
             
            			// to always return numbers. However, to satisfy the type checker (and
         
     | 
| 
       73 
95 
     | 
    
         
             
            			// possibly improperly-defined types),
         
     | 
| 
         @@ -75,18 +97,136 @@ abstract class BezierJSWrapper extends Abstract2DShape { 
     | 
|
| 
       75 
97 
     | 
    
         
             
            				t = parseFloat(t);
         
     | 
| 
       76 
98 
     | 
    
         
             
            			}
         
     | 
| 
       77 
99 
     | 
    
         | 
| 
       78 
     | 
    
         
            -
            			const point = Vec2.ofXY( 
     | 
| 
      
 100 
     | 
    
         
            +
            			const point = Vec2.ofXY(this.at(t));
         
     | 
| 
       79 
101 
     | 
    
         | 
| 
       80 
102 
     | 
    
         
             
            			// Ensure that the intersection is on the line segment
         
     | 
| 
       81 
     | 
    
         
            -
            			if (point. 
     | 
| 
       82 
     | 
    
         
            -
            					|| point. 
     | 
| 
      
 103 
     | 
    
         
            +
            			if (point.distanceTo(line.p1) > line.length
         
     | 
| 
      
 104 
     | 
    
         
            +
            					|| point.distanceTo(line.p2) > line.length) {
         
     | 
| 
       83 
105 
     | 
    
         
             
            				return null;
         
     | 
| 
       84 
106 
     | 
    
         
             
            			}
         
     | 
| 
       85 
107 
     | 
    
         | 
| 
       86 
     | 
    
         
            -
            			return  
     | 
| 
       87 
     | 
    
         
            -
            		}).filter(entry => entry !== null) as  
     | 
| 
      
 108 
     | 
    
         
            +
            			return t;
         
     | 
| 
      
 109 
     | 
    
         
            +
            		}).filter(entry => entry !== null) as number[];
         
     | 
| 
      
 110 
     | 
    
         
            +
            	}
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
            	public override splitAt(t: number): [BezierJSWrapper] | [BezierJSWrapper, BezierJSWrapper] {
         
     | 
| 
      
 113 
     | 
    
         
            +
            		if (t <= 0 || t >= 1) {
         
     | 
| 
      
 114 
     | 
    
         
            +
            			return [ this ];
         
     | 
| 
      
 115 
     | 
    
         
            +
            		}
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
            		const bezier = this.getBezier();
         
     | 
| 
      
 118 
     | 
    
         
            +
            		const split = bezier.split(t);
         
     | 
| 
      
 119 
     | 
    
         
            +
            		return [
         
     | 
| 
      
 120 
     | 
    
         
            +
            			new BezierJSWrapperImpl(split.left.points.map(point => Vec2.ofXY(point)), split.left),
         
     | 
| 
      
 121 
     | 
    
         
            +
            			new BezierJSWrapperImpl(split.right.points.map(point => Vec2.ofXY(point)), split.right),
         
     | 
| 
      
 122 
     | 
    
         
            +
            		];
         
     | 
| 
      
 123 
     | 
    
         
            +
            	}
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
            	public override nearestPointTo(point: Point2) {
         
     | 
| 
      
 126 
     | 
    
         
            +
            		// One implementation could be similar to this:
         
     | 
| 
      
 127 
     | 
    
         
            +
            		//   const projection = this.getBezier().project(point);
         
     | 
| 
      
 128 
     | 
    
         
            +
            		//   return {
         
     | 
| 
      
 129 
     | 
    
         
            +
            		//    point: Vec2.ofXY(projection),
         
     | 
| 
      
 130 
     | 
    
         
            +
            		//    parameterValue: projection.t!,
         
     | 
| 
      
 131 
     | 
    
         
            +
            		//   };
         
     | 
| 
      
 132 
     | 
    
         
            +
            		// However, Bezier-js is rather impercise (and relies on a lookup table).
         
     | 
| 
      
 133 
     | 
    
         
            +
            		// Thus, we instead use Newton's Method:
         
     | 
| 
      
 134 
     | 
    
         
            +
             
     | 
| 
      
 135 
     | 
    
         
            +
            		// We want to find t such that f(t) = |B(t) - p|² is minimized.
         
     | 
| 
      
 136 
     | 
    
         
            +
            		// Expanding,
         
     | 
| 
      
 137 
     | 
    
         
            +
            		//   f(t)  = (Bₓ(t) - pₓ)² + (Bᵧ(t) - pᵧ)²
         
     | 
| 
      
 138 
     | 
    
         
            +
            		// ⇒ f'(t) = Dₜ(Bₓ(t) - pₓ)² + Dₜ(Bᵧ(t) - pᵧ)²
         
     | 
| 
      
 139 
     | 
    
         
            +
            		// ⇒ f'(t) = 2(Bₓ(t) - pₓ)(Bₓ'(t)) + 2(Bᵧ(t) - pᵧ)(Bᵧ'(t))
         
     | 
| 
      
 140 
     | 
    
         
            +
            		//         = 2Bₓ(t)Bₓ'(t) - 2pₓBₓ'(t) + 2Bᵧ(t)Bᵧ'(t) - 2pᵧBᵧ'(t)
         
     | 
| 
      
 141 
     | 
    
         
            +
            		// ⇒ f''(t)= 2Bₓ'(t)Bₓ'(t) + 2Bₓ(t)Bₓ''(t) - 2pₓBₓ''(t) + 2Bᵧ'(t)Bᵧ'(t)
         
     | 
| 
      
 142 
     | 
    
         
            +
            		//         + 2Bᵧ(t)Bᵧ''(t) - 2pᵧBᵧ''(t)
         
     | 
| 
      
 143 
     | 
    
         
            +
            		// Because f'(t) = 0 at relative extrema, we can use Newton's Method
         
     | 
| 
      
 144 
     | 
    
         
            +
            		// to improve on an initial guess.
         
     | 
| 
      
 145 
     | 
    
         
            +
             
     | 
| 
      
 146 
     | 
    
         
            +
            		const sqrDistAt = (t: number) => point.squareDistanceTo(this.at(t));
         
     | 
| 
      
 147 
     | 
    
         
            +
            		const yIntercept = sqrDistAt(0);
         
     | 
| 
      
 148 
     | 
    
         
            +
            		let t = 0;
         
     | 
| 
      
 149 
     | 
    
         
            +
            		let minSqrDist = yIntercept;
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
            		// Start by testing a few points:
         
     | 
| 
      
 152 
     | 
    
         
            +
            		const pointsToTest = 4;
         
     | 
| 
      
 153 
     | 
    
         
            +
            		for (let i = 0; i < pointsToTest; i ++) {
         
     | 
| 
      
 154 
     | 
    
         
            +
            			const testT = i / (pointsToTest - 1);
         
     | 
| 
      
 155 
     | 
    
         
            +
            			const testMinSqrDist = sqrDistAt(testT);
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
            			if (testMinSqrDist < minSqrDist) {
         
     | 
| 
      
 158 
     | 
    
         
            +
            				t = testT;
         
     | 
| 
      
 159 
     | 
    
         
            +
            				minSqrDist = testMinSqrDist;
         
     | 
| 
      
 160 
     | 
    
         
            +
            			}
         
     | 
| 
      
 161 
     | 
    
         
            +
            		}
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
            		// To use Newton's Method, we need to evaluate the second derivative of the distance
         
     | 
| 
      
 164 
     | 
    
         
            +
            		// function:
         
     | 
| 
      
 165 
     | 
    
         
            +
            		const secondDerivativeAt = (t: number) => {
         
     | 
| 
      
 166 
     | 
    
         
            +
            			// f''(t) = 2Bₓ'(t)Bₓ'(t) + 2Bₓ(t)Bₓ''(t) - 2pₓBₓ''(t)
         
     | 
| 
      
 167 
     | 
    
         
            +
            			//        + 2Bᵧ'(t)Bᵧ'(t) + 2Bᵧ(t)Bᵧ''(t) - 2pᵧBᵧ''(t)
         
     | 
| 
      
 168 
     | 
    
         
            +
            			const b = this.at(t);
         
     | 
| 
      
 169 
     | 
    
         
            +
            			const bPrime = this.derivativeAt(t);
         
     | 
| 
      
 170 
     | 
    
         
            +
            			const bPrimePrime = this.secondDerivativeAt(t);
         
     | 
| 
      
 171 
     | 
    
         
            +
            			return (
         
     | 
| 
      
 172 
     | 
    
         
            +
            				2 * bPrime.x * bPrime.x  +  2 * b.x * bPrimePrime.x  -  2 * point.x * bPrimePrime.x
         
     | 
| 
      
 173 
     | 
    
         
            +
            				+ 2 * bPrime.y * bPrime.y  +  2 * b.y * bPrimePrime.y  -  2 * point.y * bPrimePrime.y
         
     | 
| 
      
 174 
     | 
    
         
            +
            			);
         
     | 
| 
      
 175 
     | 
    
         
            +
            		};
         
     | 
| 
      
 176 
     | 
    
         
            +
            		// Because we're zeroing f'(t), we also need to be able to compute it:
         
     | 
| 
      
 177 
     | 
    
         
            +
            		const derivativeAt = (t: number) => {
         
     | 
| 
      
 178 
     | 
    
         
            +
            			// f'(t) = 2Bₓ(t)Bₓ'(t) - 2pₓBₓ'(t) + 2Bᵧ(t)Bᵧ'(t) - 2pᵧBᵧ'(t)
         
     | 
| 
      
 179 
     | 
    
         
            +
            			const b = this.at(t);
         
     | 
| 
      
 180 
     | 
    
         
            +
            			const bPrime = this.derivativeAt(t);
         
     | 
| 
      
 181 
     | 
    
         
            +
            			return (
         
     | 
| 
      
 182 
     | 
    
         
            +
            				2 * b.x * bPrime.x - 2 * point.x * bPrime.x
         
     | 
| 
      
 183 
     | 
    
         
            +
            				+ 2 * b.y * bPrime.y - 2 * point.y * bPrime.y
         
     | 
| 
      
 184 
     | 
    
         
            +
            			);
         
     | 
| 
      
 185 
     | 
    
         
            +
            		};
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
            		const iterate = () => {
         
     | 
| 
      
 188 
     | 
    
         
            +
            			const slope = secondDerivativeAt(t);
         
     | 
| 
      
 189 
     | 
    
         
            +
            			// We intersect a line through the point on f'(t) at t with the x-axis:
         
     | 
| 
      
 190 
     | 
    
         
            +
            			//    y = m(x - x₀) + y₀
         
     | 
| 
      
 191 
     | 
    
         
            +
            			// ⇒  x - x₀ = (y - y₀) / m
         
     | 
| 
      
 192 
     | 
    
         
            +
            			// ⇒  x = (y - y₀) / m + x₀
         
     | 
| 
      
 193 
     | 
    
         
            +
            			//
         
     | 
| 
      
 194 
     | 
    
         
            +
            			// Thus, when zeroed,
         
     | 
| 
      
 195 
     | 
    
         
            +
            			//   tN = (0 - f'(t)) / m + t
         
     | 
| 
      
 196 
     | 
    
         
            +
            			const newT = (0 - derivativeAt(t)) / slope + t;
         
     | 
| 
      
 197 
     | 
    
         
            +
            			//const distDiff = sqrDistAt(newT) - sqrDistAt(t);
         
     | 
| 
      
 198 
     | 
    
         
            +
            			//console.assert(distDiff <= 0, `${-distDiff} >= 0`);
         
     | 
| 
      
 199 
     | 
    
         
            +
            			t = newT;
         
     | 
| 
      
 200 
     | 
    
         
            +
            			if (t > 1) {
         
     | 
| 
      
 201 
     | 
    
         
            +
            				t = 1;
         
     | 
| 
      
 202 
     | 
    
         
            +
            			} else if (t < 0) {
         
     | 
| 
      
 203 
     | 
    
         
            +
            				t = 0;
         
     | 
| 
      
 204 
     | 
    
         
            +
            			}
         
     | 
| 
      
 205 
     | 
    
         
            +
            		};
         
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
      
 207 
     | 
    
         
            +
            		for (let i = 0; i < 12; i++) {
         
     | 
| 
      
 208 
     | 
    
         
            +
            			iterate();
         
     | 
| 
      
 209 
     | 
    
         
            +
            		}
         
     | 
| 
      
 210 
     | 
    
         
            +
             
     | 
| 
      
 211 
     | 
    
         
            +
            		return { parameterValue: t, point: this.at(t) };
         
     | 
| 
      
 212 
     | 
    
         
            +
            	}
         
     | 
| 
      
 213 
     | 
    
         
            +
             
     | 
| 
      
 214 
     | 
    
         
            +
            	public override toString() {
         
     | 
| 
      
 215 
     | 
    
         
            +
            		return `Bézier(${this.getPoints().map(point => point.toString()).join(', ')})`;
         
     | 
| 
      
 216 
     | 
    
         
            +
            	}
         
     | 
| 
      
 217 
     | 
    
         
            +
            }
         
     | 
| 
      
 218 
     | 
    
         
            +
             
     | 
| 
      
 219 
     | 
    
         
            +
            /**
         
     | 
| 
      
 220 
     | 
    
         
            +
             * Private concrete implementation of `BezierJSWrapper`, used by methods above that need to return a wrapper
         
     | 
| 
      
 221 
     | 
    
         
            +
             * around a `Bezier`.
         
     | 
| 
      
 222 
     | 
    
         
            +
             */
         
     | 
| 
      
 223 
     | 
    
         
            +
            class BezierJSWrapperImpl extends BezierJSWrapper {
         
     | 
| 
      
 224 
     | 
    
         
            +
            	public constructor(private controlPoints: readonly Point2[], curve?: Bezier) {
         
     | 
| 
      
 225 
     | 
    
         
            +
            		super(curve);
         
     | 
| 
      
 226 
     | 
    
         
            +
            	}
         
     | 
| 
       88 
227 
     | 
    
         | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
      
 228 
     | 
    
         
            +
            	public override getPoints() {
         
     | 
| 
      
 229 
     | 
    
         
            +
            		return this.controlPoints;
         
     | 
| 
       90 
230 
     | 
    
         
             
            	}
         
     | 
| 
       91 
231 
     | 
    
         
             
            }
         
     | 
| 
       92 
232 
     | 
    
         | 
| 
         @@ -28,7 +28,7 @@ describe('Line2', () => { 
     | 
|
| 
       28 
28 
     | 
    
         | 
| 
       29 
29 
     | 
    
         
             
            		expect(line1.intersection(line2)?.point).objEq(Vec2.of(0, 10));
         
     | 
| 
       30 
30 
     | 
    
         | 
| 
       31 
     | 
    
         
            -
            		// t=10 implies 10 units along  
     | 
| 
      
 31 
     | 
    
         
            +
            		// t=10 implies 10 units along the line from (10, 10) to (-10, 10)
         
     | 
| 
       32 
32 
     | 
    
         
             
            		expect(line1.intersection(line2)?.t).toBe(10);
         
     | 
| 
       33 
33 
     | 
    
         | 
| 
       34 
34 
     | 
    
         
             
            		// Similarly, t = 12 implies 12 units above (0, -2) in the direction of (0, 200)
         
     | 
| 
         @@ -96,4 +96,38 @@ describe('Line2', () => { 
     | 
|
| 
       96 
96 
     | 
    
         
             
            			p2: Vec2.of(3, 98),
         
     | 
| 
       97 
97 
     | 
    
         
             
            		});
         
     | 
| 
       98 
98 
     | 
    
         
             
            	});
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
            	it.each([
         
     | 
| 
      
 101 
     | 
    
         
            +
            		{ from: Vec2.of(0, 0), to: Vec2.of(2, 2) },
         
     | 
| 
      
 102 
     | 
    
         
            +
            		{ from: Vec2.of(100, 0), to: Vec2.of(2, 2) },
         
     | 
| 
      
 103 
     | 
    
         
            +
            	])('should be able to split a line segment between %j', ({ from, to }) => {
         
     | 
| 
      
 104 
     | 
    
         
            +
            		const midpoint = from.lerp(to, 0.5);
         
     | 
| 
      
 105 
     | 
    
         
            +
            		const lineSegment = new LineSegment2(from, to);
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
            		// Halving
         
     | 
| 
      
 108 
     | 
    
         
            +
            		//
         
     | 
| 
      
 109 
     | 
    
         
            +
            		expect(lineSegment.at(0.5)).objEq(midpoint);
         
     | 
| 
      
 110 
     | 
    
         
            +
            		const [ firstHalf, secondHalf ] = lineSegment.splitAt(0.5);
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
            		if (!secondHalf) {
         
     | 
| 
      
 113 
     | 
    
         
            +
            			throw new Error('Splitting a line segment in half should yield two line segments.');
         
     | 
| 
      
 114 
     | 
    
         
            +
            		}
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
            		expect(firstHalf.p2).objEq(midpoint);
         
     | 
| 
      
 117 
     | 
    
         
            +
            		expect(firstHalf.p1).objEq(from);
         
     | 
| 
      
 118 
     | 
    
         
            +
            		expect(secondHalf.p2).objEq(to);
         
     | 
| 
      
 119 
     | 
    
         
            +
            		expect(secondHalf.p1).objEq(midpoint);
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
            		// Before start/end
         
     | 
| 
      
 122 
     | 
    
         
            +
            		expect(lineSegment.splitAt(0)[0]).objEq(lineSegment);
         
     | 
| 
      
 123 
     | 
    
         
            +
            		expect(lineSegment.splitAt(0)).toHaveLength(1);
         
     | 
| 
      
 124 
     | 
    
         
            +
            		expect(lineSegment.splitAt(1)).toHaveLength(1);
         
     | 
| 
      
 125 
     | 
    
         
            +
            		expect(lineSegment.splitAt(2)).toHaveLength(1);
         
     | 
| 
      
 126 
     | 
    
         
            +
            	});
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
            	it('equivalence check should allow ignoring direction', () => {
         
     | 
| 
      
 129 
     | 
    
         
            +
            		expect(new LineSegment2(Vec2.zero, Vec2.unitX)).objEq(new LineSegment2(Vec2.zero, Vec2.unitX));
         
     | 
| 
      
 130 
     | 
    
         
            +
            		expect(new LineSegment2(Vec2.zero, Vec2.unitX)).objEq(new LineSegment2(Vec2.unitX, Vec2.zero));
         
     | 
| 
      
 131 
     | 
    
         
            +
            		expect(new LineSegment2(Vec2.zero, Vec2.unitX)).not.objEq(new LineSegment2(Vec2.unitX, Vec2.zero), { ignoreDirection: false });
         
     | 
| 
      
 132 
     | 
    
         
            +
            	});
         
     | 
| 
       99 
133 
     | 
    
         
             
            });
         
     | 
| 
         @@ -1,7 +1,8 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            import Mat33 from '../Mat33';
         
     | 
| 
       2 
2 
     | 
    
         
             
            import Rect2 from './Rect2';
         
     | 
| 
       3 
3 
     | 
    
         
             
            import { Vec2, Point2 } from '../Vec2';
         
     | 
| 
       4 
     | 
    
         
            -
            import  
     | 
| 
      
 4 
     | 
    
         
            +
            import Parameterized2DShape from './Parameterized2DShape';
         
     | 
| 
      
 5 
     | 
    
         
            +
            import Vec3 from '../Vec3';
         
     | 
| 
       5 
6 
     | 
    
         | 
| 
       6 
7 
     | 
    
         
             
            interface IntersectionResult {
         
     | 
| 
       7 
8 
     | 
    
         
             
            	point: Point2;
         
     | 
| 
         @@ -9,7 +10,7 @@ interface IntersectionResult { 
     | 
|
| 
       9 
10 
     | 
    
         
             
            }
         
     | 
| 
       10 
11 
     | 
    
         | 
| 
       11 
12 
     | 
    
         
             
            /** Represents a line segment. A `LineSegment2` is immutable. */
         
     | 
| 
       12 
     | 
    
         
            -
            export class LineSegment2 extends  
     | 
| 
      
 13 
     | 
    
         
            +
            export class LineSegment2 extends Parameterized2DShape {
         
     | 
| 
       13 
14 
     | 
    
         
             
            	// invariant: ||direction|| = 1
         
     | 
| 
       14 
15 
     | 
    
         | 
| 
       15 
16 
     | 
    
         
             
            	/**
         
     | 
| 
         @@ -58,8 +59,12 @@ export class LineSegment2 extends Abstract2DShape { 
     | 
|
| 
       58 
59 
     | 
    
         
             
            		return this.point2;
         
     | 
| 
       59 
60 
     | 
    
         
             
            	}
         
     | 
| 
       60 
61 
     | 
    
         | 
| 
      
 62 
     | 
    
         
            +
            	public get center(): Point2 {
         
     | 
| 
      
 63 
     | 
    
         
            +
            		return this.point1.lerp(this.point2, 0.5);
         
     | 
| 
      
 64 
     | 
    
         
            +
            	}
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
       61 
66 
     | 
    
         
             
            	/**
         
     | 
| 
       62 
     | 
    
         
            -
            	 * Gets a point a distance `t` along this line.
         
     | 
| 
      
 67 
     | 
    
         
            +
            	 * Gets a point a **distance** `t` along this line.
         
     | 
| 
       63 
68 
     | 
    
         
             
            	 *
         
     | 
| 
       64 
69 
     | 
    
         
             
            	 * @deprecated
         
     | 
| 
       65 
70 
     | 
    
         
             
            	 */
         
     | 
| 
         @@ -74,11 +79,40 @@ export class LineSegment2 extends Abstract2DShape { 
     | 
|
| 
       74 
79 
     | 
    
         
             
            	 *
         
     | 
| 
       75 
80 
     | 
    
         
             
            	 * `t` should be in `[0, 1]`.
         
     | 
| 
       76 
81 
     | 
    
         
             
            	 */
         
     | 
| 
       77 
     | 
    
         
            -
            	public at(t: number): Point2 {
         
     | 
| 
      
 82 
     | 
    
         
            +
            	public override at(t: number): Point2 {
         
     | 
| 
       78 
83 
     | 
    
         
             
            		return this.get(t * this.length);
         
     | 
| 
       79 
84 
     | 
    
         
             
            	}
         
     | 
| 
       80 
85 
     | 
    
         | 
| 
      
 86 
     | 
    
         
            +
            	public override normalAt(_t: number): Vec2 {
         
     | 
| 
      
 87 
     | 
    
         
            +
            		return this.direction.orthog();
         
     | 
| 
      
 88 
     | 
    
         
            +
            	}
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
            	public override tangentAt(_t: number): Vec3 {
         
     | 
| 
      
 91 
     | 
    
         
            +
            		return this.direction;
         
     | 
| 
      
 92 
     | 
    
         
            +
            	}
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
            	public splitAt(t: number): [LineSegment2]|[LineSegment2,LineSegment2] {
         
     | 
| 
      
 95 
     | 
    
         
            +
            		if (t <= 0 || t >= 1) {
         
     | 
| 
      
 96 
     | 
    
         
            +
            			return [this];
         
     | 
| 
      
 97 
     | 
    
         
            +
            		}
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
            		return [
         
     | 
| 
      
 100 
     | 
    
         
            +
            			new LineSegment2(this.point1, this.at(t)),
         
     | 
| 
      
 101 
     | 
    
         
            +
            			new LineSegment2(this.at(t), this.point2),
         
     | 
| 
      
 102 
     | 
    
         
            +
            		];
         
     | 
| 
      
 103 
     | 
    
         
            +
            	}
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
            	/**
         
     | 
| 
      
 106 
     | 
    
         
            +
            	 * Returns the intersection of this with another line segment.
         
     | 
| 
      
 107 
     | 
    
         
            +
            	 *
         
     | 
| 
      
 108 
     | 
    
         
            +
            	 * **WARNING**: The parameter value returned by this method does not range from 0 to 1 and
         
     | 
| 
      
 109 
     | 
    
         
            +
            	 *              is currently a length.
         
     | 
| 
      
 110 
     | 
    
         
            +
            	 *              This will change in a future release.
         
     | 
| 
      
 111 
     | 
    
         
            +
            	 * @deprecated
         
     | 
| 
      
 112 
     | 
    
         
            +
            	 */
         
     | 
| 
       81 
113 
     | 
    
         
             
            	public intersection(other: LineSegment2): IntersectionResult|null {
         
     | 
| 
      
 114 
     | 
    
         
            +
            		// TODO(v2.0.0): Make this return a `t` value from `0` to `1`.
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
       82 
116 
     | 
    
         
             
            		// We want x₁(t) = x₂(t) and y₁(t) = y₂(t)
         
     | 
| 
       83 
117 
     | 
    
         
             
            		// Observe that
         
     | 
| 
       84 
118 
     | 
    
         
             
            		// x = this.point1.x + this.direction.x · t₁
         
     | 
| 
         @@ -146,10 +180,10 @@ export class LineSegment2 extends Abstract2DShape { 
     | 
|
| 
       146 
180 
     | 
    
         
             
            		}
         
     | 
| 
       147 
181 
     | 
    
         | 
| 
       148 
182 
     | 
    
         
             
            		// Ensure the result is in this/the other segment.
         
     | 
| 
       149 
     | 
    
         
            -
            		const resultToP1 = resultPoint. 
     | 
| 
       150 
     | 
    
         
            -
            		const resultToP2 = resultPoint. 
     | 
| 
       151 
     | 
    
         
            -
            		const resultToP3 = resultPoint. 
     | 
| 
       152 
     | 
    
         
            -
            		const resultToP4 = resultPoint. 
     | 
| 
      
 183 
     | 
    
         
            +
            		const resultToP1 = resultPoint.distanceTo(this.point1);
         
     | 
| 
      
 184 
     | 
    
         
            +
            		const resultToP2 = resultPoint.distanceTo(this.point2);
         
     | 
| 
      
 185 
     | 
    
         
            +
            		const resultToP3 = resultPoint.distanceTo(other.point1);
         
     | 
| 
      
 186 
     | 
    
         
            +
            		const resultToP4 = resultPoint.distanceTo(other.point2);
         
     | 
| 
       153 
187 
     | 
    
         
             
            		if (resultToP1 > this.length
         
     | 
| 
       154 
188 
     | 
    
         
             
            			|| resultToP2 > this.length
         
     | 
| 
       155 
189 
     | 
    
         
             
            			|| resultToP3 > other.length
         
     | 
| 
         @@ -167,6 +201,15 @@ export class LineSegment2 extends Abstract2DShape { 
     | 
|
| 
       167 
201 
     | 
    
         
             
            		return this.intersection(other) !== null;
         
     | 
| 
       168 
202 
     | 
    
         
             
            	}
         
     | 
| 
       169 
203 
     | 
    
         | 
| 
      
 204 
     | 
    
         
            +
            	public override argIntersectsLineSegment(lineSegment: LineSegment2) {
         
     | 
| 
      
 205 
     | 
    
         
            +
            		const intersection = this.intersection(lineSegment);
         
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
      
 207 
     | 
    
         
            +
            		if (intersection) {
         
     | 
| 
      
 208 
     | 
    
         
            +
            			return [ intersection.t / this.length ];
         
     | 
| 
      
 209 
     | 
    
         
            +
            		}
         
     | 
| 
      
 210 
     | 
    
         
            +
            		return [];
         
     | 
| 
      
 211 
     | 
    
         
            +
            	}
         
     | 
| 
      
 212 
     | 
    
         
            +
             
     | 
| 
       170 
213 
     | 
    
         
             
            	/**
         
     | 
| 
       171 
214 
     | 
    
         
             
            	 * Returns the points at which this line segment intersects the
         
     | 
| 
       172 
215 
     | 
    
         
             
            	 * given line segment.
         
     | 
| 
         @@ -186,6 +229,10 @@ export class LineSegment2 extends Abstract2DShape { 
     | 
|
| 
       186 
229 
     | 
    
         | 
| 
       187 
230 
     | 
    
         
             
            	// Returns the closest point on this to [target]
         
     | 
| 
       188 
231 
     | 
    
         
             
            	public closestPointTo(target: Point2) {
         
     | 
| 
      
 232 
     | 
    
         
            +
            		return this.nearestPointTo(target).point;
         
     | 
| 
      
 233 
     | 
    
         
            +
            	}
         
     | 
| 
      
 234 
     | 
    
         
            +
             
     | 
| 
      
 235 
     | 
    
         
            +
            	public override nearestPointTo(target: Vec3): { point: Vec3; parameterValue: number; } {
         
     | 
| 
       189 
236 
     | 
    
         
             
            		// Distance from P1 along this' direction.
         
     | 
| 
       190 
237 
     | 
    
         
             
            		const projectedDistFromP1 = target.minus(this.p1).dot(this.direction);
         
     | 
| 
       191 
238 
     | 
    
         
             
            		const projectedDistFromP2 = this.length - projectedDistFromP1;
         
     | 
| 
         @@ -193,13 +240,13 @@ export class LineSegment2 extends Abstract2DShape { 
     | 
|
| 
       193 
240 
     | 
    
         
             
            		const projection = this.p1.plus(this.direction.times(projectedDistFromP1));
         
     | 
| 
       194 
241 
     | 
    
         | 
| 
       195 
242 
     | 
    
         
             
            		if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) {
         
     | 
| 
       196 
     | 
    
         
            -
            			return projection;
         
     | 
| 
      
 243 
     | 
    
         
            +
            			return { point: projection, parameterValue: projectedDistFromP1 / this.length };
         
     | 
| 
       197 
244 
     | 
    
         
             
            		}
         
     | 
| 
       198 
245 
     | 
    
         | 
| 
       199 
246 
     | 
    
         
             
            		if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) {
         
     | 
| 
       200 
     | 
    
         
            -
            			return this.p2;
         
     | 
| 
      
 247 
     | 
    
         
            +
            			return { point: this.p2, parameterValue: 1 };
         
     | 
| 
       201 
248 
     | 
    
         
             
            		} else {
         
     | 
| 
       202 
     | 
    
         
            -
            			return this.p1;
         
     | 
| 
      
 249 
     | 
    
         
            +
            			return { point: this.p1, parameterValue: 0 };
         
     | 
| 
       203 
250 
     | 
    
         
             
            		}
         
     | 
| 
       204 
251 
     | 
    
         
             
            	}
         
     | 
| 
       205 
252 
     | 
    
         | 
| 
         @@ -228,5 +275,26 @@ export class LineSegment2 extends Abstract2DShape { 
     | 
|
| 
       228 
275 
     | 
    
         
             
            	public override toString() {
         
     | 
| 
       229 
276 
     | 
    
         
             
            		return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
         
     | 
| 
       230 
277 
     | 
    
         
             
            	}
         
     | 
| 
      
 278 
     | 
    
         
            +
             
     | 
| 
      
 279 
     | 
    
         
            +
            	/**
         
     | 
| 
      
 280 
     | 
    
         
            +
            	 * Returns `true` iff this is equivalent to `other`.
         
     | 
| 
      
 281 
     | 
    
         
            +
            	 *
         
     | 
| 
      
 282 
     | 
    
         
            +
            	 * **Options**:
         
     | 
| 
      
 283 
     | 
    
         
            +
            	 * - `tolerance`: The maximum difference between endpoints. (Default: 0)
         
     | 
| 
      
 284 
     | 
    
         
            +
            	 * - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
         
     | 
| 
      
 285 
     | 
    
         
            +
            	 */
         
     | 
| 
      
 286 
     | 
    
         
            +
            	public eq(other: LineSegment2, options?: { tolerance?: number, ignoreDirection?: boolean }) {
         
     | 
| 
      
 287 
     | 
    
         
            +
            		if (!(other instanceof LineSegment2)) {
         
     | 
| 
      
 288 
     | 
    
         
            +
            			return false;
         
     | 
| 
      
 289 
     | 
    
         
            +
            		}
         
     | 
| 
      
 290 
     | 
    
         
            +
             
     | 
| 
      
 291 
     | 
    
         
            +
            		const tolerance = options?.tolerance;
         
     | 
| 
      
 292 
     | 
    
         
            +
            		const ignoreDirection = options?.ignoreDirection ?? true;
         
     | 
| 
      
 293 
     | 
    
         
            +
             
     | 
| 
      
 294 
     | 
    
         
            +
            		return (
         
     | 
| 
      
 295 
     | 
    
         
            +
            			(other.p1.eq(this.p1, tolerance) && other.p2.eq(this.p2, tolerance))
         
     | 
| 
      
 296 
     | 
    
         
            +
            			|| (ignoreDirection && other.p1.eq(this.p2, tolerance) && other.p2.eq(this.p1, tolerance))
         
     | 
| 
      
 297 
     | 
    
         
            +
            		);
         
     | 
| 
      
 298 
     | 
    
         
            +
            	}
         
     | 
| 
       231 
299 
     | 
    
         
             
            }
         
     | 
| 
       232 
300 
     | 
    
         
             
            export default LineSegment2;
         
     | 
| 
         @@ -0,0 +1,39 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            import { Point2, Vec2 } from '../Vec2';
         
     | 
| 
      
 2 
     | 
    
         
            +
            import Abstract2DShape from './Abstract2DShape';
         
     | 
| 
      
 3 
     | 
    
         
            +
            import LineSegment2 from './LineSegment2';
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            /** A 2-dimensional path with parameter interval $t \in [0, 1]$. */
         
     | 
| 
      
 6 
     | 
    
         
            +
            export abstract class Parameterized2DShape extends Abstract2DShape {
         
     | 
| 
      
 7 
     | 
    
         
            +
            	/** Returns this at a given parameter. $t \in [0, 1]$ */
         
     | 
| 
      
 8 
     | 
    
         
            +
            	abstract at(t: number): Point2;
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            	/** Computes the unit normal vector at $t$. */
         
     | 
| 
      
 11 
     | 
    
         
            +
            	abstract normalAt(t: number): Vec2;
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            	abstract tangentAt(t: number): Vec2;
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            	/**
         
     | 
| 
      
 16 
     | 
    
         
            +
            	 * Divides this shape into two separate shapes at parameter value $t$.
         
     | 
| 
      
 17 
     | 
    
         
            +
            	 */
         
     | 
| 
      
 18 
     | 
    
         
            +
            	abstract splitAt(t: number): [ Parameterized2DShape ] | [ Parameterized2DShape, Parameterized2DShape ];
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            	/**
         
     | 
| 
      
 21 
     | 
    
         
            +
            	 * Returns the nearest point on `this` to `point` and the `parameterValue` at which
         
     | 
| 
      
 22 
     | 
    
         
            +
            	 * that point occurs.
         
     | 
| 
      
 23 
     | 
    
         
            +
            	 */
         
     | 
| 
      
 24 
     | 
    
         
            +
            	abstract nearestPointTo(point: Point2): { point: Point2, parameterValue: number };
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            	/**
         
     | 
| 
      
 27 
     | 
    
         
            +
            	 * Returns the **parameter values** at which `lineSegment` intersects this shape.
         
     | 
| 
      
 28 
     | 
    
         
            +
            	 *
         
     | 
| 
      
 29 
     | 
    
         
            +
            	 * See also {@link intersectsLineSegment}
         
     | 
| 
      
 30 
     | 
    
         
            +
            	 */
         
     | 
| 
      
 31 
     | 
    
         
            +
            	public abstract argIntersectsLineSegment(lineSegment: LineSegment2): number[];
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
            	public override intersectsLineSegment(line: LineSegment2): Point2[] {
         
     | 
| 
      
 35 
     | 
    
         
            +
            		return this.argIntersectsLineSegment(line).map(t => this.at(t));
         
     | 
| 
      
 36 
     | 
    
         
            +
            	}
         
     | 
| 
      
 37 
     | 
    
         
            +
            }
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
            export default Parameterized2DShape;
         
     | 
    
        package/src/shapes/Path.test.ts
    CHANGED
    
    | 
         @@ -60,6 +60,24 @@ describe('Path', () => { 
     | 
|
| 
       60 
60 
     | 
    
         
             
            		);
         
     | 
| 
       61 
61 
     | 
    
         
             
            	});
         
     | 
| 
       62 
62 
     | 
    
         | 
| 
      
 63 
     | 
    
         
            +
            	it.each([
         
     | 
| 
      
 64 
     | 
    
         
            +
            		[ 'm0,0 L1,1', 'M0,0 L1,1', true ],
         
     | 
| 
      
 65 
     | 
    
         
            +
            		[ 'm0,0 L1,1', 'M1,1 L0,0', false ],
         
     | 
| 
      
 66 
     | 
    
         
            +
            		[ 'm0,0 L1,1 Q2,3 4,5', 'M1,1 L0,0', false ],
         
     | 
| 
      
 67 
     | 
    
         
            +
            		[ 'm0,0 L1,1 Q2,3 4,5', 'M1,1 L0,0 Q2,3 4,5', false ],
         
     | 
| 
      
 68 
     | 
    
         
            +
            		[ 'm0,0 L1,1 Q2,3 4,5', 'M0,0 L1,1 Q2,3 4,5', true ],
         
     | 
| 
      
 69 
     | 
    
         
            +
            		[ '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 ],
         
     | 
| 
      
 70 
     | 
    
         
            +
            		[ '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 ],
         
     | 
| 
      
 71 
     | 
    
         
            +
            		[ '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 ],
         
     | 
| 
      
 72 
     | 
    
         
            +
            		[ '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 ],
         
     | 
| 
      
 73 
     | 
    
         
            +
            		[ '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 ],
         
     | 
| 
      
 74 
     | 
    
         
            +
            		[ '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 ],
         
     | 
| 
      
 75 
     | 
    
         
            +
            	])('.eq should check equality', (path1Str, path2Str, shouldEqual) => {
         
     | 
| 
      
 76 
     | 
    
         
            +
            		expect(Path.fromString(path1Str)).objEq(Path.fromString(path1Str));
         
     | 
| 
      
 77 
     | 
    
         
            +
            		expect(Path.fromString(path2Str)).objEq(Path.fromString(path2Str));
         
     | 
| 
      
 78 
     | 
    
         
            +
            		expect(Path.fromString(path1Str).eq(Path.fromString(path2Str))).toBe(shouldEqual);
         
     | 
| 
      
 79 
     | 
    
         
            +
            	});
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
       63 
81 
     | 
    
         
             
            	describe('intersection', () => {
         
     | 
| 
       64 
82 
     | 
    
         
             
            		it('should give all intersections for a path made up of lines', () => {
         
     | 
| 
       65 
83 
     | 
    
         
             
            			const lineStart = Vec2.of(100, 100);
         
     | 
| 
         @@ -179,7 +197,7 @@ describe('Path', () => { 
     | 
|
| 
       179 
197 
     | 
    
         
             
            			});
         
     | 
| 
       180 
198 
     | 
    
         
             
            		});
         
     | 
| 
       181 
199 
     | 
    
         | 
| 
       182 
     | 
    
         
            -
            		it('should  
     | 
| 
      
 200 
     | 
    
         
            +
            		it('should correctly report intersections for a simple Bézier curve path', () => {
         
     | 
| 
       183 
201 
     | 
    
         
             
            			const lineStart = Vec2.zero;
         
     | 
| 
       184 
202 
     | 
    
         
             
            			const path = new Path(lineStart, [
         
     | 
| 
       185 
203 
     | 
    
         
             
            				{
         
     | 
| 
         @@ -196,13 +214,36 @@ describe('Path', () => { 
     | 
|
| 
       196 
214 
     | 
    
         
             
            			let intersections = path.intersection(
         
     | 
| 
       197 
215 
     | 
    
         
             
            				new LineSegment2(Vec2.of(-1, 0.5), Vec2.of(2, 0.5)), strokeWidth,
         
     | 
| 
       198 
216 
     | 
    
         
             
            			);
         
     | 
| 
       199 
     | 
    
         
            -
            			expect(intersections 
     | 
| 
      
 217 
     | 
    
         
            +
            			expect(intersections).toHaveLength(0);
         
     | 
| 
       200 
218 
     | 
    
         | 
| 
       201 
219 
     | 
    
         
             
            			// Should be an intersection when exiting/entering the edge of the stroke
         
     | 
| 
       202 
220 
     | 
    
         
             
            			intersections = path.intersection(
         
     | 
| 
       203 
221 
     | 
    
         
             
            				new LineSegment2(Vec2.of(0, 0.5), Vec2.of(8, 0.5)), strokeWidth,
         
     | 
| 
       204 
222 
     | 
    
         
             
            			);
         
     | 
| 
       205 
     | 
    
         
            -
            			expect(intersections 
     | 
| 
      
 223 
     | 
    
         
            +
            			expect(intersections).toHaveLength(1);
         
     | 
| 
      
 224 
     | 
    
         
            +
            		});
         
     | 
| 
      
 225 
     | 
    
         
            +
             
     | 
| 
      
 226 
     | 
    
         
            +
            		it('should correctly report intersections near the cap of a line-like Bézier', () => {
         
     | 
| 
      
 227 
     | 
    
         
            +
            			const path = Path.fromString('M0,0Q14,0 27,0');
         
     | 
| 
      
 228 
     | 
    
         
            +
            			expect(
         
     | 
| 
      
 229 
     | 
    
         
            +
            				path.intersection(
         
     | 
| 
      
 230 
     | 
    
         
            +
            					new LineSegment2(Vec2.of(0, -100), Vec2.of(0, 100)),
         
     | 
| 
      
 231 
     | 
    
         
            +
            					10,
         
     | 
| 
      
 232 
     | 
    
         
            +
            				),
         
     | 
| 
      
 233 
     | 
    
         
            +
             
     | 
| 
      
 234 
     | 
    
         
            +
            				// Should have intersections, despite being at the cap of the Bézier
         
     | 
| 
      
 235 
     | 
    
         
            +
            				// curve.
         
     | 
| 
      
 236 
     | 
    
         
            +
            			).toHaveLength(2);
         
     | 
| 
      
 237 
     | 
    
         
            +
            		});
         
     | 
| 
      
 238 
     | 
    
         
            +
             
     | 
| 
      
 239 
     | 
    
         
            +
            		it.each([
         
     | 
| 
      
 240 
     | 
    
         
            +
            			[new LineSegment2(Vec2.of(43.5,-12.5), Vec2.of(40.5,24.5)), 0],
         
     | 
| 
      
 241 
     | 
    
         
            +
            			// TODO: The below case is failing. It seems to be a Bezier-js bug though...
         
     | 
| 
      
 242 
     | 
    
         
            +
            			// (The Bézier.js method returns an empty array).
         
     | 
| 
      
 243 
     | 
    
         
            +
            			//[new LineSegment2(Vec2.of(35.5,19.5), Vec2.of(38.5,-17.5)), 0],
         
     | 
| 
      
 244 
     | 
    
         
            +
            		])('should correctly report positive intersections with a line-like Bézier', (line, strokeRadius) => {
         
     | 
| 
      
 245 
     | 
    
         
            +
            			const bezier = Path.fromString('M0,0 Q50,0 100,0');
         
     | 
| 
      
 246 
     | 
    
         
            +
            			expect(bezier.intersection(line, strokeRadius).length).toBeGreaterThan(0);
         
     | 
| 
       206 
247 
     | 
    
         
             
            		});
         
     | 
| 
       207 
248 
     | 
    
         
             
            	});
         
     | 
| 
       208 
249 
     | 
    
         | 
| 
         @@ -306,4 +347,23 @@ describe('Path', () => { 
     | 
|
| 
       306 
347 
     | 
    
         
             
            			expect(strokedRect.startPoint).objEq(lastSegment.point);
         
     | 
| 
       307 
348 
     | 
    
         
             
            		});
         
     | 
| 
       308 
349 
     | 
    
         
             
            	});
         
     | 
| 
      
 350 
     | 
    
         
            +
             
     | 
| 
      
 351 
     | 
    
         
            +
            	it.each([
         
     | 
| 
      
 352 
     | 
    
         
            +
            		[ 'm0,0 L1,1', 'M1,1 L0,0' ],
         
     | 
| 
      
 353 
     | 
    
         
            +
            		[ 'm0,0 L1,1', 'M1,1 L0,0' ],
         
     | 
| 
      
 354 
     | 
    
         
            +
            		[ 'M0,0 L1,1 Q2,2 3,3', 'M3,3 Q2,2 1,1 L0,0' ],
         
     | 
| 
      
 355 
     | 
    
         
            +
            		[ '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' ],
         
     | 
| 
      
 356 
     | 
    
         
            +
            	])('.reversed should reverse paths', (original, expected) => {
         
     | 
| 
      
 357 
     | 
    
         
            +
            		expect(Path.fromString(original).reversed()).objEq(Path.fromString(expected));
         
     | 
| 
      
 358 
     | 
    
         
            +
            		expect(Path.fromString(expected).reversed()).objEq(Path.fromString(original));
         
     | 
| 
      
 359 
     | 
    
         
            +
            		expect(Path.fromString(original).reversed().reversed()).objEq(Path.fromString(original));
         
     | 
| 
      
 360 
     | 
    
         
            +
            	});
         
     | 
| 
      
 361 
     | 
    
         
            +
             
     | 
| 
      
 362 
     | 
    
         
            +
            	it.each([
         
     | 
| 
      
 363 
     | 
    
         
            +
            		[ 'm0,0 l1,0', Vec2.of(0, 0), Vec2.of(0, 0) ],
         
     | 
| 
      
 364 
     | 
    
         
            +
            		[ 'm0,0 l1,0', Vec2.of(0.5, 0), Vec2.of(0.5, 0) ],
         
     | 
| 
      
 365 
     | 
    
         
            +
            		[ 'm0,0 Q1,0 1,2', Vec2.of(1, 0), Vec2.of(0.6236, 0.299) ],
         
     | 
| 
      
 366 
     | 
    
         
            +
            	])('.nearestPointTo should return the closest point on a path to the given parameter (case %#)', (path, point, expectedClosest) => {
         
     | 
| 
      
 367 
     | 
    
         
            +
            		expect(Path.fromString(path).nearestPointTo(point).point).objEq(expectedClosest, 0.002);
         
     | 
| 
      
 368 
     | 
    
         
            +
            	});
         
     | 
| 
       309 
369 
     | 
    
         
             
            });
         
     |