@js-draw/math 1.17.0 → 1.18.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.
Files changed (56) hide show
  1. package/dist/cjs/Mat33.js +6 -1
  2. package/dist/cjs/Vec3.d.ts +2 -1
  3. package/dist/cjs/Vec3.js +5 -7
  4. package/dist/cjs/lib.d.ts +2 -1
  5. package/dist/cjs/lib.js +5 -1
  6. package/dist/cjs/shapes/BezierJSWrapper.d.ts +4 -0
  7. package/dist/cjs/shapes/BezierJSWrapper.js +35 -0
  8. package/dist/cjs/shapes/LineSegment2.d.ts +11 -0
  9. package/dist/cjs/shapes/LineSegment2.js +26 -1
  10. package/dist/cjs/shapes/Parameterized2DShape.d.ts +6 -1
  11. package/dist/cjs/shapes/Parameterized2DShape.js +6 -1
  12. package/dist/cjs/shapes/Path.d.ts +96 -12
  13. package/dist/cjs/shapes/Path.js +338 -15
  14. package/dist/cjs/shapes/QuadraticBezier.d.ts +2 -3
  15. package/dist/cjs/shapes/QuadraticBezier.js +2 -3
  16. package/dist/cjs/shapes/Rect2.d.ts +6 -1
  17. package/dist/cjs/shapes/Rect2.js +5 -1
  18. package/dist/cjs/utils/convexHull2Of.d.ts +9 -0
  19. package/dist/cjs/utils/convexHull2Of.js +61 -0
  20. package/dist/cjs/utils/convexHull2Of.test.d.ts +1 -0
  21. package/dist/mjs/Mat33.mjs +6 -1
  22. package/dist/mjs/Vec3.d.ts +2 -1
  23. package/dist/mjs/Vec3.mjs +5 -7
  24. package/dist/mjs/lib.d.ts +2 -1
  25. package/dist/mjs/lib.mjs +2 -1
  26. package/dist/mjs/shapes/BezierJSWrapper.d.ts +4 -0
  27. package/dist/mjs/shapes/BezierJSWrapper.mjs +35 -0
  28. package/dist/mjs/shapes/LineSegment2.d.ts +11 -0
  29. package/dist/mjs/shapes/LineSegment2.mjs +26 -1
  30. package/dist/mjs/shapes/Parameterized2DShape.d.ts +6 -1
  31. package/dist/mjs/shapes/Parameterized2DShape.mjs +6 -1
  32. package/dist/mjs/shapes/Path.d.ts +96 -12
  33. package/dist/mjs/shapes/Path.mjs +335 -14
  34. package/dist/mjs/shapes/QuadraticBezier.d.ts +2 -3
  35. package/dist/mjs/shapes/QuadraticBezier.mjs +2 -3
  36. package/dist/mjs/shapes/Rect2.d.ts +6 -1
  37. package/dist/mjs/shapes/Rect2.mjs +5 -1
  38. package/dist/mjs/utils/convexHull2Of.d.ts +9 -0
  39. package/dist/mjs/utils/convexHull2Of.mjs +59 -0
  40. package/dist/mjs/utils/convexHull2Of.test.d.ts +1 -0
  41. package/package.json +2 -2
  42. package/src/Mat33.ts +8 -2
  43. package/src/Vec3.test.ts +16 -0
  44. package/src/Vec3.ts +7 -8
  45. package/src/lib.ts +3 -0
  46. package/src/shapes/BezierJSWrapper.ts +41 -0
  47. package/src/shapes/LineSegment2.test.ts +26 -0
  48. package/src/shapes/LineSegment2.ts +31 -1
  49. package/src/shapes/Parameterized2DShape.ts +6 -1
  50. package/src/shapes/Path.test.ts +173 -5
  51. package/src/shapes/Path.ts +390 -18
  52. package/src/shapes/QuadraticBezier.test.ts +21 -0
  53. package/src/shapes/QuadraticBezier.ts +2 -3
  54. package/src/shapes/Rect2.ts +6 -2
  55. package/src/utils/convexHull2Of.test.ts +43 -0
  56. package/src/utils/convexHull2Of.ts +71 -0
