@js-draw/math 1.11.1 → 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. package/dist/cjs/Vec3.d.ts +21 -0
  2. package/dist/cjs/Vec3.js +28 -0
  3. package/dist/cjs/lib.d.ts +2 -2
  4. package/dist/cjs/lib.js +16 -3
  5. package/dist/cjs/rounding/cleanUpNumber.d.ts +3 -0
  6. package/dist/cjs/rounding/cleanUpNumber.js +35 -0
  7. package/dist/cjs/rounding/constants.d.ts +1 -0
  8. package/dist/cjs/rounding/constants.js +4 -0
  9. package/dist/cjs/rounding/getLenAfterDecimal.d.ts +10 -0
  10. package/dist/cjs/rounding/getLenAfterDecimal.js +30 -0
  11. package/dist/cjs/rounding/lib.d.ts +1 -0
  12. package/dist/cjs/rounding/lib.js +5 -0
  13. package/dist/cjs/{rounding.d.ts → rounding/toRoundedString.d.ts} +1 -3
  14. package/dist/cjs/rounding/toRoundedString.js +54 -0
  15. package/dist/cjs/rounding/toStringOfSamePrecision.d.ts +2 -0
  16. package/dist/cjs/rounding/toStringOfSamePrecision.js +58 -0
  17. package/dist/cjs/rounding/toStringOfSamePrecision.test.d.ts +1 -0
  18. package/dist/cjs/shapes/Abstract2DShape.d.ts +3 -0
  19. package/dist/cjs/shapes/BezierJSWrapper.d.ts +15 -5
  20. package/dist/cjs/shapes/BezierJSWrapper.js +135 -18
  21. package/dist/cjs/shapes/LineSegment2.d.ts +34 -5
  22. package/dist/cjs/shapes/LineSegment2.js +63 -10
  23. package/dist/cjs/shapes/Parameterized2DShape.d.ts +31 -0
  24. package/dist/cjs/shapes/Parameterized2DShape.js +15 -0
  25. package/dist/cjs/shapes/Path.d.ts +40 -6
  26. package/dist/cjs/shapes/Path.js +181 -22
  27. package/dist/cjs/shapes/PointShape2D.d.ts +14 -3
  28. package/dist/cjs/shapes/PointShape2D.js +28 -5
  29. package/dist/cjs/shapes/QuadraticBezier.d.ts +4 -0
  30. package/dist/cjs/shapes/QuadraticBezier.js +19 -4
  31. package/dist/cjs/shapes/Rect2.d.ts +3 -0
  32. package/dist/cjs/shapes/Rect2.js +4 -1
  33. package/dist/mjs/Vec3.d.ts +21 -0
  34. package/dist/mjs/Vec3.mjs +28 -0
  35. package/dist/mjs/lib.d.ts +2 -2
  36. package/dist/mjs/lib.mjs +1 -1
  37. package/dist/mjs/rounding/cleanUpNumber.d.ts +3 -0
  38. package/dist/mjs/rounding/cleanUpNumber.mjs +31 -0
  39. package/dist/mjs/rounding/cleanUpNumber.test.d.ts +1 -0
  40. package/dist/mjs/rounding/constants.d.ts +1 -0
  41. package/dist/mjs/rounding/constants.mjs +1 -0
  42. package/dist/mjs/rounding/getLenAfterDecimal.d.ts +10 -0
  43. package/dist/mjs/rounding/getLenAfterDecimal.mjs +26 -0
  44. package/dist/mjs/rounding/lib.d.ts +1 -0
  45. package/dist/mjs/rounding/lib.mjs +1 -0
  46. package/dist/mjs/{rounding.d.ts → rounding/toRoundedString.d.ts} +1 -3
  47. package/dist/mjs/rounding/toRoundedString.mjs +47 -0
  48. package/dist/mjs/rounding/toRoundedString.test.d.ts +1 -0
  49. package/dist/mjs/rounding/toStringOfSamePrecision.d.ts +2 -0
  50. package/dist/mjs/rounding/toStringOfSamePrecision.mjs +51 -0
  51. package/dist/mjs/rounding/toStringOfSamePrecision.test.d.ts +1 -0
  52. package/dist/mjs/shapes/Abstract2DShape.d.ts +3 -0
  53. package/dist/mjs/shapes/BezierJSWrapper.d.ts +15 -5
  54. package/dist/mjs/shapes/BezierJSWrapper.mjs +133 -18
  55. package/dist/mjs/shapes/LineSegment2.d.ts +34 -5
  56. package/dist/mjs/shapes/LineSegment2.mjs +63 -10
  57. package/dist/mjs/shapes/Parameterized2DShape.d.ts +31 -0
  58. package/dist/mjs/shapes/Parameterized2DShape.mjs +8 -0
  59. package/dist/mjs/shapes/Path.d.ts +40 -6
  60. package/dist/mjs/shapes/Path.mjs +175 -16
  61. package/dist/mjs/shapes/PointShape2D.d.ts +14 -3
  62. package/dist/mjs/shapes/PointShape2D.mjs +28 -5
  63. package/dist/mjs/shapes/QuadraticBezier.d.ts +4 -0
  64. package/dist/mjs/shapes/QuadraticBezier.mjs +19 -4
  65. package/dist/mjs/shapes/Rect2.d.ts +3 -0
  66. package/dist/mjs/shapes/Rect2.mjs +4 -1
  67. package/package.json +5 -5
  68. package/src/Vec3.test.ts +26 -7
  69. package/src/Vec3.ts +30 -0
  70. package/src/lib.ts +3 -1
  71. package/src/rounding/cleanUpNumber.test.ts +15 -0
  72. package/src/rounding/cleanUpNumber.ts +38 -0
  73. package/src/rounding/constants.ts +3 -0
  74. package/src/rounding/getLenAfterDecimal.ts +29 -0
  75. package/src/rounding/lib.ts +2 -0
  76. package/src/rounding/toRoundedString.test.ts +32 -0
  77. package/src/rounding/toRoundedString.ts +57 -0
  78. package/src/rounding/toStringOfSamePrecision.test.ts +21 -0
  79. package/src/rounding/toStringOfSamePrecision.ts +63 -0
  80. package/src/shapes/Abstract2DShape.ts +3 -0
  81. package/src/shapes/BezierJSWrapper.ts +154 -14
  82. package/src/shapes/LineSegment2.test.ts +35 -1
  83. package/src/shapes/LineSegment2.ts +79 -11
  84. package/src/shapes/Parameterized2DShape.ts +39 -0
  85. package/src/shapes/Path.test.ts +63 -3
  86. package/src/shapes/Path.ts +211 -26
  87. package/src/shapes/PointShape2D.ts +33 -6
  88. package/src/shapes/QuadraticBezier.test.ts +48 -12
  89. package/src/shapes/QuadraticBezier.ts +23 -5
  90. package/src/shapes/Rect2.ts +4 -1
  91. package/dist/cjs/rounding.js +0 -146
  92. package/dist/mjs/rounding.mjs +0 -139
  93. package/src/rounding.test.ts +0 -65
  94. package/src/rounding.ts +0 -168
  95. /package/dist/cjs/{rounding.test.d.ts → rounding/cleanUpNumber.test.d.ts} +0 -0
  96. /package/dist/{mjs/rounding.test.d.ts → cjs/rounding/toRoundedString.test.d.ts} +0 -0
