@js-draw/math 1.17.0 → 1.18.0

Sign up to get free protection for your applications and to get access to all the features.
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;