package/dist/mjs/lib.d.ts CHANGED
@@ -17,8 +17,9 @@
17
17
  * @packageDocumentation
18
18
  */
19
19
  export { LineSegment2 } from './shapes/LineSegment2';
20
- export { Path, IntersectionResult as PathIntersectionResult, CurveIndexRecord as PathCurveIndex, PathCommandType, PathCommand, LinePathCommand, MoveToPathCommand, QuadraticBezierPathCommand, CubicBezierPathCommand, } from './shapes/Path';
20
+ export { Path, IntersectionResult as PathIntersectionResult, CurveIndexRecord as PathCurveIndex, stepCurveIndexBy as stepPathIndexBy, compareCurveIndices as comparePathIndices, PathCommandType, PathCommand, LinePathCommand, MoveToPathCommand, QuadraticBezierPathCommand, CubicBezierPathCommand, } from './shapes/Path';
21
21
  export { Rect2 } from './shapes/Rect2';
22
+ export { Parameterized2DShape } from './shapes/Parameterized2DShape';
22
23
  export { QuadraticBezier } from './shapes/QuadraticBezier';
23
24
  export { Abstract2DShape } from './shapes/Abstract2DShape';
24
25
  export { Mat33, Mat33Array } from './Mat33';
package/dist/mjs/lib.mjs CHANGED
@@ -17,8 +17,9 @@
17
17
  * @packageDocumentation
18
18
  */
19
19
  export { LineSegment2 } from './shapes/LineSegment2.mjs';
20
- export { Path, PathCommandType, } from './shapes/Path.mjs';
20
+ export { Path, stepCurveIndexBy as stepPathIndexBy, compareCurveIndices as comparePathIndices, PathCommandType, } from './shapes/Path.mjs';
21
21
  export { Rect2 } from './shapes/Rect2.mjs';
22
+ export { Parameterized2DShape } from './shapes/Parameterized2DShape.mjs';
22
23
  export { QuadraticBezier } from './shapes/QuadraticBezier.mjs';
23
24
  export { Abstract2DShape } from './shapes/Abstract2DShape.mjs';
24
25
  export { Mat33 } from './Mat33.mjs';
@@ -41,6 +41,10 @@ export declare abstract class BezierJSWrapper extends Parameterized2DShape {
41
41
  parameterValue: number;
42
42
  point: import("../Vec3").Vec3;
43
43
  };
44
+ intersectsBezier(other: BezierJSWrapper): {
45
+ parameterValue: number;
46
+ point: import("../Vec3").Vec3;
47
+ }[];
44
48
  toString(): string;
45
49
  }
46
50
  export default BezierJSWrapper;
@@ -12,6 +12,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
12
12
  var _BezierJSWrapper_bezierJs;
13
13
  import { Bezier } from 'bezier-js';
14
14
  import { Vec2 } from '../Vec2.mjs';
15
+ import LineSegment2 from './LineSegment2.mjs';
15
16
  import Rect2 from './Rect2.mjs';
16
17
  import Parameterized2DShape from './Parameterized2DShape.mjs';