@@ -1,13 +1,14 @@
1
1
  import Mat33 from '../Mat33';
2
2
  import Rect2 from './Rect2';
3
3
  import { Vec2, Point2 } from '../Vec2';
4
- import Abstract2DShape from './Abstract2DShape';
4
+ import Parameterized2DShape from './Parameterized2DShape';
5
+ import Vec3 from '../Vec3';
5
6
  interface IntersectionResult {
6
7
  point: Point2;
7
8
  t: number;
8
9
  }
9
10
  /** Represents a line segment. A `LineSegment2` is immutable. */
10
- export declare class LineSegment2 extends Abstract2DShape {
11
+ export declare class LineSegment2 extends Parameterized2DShape {
11
12
  private readonly point1;
12
13
  private readonly point2;
13
14
  /**
@@ -28,8 +29,9 @@ export declare class LineSegment2 extends Abstract2DShape {
28
29
  get p1(): Point2;
29
30
  /** Alias for `point2`. */
30
31
  get p2(): Point2;
32
+ get center(): Point2;
31
33
  /**
32
- * Gets a point a distance `t` along this line.
34
+ * Gets a point a **distance** `t` along this line.
33
35
  *
34
36
  * @deprecated
35
37
  */
@@ -42,8 +44,20 @@ export declare class LineSegment2 extends Abstract2DShape {
42
44
  * `t` should be in `[0, 1]`.
43
45
  */
44
46
  at(t: number): Point2;
47
+ normalAt(_t: number): Vec2;
48
+ tangentAt(_t: number): Vec3;
49
+ splitAt(t: number): [LineSegment2] | [LineSegment2, LineSegment2];
50
+ /**
51
+ * Returns the intersection of this with another line segment.
52
+ *
53
+ * **WARNING**: The parameter value returned by this method does not range from 0 to 1 and
54
+ * is currently a length.
55
+ * This will change in a future release.
56
+ * @deprecated
57
+ */
45
58
  intersection(other: LineSegment2): IntersectionResult | null;
46
59
  intersects(other: LineSegment2): boolean;
60
+ argIntersectsLineSegment(lineSegment: LineSegment2): number[];
47
61
  /**
48
62
  * Returns the points at which this line segment intersects the
49
63
  * given line segment.
@@ -52,8 +66,12 @@ export declare class LineSegment2 extends Abstract2DShape {
52
66
  * line segment. This method, by contrast, returns **the point** at which the intersection
53
67
  * occurs, if such a point exists.
54
68
  */
55
- intersectsLineSegment(lineSegment: LineSegment2): import("../Vec3").Vec3[];
56
- closestPointTo(target: Point2): import("../Vec3").Vec3;
69
+ intersectsLineSegment(lineSegment: LineSegment2): Vec3[];
70
+ closestPointTo(target: Point2): Vec3;
71
+ nearestPointTo(target: Vec3): {
72
+ point: Vec3;
73
+ parameterValue: number;
74
+ };
57
75
  /**
58
76
  * Returns the distance from this line segment to `target`.
59
77
  *
@@ -66,5 +84,16 @@ export declare class LineSegment2 extends Abstract2DShape {
66
84
  /** @inheritdoc */
67
85
  getTightBoundingBox(): Rect2;
68
86
  toString(): string;
87
+ /**
88
+ * Returns `true` iff this is equivalent to `other`.
89
+ *
90
+ * **Options**:
91
+ * - `tolerance`: The maximum difference between endpoints. (Default: 0)
92
+ * - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
93
+ */
94
+ eq(other: LineSegment2, options?: {
95
+ tolerance?: number;
96
+ ignoreDirection?: boolean;
97
+ }): boolean;
69
98
  }
70
99
  export default LineSegment2;
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.LineSegment2 = void 0;
7
7
  const Rect2_1 = __importDefault(require("./Rect2"));
8
8
  const Vec2_1 = require("../Vec2");
9
- const Abstract2DShape_1 = __importDefault(require("./Abstract2DShape"));
9
+ const Parameterized2DShape_1 = __importDefault(require("./Parameterized2DShape"));
10
10
  /** Represents a line segment. A `LineSegment2` is immutable. */
11
- class LineSegment2 extends Abstract2DShape_1.default {
11
+ class LineSegment2 extends Parameterized2DShape_1.default {
12
12
  /** Creates a new `LineSegment2` from its endpoints. */
13
13
  constructor(point1, point2) {
14
14
  super();
@@ -32,8 +32,11 @@ class LineSegment2 extends Abstract2DShape_1.default {
32
32
  get p2() {
33
33
  return this.point2;
34
34
  }
35
+ get center() {
36
+ return this.point1.lerp(this.point2, 0.5);
37
+ }
35
38
  /**
36
- * Gets a point a distance `t` along this line.
39
+ * Gets a point a **distance** `t` along this line.
37
40
  *
38
41
  * @deprecated
39
42
  */
@@ -50,7 +53,31 @@ class LineSegment2 extends Abstract2DShape_1.default {
50
53
  at(t) {
51
54
  return this.get(t * this.length);
52
55
  }
56
+ normalAt(_t) {
57
+ return this.direction.orthog();
58
+ }
59
+ tangentAt(_t) {
60
+ return this.direction;
61
+ }
62
+ splitAt(t) {
63
+ if (t <= 0 || t >= 1) {
64
+ return [this];
65
+ }
66
+ return [
67
+ new LineSegment2(this.point1, this.at(t)),
68
+ new LineSegment2(this.at(t), this.point2),
69
+ ];
70
+ }
71
+ /**
72
+ * Returns the intersection of this with another line segment.
73
+ *
74
+ * **WARNING**: The parameter value returned by this method does not range from 0 to 1 and
75
+ * is currently a length.
76
+ * This will change in a future release.
77
+ * @deprecated
78
+ */
53
79
  intersection(other) {
80
+ // TODO(v2.0.0): Make this return a `t` value from `0` to `1`.
54
81
  // We want x₁(t) = x₂(t) and y₁(t) = y₂(t)
55
82
  // Observe that
56
83
  // x = this.point1.x + this.direction.x · t₁
@@ -109,10 +136,10 @@ class LineSegment2 extends Abstract2DShape_1.default {
109
136
  resultT = (xIntersect - this.point1.x) / this.direction.x;
110
137
  }
111
138
  // Ensure the result is in this/the other segment.
112
- const resultToP1 = resultPoint.minus(this.point1).magnitude();
113
- const resultToP2 = resultPoint.minus(this.point2).magnitude();
114
- const resultToP3 = resultPoint.minus(other.point1).magnitude();
115
- const resultToP4 = resultPoint.minus(other.point2).magnitude();
139
+ const resultToP1 = resultPoint.distanceTo(this.point1);
140
+ const resultToP2 = resultPoint.distanceTo(this.point2);
141
+ const resultToP3 = resultPoint.distanceTo(other.point1);
142
+ const resultToP4 = resultPoint.distanceTo(other.point2);
116
143
  if (resultToP1 > this.length
117
144
  || resultToP2 > this.length
118
145
  || resultToP3 > other.length
@@ -127,6 +154,13 @@ class LineSegment2 extends Abstract2DShape_1.default {
127
154
  intersects(other) {
128
155
  return this.intersection(other) !== null;
129
156
  }
157
+ argIntersectsLineSegment(lineSegment) {
158
+ const intersection = this.intersection(lineSegment);
159
+ if (intersection) {
160
+ return [intersection.t / this.length];
161
+ }
162
+ return [];
163
+ }
130
164
  /**
131
165
  * Returns the points at which this line segment intersects the
132
166
  * given line segment.
@@ -144,18 +178,21 @@ class LineSegment2 extends Abstract2DShape_1.default {
144
178
  }
145
179
  // Returns the closest point on this to [target]
146
180
  closestPointTo(target) {
181
+ return this.nearestPointTo(target).point;
182
+ }
183
+ nearestPointTo(target) {
147
184
  // Distance from P1 along this' direction.
148
185
  const projectedDistFromP1 = target.minus(this.p1).dot(this.direction);
149
186
  const projectedDistFromP2 = this.length - projectedDistFromP1;
150
187
  const projection = this.p1.plus(this.direction.times(projectedDistFromP1));
151
188
  if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) {
152
- return projection;
189
+ return { point: projection, parameterValue: projectedDistFromP1 / this.length };
153
190
  }
154
191
  if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) {
155
- return this.p2;
192
+ return { point: this.p2, parameterValue: 1 };
156
193
  }
157
194
  else {
158
- return this.p1;
195
+ return { point: this.p1, parameterValue: 0 };
159
196
  }
160
197
  }
161
198
  /**
@@ -178,6 +215,22 @@ class LineSegment2 extends Abstract2DShape_1.default {
178
215
  toString() {
179
216
  return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
180
217
  }
218
+ /**
219
+ * Returns `true` iff this is equivalent to `other`.
220
+ *
221
+ * **Options**:
222
+ * - `tolerance`: The maximum difference between endpoints. (Default: 0)
223
+ * - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`)
224
+ */
225
+ eq(other, options) {
226
+ if (!(other instanceof LineSegment2)) {
227
+ return false;
228
+ }
229
+ const tolerance = options?.tolerance;
230
+ const ignoreDirection = options?.ignoreDirection ?? true;
231
+ return ((other.p1.eq(this.p1, tolerance) && other.p2.eq(this.p2, tolerance))
232
+ || (ignoreDirection && other.p1.eq(this.p2, tolerance) && other.p2.eq(this.p1, tolerance)));
233
+ }
181
234
  }
182
235
  exports.LineSegment2 = LineSegment2;
183
236
  exports.default = LineSegment2;
@@ -0,0 +1,31 @@
1
+ import { Point2, Vec2 } from '../Vec2';
2
+ import Abstract2DShape from './Abstract2DShape';
3
+ import LineSegment2 from './LineSegment2';
4
+ /** A 2-dimensional path with parameter interval $t \in [0, 1]$. */
5
+ export declare abstract class Parameterized2DShape extends Abstract2DShape {
6
+ /** Returns this at a given parameter. $t \in [0, 1]$ */
7
+ abstract at(t: number): Point2;
8
+ /** Computes the unit normal vector at $t$. */
9
+ abstract normalAt(t: number): Vec2;
10
+ abstract tangentAt(t: number): Vec2;
11
+ /**
12
+ * Divides this shape into two separate shapes at parameter value $t$.
13
+ */
14
+ abstract splitAt(t: number): [Parameterized2DShape] | [Parameterized2DShape, Parameterized2DShape];
15
+ /**
16
+ * Returns the nearest point on `this` to `point` and the `parameterValue` at which
17
+ * that point occurs.
18
+ */
19
+ abstract nearestPointTo(point: Point2): {
20
+ point: Point2;
21
+ parameterValue: number;
22
+ };
23
+ /**
24
+ * Returns the **parameter values** at which `lineSegment` intersects this shape.
25
+ *
26
+ * See also {@link intersectsLineSegment}
27
+ */
28
+ abstract argIntersectsLineSegment(lineSegment: LineSegment2): number[];
29
+ intersectsLineSegment(line: LineSegment2): Point2[];
30
+ }
31
+ export default Parameterized2DShape;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Parameterized2DShape = void 0;
7
+ const Abstract2DShape_1 = __importDefault(require("./Abstract2DShape"));
8
+ /** A 2-dimensional path with parameter interval $t \in [0, 1]$. */
9
+ class Parameterized2DShape extends Abstract2DShape_1.default {
10
+ intersectsLineSegment(line) {
11
+ return this.argIntersectsLineSegment(line).map(t => this.at(t));
12
+ }
13
+ }
14
+ exports.Parameterized2DShape = Parameterized2DShape;
15
+ exports.default = Parameterized2DShape;
@@ -2,7 +2,7 @@ import LineSegment2 from './LineSegment2';
2
2
  import Mat33 from '../Mat33';
3
3
  import Rect2 from './Rect2';
4
4
  import { Point2 } from '../Vec2';
5
- import Abstract2DShape from './Abstract2DShape';
5
+ import Parameterized2DShape from './Parameterized2DShape';
6
6
  export declare enum PathCommandType {
7
7
  LineTo = 0,
8
8
  MoveTo = 1,
@@ -29,12 +29,23 @@ export interface MoveToPathCommand {
29
29
  point: Point2;
30
30
  }
31
31
  export type PathCommand = CubicBezierPathCommand | QuadraticBezierPathCommand | MoveToPathCommand | LinePathCommand;
32
- interface IntersectionResult {
33
- curve: Abstract2DShape;
34
- /** @internal @deprecated */
32
+ export interface IntersectionResult {
33
+ curve: Parameterized2DShape;
34
+ curveIndex: number;
35
+ /** Parameter value for the closest point **on** the path to the intersection. @internal @deprecated */
35
36
  parameterValue?: number;
37
+ /** Point at which the intersection occured. */
36
38
  point: Point2;
37
39
  }
40
+ /**
41
+ * Allows indexing a particular part of a path.
42
+ *
43
+ * @see {@link Path.at} {@link Path.tangentAt}
44
+ */
45
+ export interface CurveIndexRecord {
46
+ curveIndex: number;
47
+ parameterValue: number;
48
+ }
38
49
  /**
39
50
  * Represents a union of lines and curves.
40
51
  */
@@ -57,7 +68,7 @@ export declare class Path {
57
68
  constructor(startPoint: Point2, parts: Readonly<PathCommand>[]);
58
69
  getExactBBox(): Rect2;
59
70
  private cachedGeometry;
60
- get geometry(): Abstract2DShape[];
71
+ get geometry(): Parameterized2DShape[];
61
72
  /**
62
73
  * Iterates through the start/end points of each component in this path.
63
74
  *
@@ -86,10 +97,31 @@ export declare class Path {
86
97
  * **Note**: `strokeRadius` is half of a stroke's width.
87
98
  */
88
99
  intersection(line: LineSegment2, strokeRadius?: number): IntersectionResult[];
100
+ /**
101
+ * @returns the nearest point on this path to the given `point`.
102
+ *
103
+ * @internal
104
+ * @beta
105
+ */
106
+ nearestPointTo(point: Point2): IntersectionResult;
107
+ at(index: CurveIndexRecord): import("../Vec3").Vec3;
108
+ tangentAt(index: CurveIndexRecord): import("../Vec3").Vec3;
89
109
  private static mapPathCommand;
90
110
  mapPoints(mapping: (point: Point2) => Point2): Path;
91
111
  transformedBy(affineTransfm: Mat33): Path;
92
- union(other: Path | null): Path;
112
+ union(other: Path | null, options?: {
113
+ allowReverse?: boolean;
114
+ }): Path;
115
+ /**
116
+ * @returns a version of this path with the direction reversed.
117
+ *
118
+ * Example:
119
+ * ```ts,runnable,console
120
+ * import {Path} from '@js-draw/math';
121
+ * console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
122
+ * ```
123
+ */
124
+ reversed(): Path;
93
125
  private getEndPoint;
94
126
  /**
95
127
  * Like {@link closedRoughlyIntersects} except takes stroke width into account.
@@ -103,6 +135,8 @@ export declare class Path {
103
135
  */
104
136
  roughlyIntersects(rect: Rect2, strokeWidth?: number): boolean;
105
137
  closedRoughlyIntersects(rect: Rect2): boolean;
138
+ /** @returns true if all points on this are equivalent to the points on `other` */
139
+ eq(other: Path, tolerance?: number): boolean;
106
140
  /**
107
141
  * Returns a path that outlines `rect`.
108
142
  *
@@ -4,13 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Path = exports.PathCommandType = void 0;
7
- const rounding_1 = require("../rounding");
8
7
  const LineSegment2_1 = __importDefault(require("./LineSegment2"));
9
8
  const Rect2_1 = __importDefault(require("./Rect2"));
10
9
  const Vec2_1 = require("../Vec2");
11
10
  const CubicBezier_1 = __importDefault(require("./CubicBezier"));
12
11
  const QuadraticBezier_1 = __importDefault(require("./QuadraticBezier"));
13
12
  const PointShape2D_1 = __importDefault(require("./PointShape2D"));
13
+ const toRoundedString_1 = __importDefault(require("../rounding/toRoundedString"));
14
+ const toStringOfSamePrecision_1 = __importDefault(require("../rounding/toStringOfSamePrecision"));
14
15
  var PathCommandType;
15
16
  (function (PathCommandType) {
16
17
  PathCommandType[PathCommandType["LineTo"] = 0] = "LineTo";
@@ -235,7 +236,7 @@ class Path {
235
236
  for (const { part, distFn, bbox } of uncheckedDistFunctions) {
236
237
  // Skip if impossible for the distance to the target to be lesser than
237
238
  // the current minimum.
238
- if (!bbox.grownBy(minDist).containsPoint(point)) {
239
+ if (isFinite(minDist) && !bbox.grownBy(minDist).containsPoint(point)) {
239
240
  continue;
240
241
  }
241
242
  const currentDist = distFn(point);
@@ -273,7 +274,7 @@ class Path {
273
274
  });
274
275
  const result = [];
275
276
  const stoppingThreshold = strokeRadius / 1000;
276
- // Returns the maximum x value explored
277
+ // Returns the maximum parameter value explored
277
278
  const raymarchFrom = (startPoint,
278
279
  // Direction to march in (multiplies line.direction)
279
280
  directionMultiplier,
@@ -317,9 +318,14 @@ class Path {
317
318
  if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
318
319
  result.push({
319
320
  point: currentPoint,
320
- parameterValue: NaN,
321
+ parameterValue: NaN, // lastPart.nearestPointTo(currentPoint).parameterValue,
321
322
  curve: lastPart,
323
+ curveIndex: this.geometry.indexOf(lastPart),
322
324
  });
325
+ // Slightly increase the parameter value to prevent the same point from being
326
+ // added to the results twice.
327
+ const parameterIncrease = strokeRadius / 20 / line.length;
328
+ lastParameter += isFinite(parameterIncrease) ? parameterIncrease : 0;
323
329
  }
324
330
  return lastParameter;
325
331
  };
@@ -352,14 +358,18 @@ class Path {
352
358
  if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
353
359
  return [];
354
360
  }
361
+ let index = 0;
355
362
  for (const part of this.geometry) {
356
- const intersection = part.intersectsLineSegment(line);
357
- if (intersection.length > 0) {
363
+ const intersections = part.argIntersectsLineSegment(line);
364
+ for (const intersection of intersections) {
358
365
  result.push({
359
366
  curve: part,
360
- point: intersection[0],
367
+ curveIndex: index,
368
+ point: part.at(intersection),
369
+ parameterValue: intersection,
361
370
  });
362
371
  }
372
+ index++;
363
373
  }
364
374
  // If given a non-zero strokeWidth, attempt to raymarch.
365
375
  // Even if raymarching, we need to collect starting points.
@@ -372,6 +382,42 @@ class Path {
372
382
  }
373
383
  return result;
374
384
  }
385
+ /**
386
+ * @returns the nearest point on this path to the given `point`.
387
+ *
388
+ * @internal
389
+ * @beta
390
+ */
391
+ nearestPointTo(point) {
392
+ // Find the closest point on this
393
+ let closestSquareDist = Infinity;
394
+ let closestPartIndex = 0;
395
+ let closestParameterValue = 0;
396
+ let closestPoint = this.startPoint;
397
+ for (let i = 0; i < this.geometry.length; i++) {
398
+ const current = this.geometry[i];
399
+ const nearestPoint = current.nearestPointTo(point);
400
+ const sqareDist = nearestPoint.point.squareDistanceTo(point);
401
+ if (i === 0 || sqareDist < closestSquareDist) {
402
+ closestPartIndex = i;
403
+ closestSquareDist = sqareDist;
404
+ closestParameterValue = nearestPoint.parameterValue;
405
+ closestPoint = nearestPoint.point;
406
+ }
407
+ }
408
+ return {
409
+ curve: this.geometry[closestPartIndex],
410
+ curveIndex: closestPartIndex,
411
+ parameterValue: closestParameterValue,
412
+ point: closestPoint,
413
+ };
414
+ }
415
+ at(index) {
416
+ return this.geometry[index.curveIndex].at(index.parameterValue);
417
+ }
418
+ tangentAt(index) {
419
+ return this.geometry[index.curveIndex].tangentAt(index.parameterValue);
420
+ }
375
421
  static mapPathCommand(part, mapping) {
376
422
  switch (part.kind) {
377
423
  case PathCommandType.MoveTo:
@@ -415,18 +461,85 @@ class Path {
415
461
  return this.mapPoints(point => affineTransfm.transformVec2(point));
416
462
  }
417
463
  // Creates a new path by joining [other] to the end of this path
418
- union(other) {
464
+ union(other,
465
+ // allowReverse: true iff reversing other or this is permitted if it means
466
+ // no moveTo command is necessary when unioning the paths.
467
+ options = { allowReverse: true }) {
419
468
  if (!other) {
420
469
  return this;
421
470
  }
422
- return new Path(this.startPoint, [
423
- ...this.parts,
424
- {
425
- kind: PathCommandType.MoveTo,
426
- point: other.startPoint,
427
- },
428
- ...other.parts,
429
- ]);
471
+ const thisEnd = this.getEndPoint();
472
+ let newParts = [];
473
+ if (thisEnd.eq(other.startPoint)) {
474
+ newParts = this.parts.concat(other.parts);
475
+ }
476
+ else if (options.allowReverse && this.startPoint.eq(other.getEndPoint())) {
477
+ return other.union(this, { allowReverse: false });
478
+ }
479
+ else if (options.allowReverse && this.startPoint.eq(other.startPoint)) {
480
+ return this.union(other.reversed(), { allowReverse: false });
481
+ }
482
+ else {
483
+ newParts = [
484
+ ...this.parts,
485
+ {
486
+ kind: PathCommandType.MoveTo,
487
+ point: other.startPoint,
488
+ },
489
+ ...other.parts,
490
+ ];
491
+ }
492
+ return new Path(this.startPoint, newParts);
493
+ }
494
+ /**
495
+ * @returns a version of this path with the direction reversed.
496
+ *
497
+ * Example:
498
+ * ```ts,runnable,console
499
+ * import {Path} from '@js-draw/math';
500
+ * console.log(Path.fromString('m0,0l1,1').reversed()); // -> M1,1 L0,0
501
+ * ```
502
+ */
503
+ reversed() {
504
+ const newStart = this.getEndPoint();
505
+ const newParts = [];
506
+ let lastPoint = this.startPoint;
507
+ for (const part of this.parts) {
508
+ switch (part.kind) {
509
+ case PathCommandType.LineTo:
510
+ case PathCommandType.MoveTo:
511
+ newParts.push({
512
+ kind: part.kind,
513
+ point: lastPoint,
514
+ });
515
+ lastPoint = part.point;
516
+ break;
517
+ case PathCommandType.CubicBezierTo:
518
+ newParts.push({
519
+ kind: part.kind,
520
+ controlPoint1: part.controlPoint2,
521
+ controlPoint2: part.controlPoint1,
522
+ endPoint: lastPoint,
523
+ });
524
+ lastPoint = part.endPoint;
525
+ break;
526
+ case PathCommandType.QuadraticBezierTo:
527
+ newParts.push({
528
+ kind: part.kind,
529
+ controlPoint: part.controlPoint,
530
+ endPoint: lastPoint,
531
+ });
532
+ lastPoint = part.endPoint;
533
+ break;
534
+ default:
535
+ {
536
+ const exhaustivenessCheck = part;
537
+ return exhaustivenessCheck;
538
+ }
539
+ }
540
+ }
541
+ newParts.reverse();
542
+ return new Path(newStart, newParts);
430
543
  }
431
544
  getEndPoint() {
432
545
  if (this.parts.length === 0) {
@@ -518,6 +631,52 @@ class Path {
518
631
  // Even? Probably no intersection.
519
632
  return false;
520
633
  }
634
+ /** @returns true if all points on this are equivalent to the points on `other` */
635
+ eq(other, tolerance) {
636
+ if (other.parts.length !== this.parts.length) {
637
+ return false;
638
+ }
639
+ for (let i = 0; i < this.parts.length; i++) {
640
+ const part1 = this.parts[i];
641
+ const part2 = other.parts[i];
642
+ switch (part1.kind) {
643
+ case PathCommandType.LineTo:
644
+ case PathCommandType.MoveTo:
645
+ if (part1.kind !== part2.kind) {
646
+ return false;
647
+ }
648
+ else if (!part1.point.eq(part2.point, tolerance)) {
649
+ return false;
650
+ }
651
+ break;
652
+ case PathCommandType.CubicBezierTo:
653
+ if (part1.kind !== part2.kind) {
654
+ return false;
655
+ }
656
+ else if (!part1.controlPoint1.eq(part2.controlPoint1, tolerance)
657
+ || !part1.controlPoint2.eq(part2.controlPoint2, tolerance)
658
+ || !part1.endPoint.eq(part2.endPoint, tolerance)) {
659
+ return false;
660
+ }
661
+ break;
662
+ case PathCommandType.QuadraticBezierTo:
663
+ if (part1.kind !== part2.kind) {
664
+ return false;
665
+ }
666
+ else if (!part1.controlPoint.eq(part2.controlPoint, tolerance)
667
+ || !part1.endPoint.eq(part2.endPoint, tolerance)) {
668
+ return false;
669
+ }
670
+ break;
671
+ default:
672
+ {
673
+ const exhaustivenessCheck = part1;
674
+ return exhaustivenessCheck;
675
+ }
676
+ }
677
+ }
678
+ return true;
679
+ }
521
680
  /**
522
681
  * Returns a path that outlines `rect`.
523
682
  *
@@ -591,15 +750,15 @@ class Path {
591
750
  const absoluteCommandParts = [];
592
751
  const relativeCommandParts = [];
593
752
  const makeAbsCommand = !prevPoint || onlyAbsCommands;
594
- const roundedPrevX = prevPoint ? (0, rounding_1.toRoundedString)(prevPoint.x) : '';
595
- const roundedPrevY = prevPoint ? (0, rounding_1.toRoundedString)(prevPoint.y) : '';
753
+ const roundedPrevX = prevPoint ? (0, toRoundedString_1.default)(prevPoint.x) : '';
754
+ const roundedPrevY = prevPoint ? (0, toRoundedString_1.default)(prevPoint.y) : '';
596
755
  for (const point of points) {
597
- const xComponent = (0, rounding_1.toRoundedString)(point.x);
598
- const yComponent = (0, rounding_1.toRoundedString)(point.y);
756
+ const xComponent = (0, toRoundedString_1.default)(point.x);
757
+ const yComponent = (0, toRoundedString_1.default)(point.y);
599
758
  // Relative commands are often shorter as strings than absolute commands.
600
759
  if (!makeAbsCommand) {
601
- const xComponentRelative = (0, rounding_1.toStringOfSamePrecision)(point.x - prevPoint.x, xComponent, roundedPrevX, roundedPrevY);
602
- const yComponentRelative = (0, rounding_1.toStringOfSamePrecision)(point.y - prevPoint.y, yComponent, roundedPrevX, roundedPrevY);
760
+ const xComponentRelative = (0, toStringOfSamePrecision_1.default)(point.x - prevPoint.x, xComponent, roundedPrevX, roundedPrevY);
761
+ const yComponentRelative = (0, toStringOfSamePrecision_1.default)(point.y - prevPoint.y, yComponent, roundedPrevX, roundedPrevY);
603
762
  // No need for an additional separator if it starts with a '-'
604
763
  if (yComponentRelative.charAt(0) === '-') {
605
764
  relativeCommandParts.push(`${xComponentRelative}${yComponentRelative}`);
@@ -1,18 +1,29 @@
1
1
  import { Point2 } from '../Vec2';
2
2
  import Vec3 from '../Vec3';
3
- import Abstract2DShape from './Abstract2DShape';
4
3
  import LineSegment2 from './LineSegment2';
4
+ import Parameterized2DShape from './Parameterized2DShape';
5
5
  import Rect2 from './Rect2';
6
6
  /**
7
7
  * Like a {@link Point2}, but with additional functionality (e.g. SDF).
8
8
  *
9
9
  * Access the internal `Point2` using the `p` property.
10
10
  */
11
- declare class PointShape2D extends Abstract2DShape {
11
+ declare class PointShape2D extends Parameterized2DShape {
12
12
  readonly p: Point2;
13
13
  constructor(p: Point2);
14
14
  signedDistance(point: Vec3): number;
15
- intersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): Vec3[];
15
+ argIntersectsLineSegment(lineSegment: LineSegment2, epsilon?: number): number[];
16
16
  getTightBoundingBox(): Rect2;
17
+ at(_t: number): Vec3;
18
+ /**
19
+ * Returns an arbitrary unit-length vector.
20
+ */
21
+ normalAt(_t: number): Vec3;
22
+ tangentAt(_t: number): Vec3;
23
+ splitAt(_t: number): [PointShape2D];
24
+ nearestPointTo(_point: Point2): {
25
+ point: Vec3;
26
+ parameterValue: number;
27
+ };
17
28
  }
18
29
  export default PointShape2D;