17
18
  /**
@@ -78,6 +79,17 @@ export class BezierJSWrapper extends Parameterized2DShape {
78
79
  return new Rect2(bbox.x.min, bbox.y.min, width, height);
79
80
  }
80
81
  argIntersectsLineSegment(line) {
82
+ // Bezier-js has a bug when all control points of a Bezier curve lie on
83
+ // a line. Our solution involves converting the Bezier into a line, then
84
+ // finding the parameter value that produced the intersection.
85
+ //
86
+ // TODO: This is unnecessarily slow. A better solution would be to fix
87
+ // the bug upstream.
88
+ const asLine = LineSegment2.ofSmallestContainingPoints(this.getPoints());
89
+ if (asLine) {
90
+ const intersection = asLine.intersectsLineSegment(line);
91
+ return intersection.map(p => this.nearestPointTo(p).parameterValue);
92
+ }
81
93
  const bezier = this.getBezier();
82
94
  return bezier.intersects(line).map(t => {
83
95
  // We're using the .intersects(line) function, which is documented
@@ -160,6 +172,8 @@ export class BezierJSWrapper extends Parameterized2DShape {
160
172
  };
161
173
  const iterate = () => {
162
174
  const slope = secondDerivativeAt(t);
175
+ if (slope === 0)
176
+ return;
163
177
  // We intersect a line through the point on f'(t) at t with the x-axis:
164
178
  // y = m(x - x₀) + y₀
165
179
  // ⇒ x - x₀ = (y - y₀) / m
@@ -183,6 +197,27 @@ export class BezierJSWrapper extends Parameterized2DShape {
183
197
  }
184
198
  return { parameterValue: t, point: this.at(t) };
185
199
  }
200
+ intersectsBezier(other) {
201
+ const intersections = this.getBezier().intersects(other.getBezier());
202
+ if (!intersections || intersections.length === 0) {
203
+ return [];
204
+ }
205
+ const result = [];
206
+ for (const intersection of intersections) {
207
+ // From http://pomax.github.io/bezierjs/#intersect-curve,
208
+ // .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
209
+ const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(intersection);
210
+ if (!match) {
211
+ throw new Error(`Incorrect format returned by .intersects: ${intersections} should be array of "number/number"!`);
212
+ }
213
+ const t = parseFloat(match[1]);
214
+ result.push({
215
+ parameterValue: t,
216
+ point: this.at(t),
217
+ });
218
+ }
219
+ return result;
220
+ }
186
221
  toString() {
187
222
  return `Bézier(${this.getPoints().map(point => point.toString()).join(', ')})`;
188
223
  }
@@ -25,6 +25,17 @@ export declare class LineSegment2 extends Parameterized2DShape {
25
25
  readonly bbox: Rect2;
26
26
  /** Creates a new `LineSegment2` from its endpoints. */
27
27
  constructor(point1: Point2, point2: Point2);
28
+ /**
29
+ * Returns the smallest line segment that contains all points in `points`, or `null`
30
+ * if no such line segment exists.
31
+ *
32
+ * @example
33
+ * ```ts,runnable
34
+ * import {LineSegment2, Vec2} from '@js-draw/math';
35
+ * console.log(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 0), Vec2.of(0, 1)]));
36
+ * ```
37
+ */
38
+ static ofSmallestContainingPoints(points: readonly Point2[]): LineSegment2 | null;
28
39
  /** Alias for `point1`. */
29
40
  get p1(): Point2;
30
41
  /** Alias for `point2`. */
@@ -16,6 +16,28 @@ export class LineSegment2 extends Parameterized2DShape {
16
16
  this.direction = this.direction.times(1 / this.length);
17
17
  }
18
18
  }
19
+ /**
20
+ * Returns the smallest line segment that contains all points in `points`, or `null`
21
+ * if no such line segment exists.
22
+ *
23
+ * @example
24
+ * ```ts,runnable
25
+ * import {LineSegment2, Vec2} from '@js-draw/math';
26
+ * console.log(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 0), Vec2.of(0, 1)]));
27
+ * ```
28
+ */
29
+ static ofSmallestContainingPoints(points) {
30
+ if (points.length <= 1)
31
+ return null;
32
+ const sorted = [...points].sort((a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y);
33
+ const line = new LineSegment2(sorted[0], sorted[sorted.length - 1]);
34
+ for (const point of sorted) {
35
+ if (!line.containsPoint(point)) {
36
+ return null;
37
+ }
38
+ }
39
+ return line;
40
+ }
19
41
  // Accessors to make LineSegment2 compatible with bezier-js's
20
42
  // interface
21
43
  /** Alias for `point1`. */
@@ -98,7 +120,10 @@ export class LineSegment2 extends Parameterized2DShape {
98
120
  // = ((o₁ᵧ - o₂ᵧ)((d₁ₓd₂ₓ)) + (d₂ᵧd₁ₓ)(o₂ₓ) - (d₁ᵧd₂ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ)
99
121
  // ⇒ y = o₁ᵧ + d₁ᵧ · (x - o₁ₓ) / d₁ₓ = ...
100
122
  let resultPoint, resultT;
101
- if (this.direction.x === 0) {
123
+ // Consider very-near-vertical lines to be vertical --- not doing so can lead to
124
+ // precision error when dividing by this.direction.x.
125
+ const small = 4e-13;
126
+ if (Math.abs(this.direction.x) < small) {
102
127
  // Vertical line: Where does the other have x = this.point1.x?
103
128
  // x = o₁ₓ = o₂ₓ + d₂ₓ · (y - o₂ᵧ) / d₂ᵧ
104
129
  // ⇒ (o₁ₓ - o₂ₓ)(d₂ᵧ/d₂ₓ) + o₂ᵧ = y
@@ -1,7 +1,12 @@
1
1
  import { Point2, Vec2 } from '../Vec2';
2
2
  import Abstract2DShape from './Abstract2DShape';
3
3
  import LineSegment2 from './LineSegment2';
4
- /** A 2-dimensional path with parameter interval $t \in [0, 1]$. */
4
+ /**
5
+ * A 2-dimensional path with parameter interval $t \in [0, 1]$.
6
+ *
7
+ * **Note:** Avoid extending this class outside of `js-draw` --- new abstract methods
8
+ * may be added between minor versions.
9
+ */
5
10
  export declare abstract class Parameterized2DShape extends Abstract2DShape {
6
11
  /** Returns this at a given parameter. $t \in [0, 1]$ */
7
12
  abstract at(t: number): Point2;
@@ -1,5 +1,10 @@
1
1
  import Abstract2DShape from './Abstract2DShape.mjs';
2
- /** A 2-dimensional path with parameter interval $t \in [0, 1]$. */
2
+ /**
3
+ * A 2-dimensional path with parameter interval $t \in [0, 1]$.
4
+ *
5
+ * **Note:** Avoid extending this class outside of `js-draw` --- new abstract methods
6
+ * may be added between minor versions.
7
+ */
3
8
  export class Parameterized2DShape extends Abstract2DShape {
4
9
  intersectsLineSegment(line) {
5
10
  return this.argIntersectsLineSegment(line).map(t => this.at(t));
@@ -32,11 +32,19 @@ export type PathCommand = CubicBezierPathCommand | QuadraticBezierPathCommand |
32
32
  export interface IntersectionResult {
33
33
  curve: Parameterized2DShape;
34
34
  curveIndex: number;
35
- /** Parameter value for the closest point **on** the path to the intersection. @internal @deprecated */
36
- parameterValue?: number;
35
+ /** Parameter value for the closest point **on** the path to the intersection. @internal */
36
+ parameterValue: number;
37
37
  /** Point at which the intersection occured. */
38
38
  point: Point2;
39
39
  }
40
+ /** Options for {@link Path.splitNear} and {@link Path.splitAt} */
41
+ export interface PathSplitOptions {
42
+ /**
43
+ * Allows mapping points on newly added segments. This is useful, for example,
44
+ * to round points to prevent long decimals when later saving.
45
+ */
46
+ mapNewPoint?: (point: Point2) => Point2;
47
+ }
40
48
  /**
41
49
  * Allows indexing a particular part of a path.
42
50
  *
@@ -46,8 +54,43 @@ export interface CurveIndexRecord {
46
54
  curveIndex: number;
47
55
  parameterValue: number;
48
56
  }
57
+ /** Returns a positive number if `a` comes after `b`, 0 if equal, and negative otherwise. */
58
+ export declare const compareCurveIndices: (a: CurveIndexRecord, b: CurveIndexRecord) => number;
59
+ /**
60
+ * Returns a version of `index` with its parameter value incremented by `stepBy`
61
+ * (which can be either positive or negative).
62
+ */
63
+ export declare const stepCurveIndexBy: (index: CurveIndexRecord, stepBy: number) => CurveIndexRecord;
49
64
  /**
50
65
  * Represents a union of lines and curves.
66
+ *
67
+ * To create a path from a string, see {@link fromString}.
68
+ *
69
+ * @example
70
+ * ```ts,runnable,console
71
+ * import {Path, Mat33, Vec2, LineSegment2} from '@js-draw/math';
72
+ *
73
+ * // Creates a path from an SVG path string.
74
+ * // In this case,
75
+ * // 1. Move to (0,0)
76
+ * // 2. Line to (100,0)
77
+ * const path = Path.fromString('M0,0 L100,0');
78
+ *
79
+ * // Logs the distance from (10,0) to the curve 1 unit
80
+ * // away from path. This curve forms a stroke with the path at
81
+ * // its center.
82
+ * const strokeRadius = 1;
83
+ * console.log(path.signedDistance(Vec2.of(10,0), strokeRadius));
84
+ *
85
+ * // Log a version of the path that's scaled by a factor of 4.
86
+ * console.log(path.transformedBy(Mat33.scaling2D(4)).toString());
87
+ *
88
+ * // Log all intersections of a stroked version of the path with
89
+ * // a vertical line segment.
90
+ * // (Try removing the `strokeRadius` parameter).
91
+ * const segment = new LineSegment2(Vec2.of(5, -100), Vec2.of(5, 100));
92
+ * console.log(path.intersection(segment, strokeRadius).map(i => i.point));
93
+ * ```
51
94
  */
52
95
  export declare class Path {
53
96
  readonly startPoint: Point2;
@@ -66,6 +109,12 @@ export declare class Path {
66
109
  * See also {@link fromString}
67
110
  */
68
111
  constructor(startPoint: Point2, parts: Readonly<PathCommand>[]);
112
+ /**
113
+ * Computes and returns the full bounding box for this path.
114
+ *
115
+ * If a slight over-estimate of a path's bounding box is sufficient, use
116
+ * {@link bbox} instead.
117
+ */
69
118
  getExactBBox(): Rect2;
70
119
  private cachedGeometry;
71
120
  get geometry(): Parameterized2DShape[];
@@ -79,7 +128,20 @@ export declare class Path {
79
128
  private cachedPolylineApproximation;
80
129
  polylineApproximation(): LineSegment2[];
81
130
  static computeBBoxForSegment(startPoint: Point2, part: PathCommand): Rect2;
82
- /** **Note**: `strokeRadius = strokeWidth / 2` */
131
+ /**
132
+ * Returns the signed distance between `point` and a curve `strokeRadius` units
133
+ * away from this path.
134
+ *
135
+ * This returns the **signed distance**, which means that points inside this shape
136
+ * have their distance negated. For example,
137
+ * ```ts,runnable,console
138
+ * import {Path, Vec2} from '@js-draw/math';
139
+ * console.log(Path.fromString('m0,0 L100,0').signedDistance(Vec2.zero, 1));
140
+ * ```
141
+ * would print `-1` because (0,0) is on `m0,0 L100,0` and thus one unit away from its boundary.
142
+ *
143
+ * **Note**: `strokeRadius = strokeWidth / 2`
144
+ */
83
145
  signedDistance(point: Point2, strokeRadius: number): number;
84
146
  /**
85
147
  * Let `S` be a closed path a distance `strokeRadius` from this path.
@@ -99,17 +161,33 @@ export declare class Path {
99
161
  intersection(line: LineSegment2, strokeRadius?: number): IntersectionResult[];
100
162
  /**
101
163
  * @returns the nearest point on this path to the given `point`.
102
- *
103
- * @internal
104
- * @beta
105
164
  */
106
165
  nearestPointTo(point: Point2): IntersectionResult;
107
166
  at(index: CurveIndexRecord): import("../Vec3").Vec3;
108
167
  tangentAt(index: CurveIndexRecord): import("../Vec3").Vec3;
168
+ /** Splits this path in two near the given `point`. */
169
+ splitNear(point: Point2, options?: PathSplitOptions): [Path] | [Path, Path];
170
+ /**
171
+ * Returns a copy of this path with `deleteFrom` until `deleteUntil` replaced with `insert`.
172
+ *
173
+ * This method is analogous to {@link Array.toSpliced}.
174
+ */
175
+ spliced(deleteFrom: CurveIndexRecord, deleteTo: CurveIndexRecord, insert: Path | undefined, options?: PathSplitOptions): Path;
176
+ splitAt(at: CurveIndexRecord, options?: PathSplitOptions): [Path] | [Path, Path];
177
+ splitAt(at: CurveIndexRecord[], options?: PathSplitOptions): Path[];
178
+ /**
179
+ * Replaces all `MoveTo` commands with `LineTo` commands and connects the end point of this
180
+ * path to the start point.
181
+ */
182
+ asClosed(): Path;
109
183
  private static mapPathCommand;
110
184
  mapPoints(mapping: (point: Point2) => Point2): Path;
111
185
  transformedBy(affineTransfm: Mat33): Path;
112
- union(other: Path | null, options?: {
186
+ /**
187
+ * @internal
188
+ */
189
+ closedContainsPoint(point: Point2): boolean;
190
+ union(other: Path | PathCommand[] | null, options?: {
113
191
  allowReverse?: boolean;
114
192
  }): Path;
115
193
  /**
@@ -122,7 +200,8 @@ export declare class Path {
122
200
  * ```
123
201
  */
124
202
  reversed(): Path;
125
- private getEndPoint;
203
+ /** Computes and returns the end point of this path */
204
+ getEndPoint(): import("../Vec3").Vec3;
126
205
  /**
127
206
  * Like {@link closedRoughlyIntersects} except takes stroke width into account.
128
207
  *
@@ -134,6 +213,12 @@ export declare class Path {
134
213
  * `strokeRadius` is half of `strokeWidth`.
135
214
  */
136
215
  roughlyIntersects(rect: Rect2, strokeWidth?: number): boolean;
216
+ /**
217
+ * Treats this as a closed path and returns true if part of `rect` is *roughly* within
218
+ * this path's interior.
219
+ *
220
+ * **Note**: Assumes that this is a closed, non-self-intersecting path.
221
+ */
137
222
  closedRoughlyIntersects(rect: Rect2): boolean;
138
223
  /** @returns true if all points on this are equivalent to the points on `other` */
139
224
  eq(other: Path, tolerance?: number): boolean;
@@ -160,10 +245,8 @@ export declare class Path {
160
245
  /**
161
246
  * Create a `Path` from a subset of the SVG path specification.
162
247
  *
163
- * ## To-do
164
- * - TODO: Support a larger subset of SVG paths
165
- * - Elliptical arcs are currently unsupported.
166
- * - TODO: Support `s`,`t` commands shorthands.
248
+ * Currently, this does not support elliptical arcs or `s` and `t` command
249
+ * shorthands. See https://github.com/personalizedrefrigerator/js-draw/pull/19.
167
250
  *
168
251
  * @example
169
252
  * ```ts,runnable,console
@@ -174,6 +257,7 @@ export declare class Path {
174
257
  * ```
175
258
  */
176
259
  static fromString(pathString: string): Path;
260
+ static fromConvexHullOf(points: Point2[]): Path;
177
261
  static empty: Path;
178
262
  }
179
263
  export default Path;