@js-draw/math 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. package/README.md +3 -0
  2. package/build-config.json +4 -0
  3. package/dist/cjs/Color4.d.ts +83 -0
  4. package/dist/cjs/Color4.js +277 -0
  5. package/dist/cjs/Mat33.d.ts +131 -0
  6. package/dist/cjs/Mat33.js +345 -0
  7. package/dist/cjs/Vec2.d.ts +42 -0
  8. package/dist/cjs/Vec2.js +48 -0
  9. package/dist/cjs/Vec3.d.ts +126 -0
  10. package/dist/cjs/Vec3.js +203 -0
  11. package/dist/cjs/lib.d.ts +27 -0
  12. package/dist/cjs/lib.js +42 -0
  13. package/dist/cjs/polynomial/solveQuadratic.d.ts +9 -0
  14. package/dist/cjs/polynomial/solveQuadratic.js +39 -0
  15. package/dist/cjs/rounding.d.ts +15 -0
  16. package/dist/cjs/rounding.js +146 -0
  17. package/dist/cjs/shapes/Abstract2DShape.d.ts +49 -0
  18. package/dist/cjs/shapes/Abstract2DShape.js +38 -0
  19. package/dist/cjs/shapes/BezierJSWrapper.d.ts +36 -0
  20. package/dist/cjs/shapes/BezierJSWrapper.js +94 -0
  21. package/dist/cjs/shapes/CubicBezier.d.ts +17 -0
  22. package/dist/cjs/shapes/CubicBezier.js +35 -0
  23. package/dist/cjs/shapes/LineSegment2.d.ts +70 -0
  24. package/dist/cjs/shapes/LineSegment2.js +183 -0
  25. package/dist/cjs/shapes/Path.d.ts +96 -0
  26. package/dist/cjs/shapes/Path.js +766 -0
  27. package/dist/cjs/shapes/PointShape2D.d.ts +18 -0
  28. package/dist/cjs/shapes/PointShape2D.js +31 -0
  29. package/dist/cjs/shapes/QuadraticBezier.d.ts +35 -0
  30. package/dist/cjs/shapes/QuadraticBezier.js +120 -0
  31. package/dist/cjs/shapes/Rect2.d.ts +58 -0
  32. package/dist/cjs/shapes/Rect2.js +259 -0
  33. package/dist/cjs/shapes/Triangle.d.ts +46 -0
  34. package/dist/cjs/shapes/Triangle.js +126 -0
  35. package/dist/mjs/Color4.d.ts +83 -0
  36. package/dist/mjs/Color4.mjs +271 -0
  37. package/dist/mjs/Mat33.d.ts +131 -0
  38. package/dist/mjs/Mat33.mjs +338 -0
  39. package/dist/mjs/Vec2.d.ts +42 -0
  40. package/dist/mjs/Vec2.mjs +42 -0
  41. package/dist/mjs/Vec3.d.ts +126 -0
  42. package/dist/mjs/Vec3.mjs +199 -0
  43. package/dist/mjs/lib.d.ts +27 -0
  44. package/dist/mjs/lib.mjs +29 -0
  45. package/dist/mjs/polynomial/solveQuadratic.d.ts +9 -0
  46. package/dist/mjs/polynomial/solveQuadratic.mjs +37 -0
  47. package/dist/mjs/rounding.d.ts +15 -0
  48. package/dist/mjs/rounding.mjs +139 -0
  49. package/dist/mjs/shapes/Abstract2DShape.d.ts +49 -0
  50. package/dist/mjs/shapes/Abstract2DShape.mjs +36 -0
  51. package/dist/mjs/shapes/BezierJSWrapper.d.ts +36 -0
  52. package/dist/mjs/shapes/BezierJSWrapper.mjs +89 -0
  53. package/dist/mjs/shapes/CubicBezier.d.ts +17 -0
  54. package/dist/mjs/shapes/CubicBezier.mjs +30 -0
  55. package/dist/mjs/shapes/LineSegment2.d.ts +70 -0
  56. package/dist/mjs/shapes/LineSegment2.mjs +176 -0
  57. package/dist/mjs/shapes/Path.d.ts +96 -0
  58. package/dist/mjs/shapes/Path.mjs +759 -0
  59. package/dist/mjs/shapes/PointShape2D.d.ts +18 -0
  60. package/dist/mjs/shapes/PointShape2D.mjs +26 -0
  61. package/dist/mjs/shapes/QuadraticBezier.d.ts +35 -0
  62. package/dist/mjs/shapes/QuadraticBezier.mjs +113 -0
  63. package/dist/mjs/shapes/Rect2.d.ts +58 -0
  64. package/dist/mjs/shapes/Rect2.mjs +252 -0
  65. package/dist/mjs/shapes/Triangle.d.ts +46 -0
  66. package/dist/mjs/shapes/Triangle.mjs +121 -0
  67. package/package.json +48 -0
  68. package/tsconfig.json +7 -0
  69. package/typedoc.json +5 -0
@@ -0,0 +1,759 @@
1
+ import { toRoundedString, toStringOfSamePrecision } from '../rounding.mjs';
2
+ import LineSegment2 from './LineSegment2.mjs';
3
+ import Rect2 from './Rect2.mjs';
4
+ import { Vec2 } from '../Vec2.mjs';
5
+ import CubicBezier from './CubicBezier.mjs';
6
+ import QuadraticBezier from './QuadraticBezier.mjs';
7
+ import PointShape2D from './PointShape2D.mjs';
8
+ export var PathCommandType;
9
+ (function (PathCommandType) {
10
+ PathCommandType[PathCommandType["LineTo"] = 0] = "LineTo";
11
+ PathCommandType[PathCommandType["MoveTo"] = 1] = "MoveTo";
12
+ PathCommandType[PathCommandType["CubicBezierTo"] = 2] = "CubicBezierTo";
13
+ PathCommandType[PathCommandType["QuadraticBezierTo"] = 3] = "QuadraticBezierTo";
14
+ })(PathCommandType || (PathCommandType = {}));
15
+ /**
16
+ * Represents a union of lines and curves.
17
+ */
18
+ export class Path {
19
+ constructor(startPoint, parts) {
20
+ this.startPoint = startPoint;
21
+ this.parts = parts;
22
+ this.cachedGeometry = null;
23
+ this.cachedPolylineApproximation = null;
24
+ this.cachedStringVersion = null;
25
+ // Initial bounding box contains one point: the start point.
26
+ this.bbox = Rect2.bboxOf([startPoint]);
27
+ // Convert into a representation of the geometry (cache for faster intersection
28
+ // calculation)
29
+ for (const part of parts) {
30
+ this.bbox = this.bbox.union(Path.computeBBoxForSegment(startPoint, part));
31
+ }
32
+ }
33
+ getExactBBox() {
34
+ const bboxes = [];
35
+ for (const part of this.geometry) {
36
+ bboxes.push(part.getTightBoundingBox());
37
+ }
38
+ return Rect2.union(...bboxes);
39
+ }
40
+ // Lazy-loads and returns this path's geometry
41
+ get geometry() {
42
+ if (this.cachedGeometry) {
43
+ return this.cachedGeometry;
44
+ }
45
+ let startPoint = this.startPoint;
46
+ const geometry = [];
47
+ for (const part of this.parts) {
48
+ switch (part.kind) {
49
+ case PathCommandType.CubicBezierTo:
50
+ geometry.push(new CubicBezier(startPoint, part.controlPoint1, part.controlPoint2, part.endPoint));
51
+ startPoint = part.endPoint;
52
+ break;
53
+ case PathCommandType.QuadraticBezierTo:
54
+ geometry.push(new QuadraticBezier(startPoint, part.controlPoint, part.endPoint));
55
+ startPoint = part.endPoint;
56
+ break;
57
+ case PathCommandType.LineTo:
58
+ geometry.push(new LineSegment2(startPoint, part.point));
59
+ startPoint = part.point;
60
+ break;
61
+ case PathCommandType.MoveTo:
62
+ geometry.push(new PointShape2D(part.point));
63
+ startPoint = part.point;
64
+ break;
65
+ }
66
+ }
67
+ this.cachedGeometry = geometry;
68
+ return this.cachedGeometry;
69
+ }
70
+ // Approximates this path with a group of line segments.
71
+ polylineApproximation() {
72
+ if (this.cachedPolylineApproximation) {
73
+ return this.cachedPolylineApproximation;
74
+ }
75
+ const points = [];
76
+ for (const part of this.parts) {
77
+ switch (part.kind) {
78
+ case PathCommandType.CubicBezierTo:
79
+ points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
80
+ break;
81
+ case PathCommandType.QuadraticBezierTo:
82
+ points.push(part.controlPoint, part.endPoint);
83
+ break;
84
+ case PathCommandType.MoveTo:
85
+ case PathCommandType.LineTo:
86
+ points.push(part.point);
87
+ break;
88
+ }
89
+ }
90
+ const result = [];
91
+ let prevPoint = this.startPoint;
92
+ for (const point of points) {
93
+ result.push(new LineSegment2(prevPoint, point));
94
+ prevPoint = point;
95
+ }
96
+ return result;
97
+ }
98
+ static computeBBoxForSegment(startPoint, part) {
99
+ const points = [startPoint];
100
+ let exhaustivenessCheck;
101
+ switch (part.kind) {
102
+ case PathCommandType.MoveTo:
103
+ case PathCommandType.LineTo:
104
+ points.push(part.point);
105
+ break;
106
+ case PathCommandType.CubicBezierTo:
107
+ points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
108
+ break;
109
+ case PathCommandType.QuadraticBezierTo:
110
+ points.push(part.controlPoint, part.endPoint);
111
+ break;
112
+ default:
113
+ exhaustivenessCheck = part;
114
+ return exhaustivenessCheck;
115
+ }
116
+ return Rect2.bboxOf(points);
117
+ }
118
+ /**
119
+ * Let `S` be a closed path a distance `strokeRadius` from this path.
120
+ *
121
+ * @returns Approximate intersections of `line` with `S` using ray marching, starting from
122
+ * both end points of `line` and each point in `additionalRaymarchStartPoints`.
123
+ */
124
+ raymarchIntersectionWith(line, strokeRadius, additionalRaymarchStartPoints = []) {
125
+ // No intersection between bounding boxes: No possible intersection
126
+ // of the interior.
127
+ if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius))) {
128
+ return [];
129
+ }
130
+ const lineLength = line.length;
131
+ const partDistFunctionRecords = [];
132
+ // Determine distance functions for all parts that the given line could possibly intersect with
133
+ for (const part of this.geometry) {
134
+ const bbox = part.getTightBoundingBox().grownBy(strokeRadius);
135
+ if (!bbox.intersects(line.bbox)) {
136
+ continue;
137
+ }
138
+ // Signed distance function
139
+ const partDist = (point) => part.signedDistance(point);
140
+ // Part signed distance function (negative result implies `point` is
141
+ // inside the shape).
142
+ const partSdf = (point) => partDist(point) - strokeRadius;
143
+ // If the line can't possibly intersect the part,
144
+ if (partSdf(line.p1) > lineLength && partSdf(line.p2) > lineLength) {
145
+ continue;
146
+ }
147
+ partDistFunctionRecords.push({
148
+ part,
149
+ distFn: partDist,
150
+ bbox,
151
+ });
152
+ }
153
+ // If no distance functions, there are no intersections.
154
+ if (partDistFunctionRecords.length === 0) {
155
+ return [];
156
+ }
157
+ // Returns the minimum distance to a part in this stroke, where only parts that the given
158
+ // line could intersect are considered.
159
+ const sdf = (point) => {
160
+ let minDist = Infinity;
161
+ let minDistPart = null;
162
+ const uncheckedDistFunctions = [];
163
+ // First pass: only curves for which the current point is inside
164
+ // the bounding box.
165
+ for (const distFnRecord of partDistFunctionRecords) {
166
+ const { part, distFn, bbox } = distFnRecord;
167
+ // Check later if the current point isn't in the bounding box.
168
+ if (!bbox.containsPoint(point)) {
169
+ uncheckedDistFunctions.push(distFnRecord);
170
+ continue;
171
+ }
172
+ const currentDist = distFn(point);
173
+ if (currentDist <= minDist) {
174
+ minDist = currentDist;
175
+ minDistPart = part;
176
+ }
177
+ }
178
+ // Second pass: Everything else
179
+ for (const { part, distFn, bbox } of uncheckedDistFunctions) {
180
+ // Skip if impossible for the distance to the target to be lesser than
181
+ // the current minimum.
182
+ if (!bbox.grownBy(minDist).containsPoint(point)) {
183
+ continue;
184
+ }
185
+ const currentDist = distFn(point);
186
+ if (currentDist <= minDist) {
187
+ minDist = currentDist;
188
+ minDistPart = part;
189
+ }
190
+ }
191
+ return [minDistPart, minDist - strokeRadius];
192
+ };
193
+ // Raymarch:
194
+ const maxRaymarchSteps = 7;
195
+ // Start raymarching from each of these points. This allows detection of multiple
196
+ // intersections.
197
+ const startPoints = [
198
+ line.p1, ...additionalRaymarchStartPoints, line.p2
199
+ ];
200
+ // Converts a point ON THE LINE to a parameter
201
+ const pointToParameter = (point) => {
202
+ // Because line.direction is a unit vector, this computes the length
203
+ // of the projection of the vector(line.p1->point) onto line.direction.
204
+ //
205
+ // Note that this can be negative if the given point is outside of the given
206
+ // line segment.
207
+ return point.minus(line.p1).dot(line.direction);
208
+ };
209
+ // Sort start points by parameter on the line.
210
+ // This allows us to determine whether the current value of a parameter
211
+ // drops down to a value already tested.
212
+ startPoints.sort((a, b) => {
213
+ const t_a = pointToParameter(a);
214
+ const t_b = pointToParameter(b);
215
+ // Sort in increasing order
216
+ return t_a - t_b;
217
+ });
218
+ const result = [];
219
+ const stoppingThreshold = strokeRadius / 1000;
220
+ // Returns the maximum x value explored
221
+ const raymarchFrom = (startPoint,
222
+ // Direction to march in (multiplies line.direction)
223
+ directionMultiplier,
224
+ // Terminate if the current point corresponds to a parameter
225
+ // below this.
226
+ minimumLineParameter) => {
227
+ let currentPoint = startPoint;
228
+ let [lastPart, lastDist] = sdf(currentPoint);
229
+ let lastParameter = pointToParameter(currentPoint);
230
+ if (lastDist > lineLength) {
231
+ return lastParameter;
232
+ }
233
+ const direction = line.direction.times(directionMultiplier);
234
+ for (let i = 0; i < maxRaymarchSteps; i++) {
235
+ // Step in the direction of the edge of the shape.
236
+ const step = lastDist;
237
+ currentPoint = currentPoint.plus(direction.times(step));
238
+ lastParameter = pointToParameter(currentPoint);
239
+ // If we're below the minimum parameter, stop. We've already tried
240
+ // this.
241
+ if (lastParameter <= minimumLineParameter) {
242
+ return lastParameter;
243
+ }
244
+ const [currentPart, signedDist] = sdf(currentPoint);
245
+ // Ensure we're stepping in the correct direction.
246
+ // Note that because we could start with a negative distance and work towards a
247
+ // positive distance, we need absolute values here.
248
+ if (Math.abs(signedDist) > Math.abs(lastDist)) {
249
+ // If not, stop.
250
+ return null;
251
+ }
252
+ lastDist = signedDist;
253
+ lastPart = currentPart;
254
+ // Is the distance close enough that we can stop early?
255
+ if (Math.abs(lastDist) < stoppingThreshold) {
256
+ break;
257
+ }
258
+ }
259
+ // Ensure that the point we ended with is on the line.
260
+ const isOnLineSegment = lastParameter >= 0 && lastParameter <= lineLength;
261
+ if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
262
+ result.push({
263
+ point: currentPoint,
264
+ parameterValue: NaN,
265
+ curve: lastPart,
266
+ });
267
+ }
268
+ return lastParameter;
269
+ };
270
+ // The maximum value of the line's parameter explored so far (0 corresponds to
271
+ // line.p1)
272
+ let maxLineT = 0;
273
+ // Raymarch for each start point.
274
+ //
275
+ // Use a for (i from 0 to length) loop because startPoints may be added
276
+ // during iteration.
277
+ for (let i = 0; i < startPoints.length; i++) {
278
+ const startPoint = startPoints[i];
279
+ // Try raymarching in both directions.
280
+ maxLineT = Math.max(maxLineT, raymarchFrom(startPoint, 1, maxLineT) ?? maxLineT);
281
+ maxLineT = Math.max(maxLineT, raymarchFrom(startPoint, -1, maxLineT) ?? maxLineT);
282
+ }
283
+ return result;
284
+ }
285
+ /**
286
+ * Returns a list of intersections with this path. If `strokeRadius` is given,
287
+ * intersections are approximated with the surface `strokeRadius` away from this.
288
+ *
289
+ * If `strokeRadius > 0`, the resultant `parameterValue` has no defined value.
290
+ */
291
+ intersection(line, strokeRadius) {
292
+ let result = [];
293
+ // Is any intersection between shapes within the bounding boxes impossible?
294
+ if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius ?? 0))) {
295
+ return [];
296
+ }
297
+ for (const part of this.geometry) {
298
+ const intersection = part.intersectsLineSegment(line);
299
+ if (intersection.length > 0) {
300
+ result.push({
301
+ curve: part,
302
+ point: intersection[0],
303
+ });
304
+ }
305
+ }
306
+ // If given a non-zero strokeWidth, attempt to raymarch.
307
+ // Even if raymarching, we need to collect starting points.
308
+ // We use the above-calculated intersections for this.
309
+ const doRaymarching = strokeRadius && strokeRadius > 1e-8;
310
+ if (doRaymarching) {
311
+ // Starting points for raymarching (in addition to the end points of the line).
312
+ const startPoints = result.map(intersection => intersection.point);
313
+ result = this.raymarchIntersectionWith(line, strokeRadius, startPoints);
314
+ }
315
+ return result;
316
+ }
317
+ static mapPathCommand(part, mapping) {
318
+ switch (part.kind) {
319
+ case PathCommandType.MoveTo:
320
+ case PathCommandType.LineTo:
321
+ return {
322
+ kind: part.kind,
323
+ point: mapping(part.point),
324
+ };
325
+ break;
326
+ case PathCommandType.CubicBezierTo:
327
+ return {
328
+ kind: part.kind,
329
+ controlPoint1: mapping(part.controlPoint1),
330
+ controlPoint2: mapping(part.controlPoint2),
331
+ endPoint: mapping(part.endPoint),
332
+ };
333
+ break;
334
+ case PathCommandType.QuadraticBezierTo:
335
+ return {
336
+ kind: part.kind,
337
+ controlPoint: mapping(part.controlPoint),
338
+ endPoint: mapping(part.endPoint),
339
+ };
340
+ break;
341
+ }
342
+ const exhaustivenessCheck = part;
343
+ return exhaustivenessCheck;
344
+ }
345
+ mapPoints(mapping) {
346
+ const startPoint = mapping(this.startPoint);
347
+ const newParts = [];
348
+ for (const part of this.parts) {
349
+ newParts.push(Path.mapPathCommand(part, mapping));
350
+ }
351
+ return new Path(startPoint, newParts);
352
+ }
353
+ transformedBy(affineTransfm) {
354
+ if (affineTransfm.isIdentity()) {
355
+ return this;
356
+ }
357
+ return this.mapPoints(point => affineTransfm.transformVec2(point));
358
+ }
359
+ // Creates a new path by joining [other] to the end of this path
360
+ union(other) {
361
+ if (!other) {
362
+ return this;
363
+ }
364
+ return new Path(this.startPoint, [
365
+ ...this.parts,
366
+ {
367
+ kind: PathCommandType.MoveTo,
368
+ point: other.startPoint,
369
+ },
370
+ ...other.parts,
371
+ ]);
372
+ }
373
+ getEndPoint() {
374
+ if (this.parts.length === 0) {
375
+ return this.startPoint;
376
+ }
377
+ const lastPart = this.parts[this.parts.length - 1];
378
+ if (lastPart.kind === PathCommandType.QuadraticBezierTo || lastPart.kind === PathCommandType.CubicBezierTo) {
379
+ return lastPart.endPoint;
380
+ }
381
+ else {
382
+ return lastPart.point;
383
+ }
384
+ }
385
+ roughlyIntersects(rect, strokeWidth = 0) {
386
+ if (this.parts.length === 0) {
387
+ return rect.containsPoint(this.startPoint);
388
+ }
389
+ const isClosed = this.startPoint.eq(this.getEndPoint());
390
+ if (isClosed && strokeWidth === 0) {
391
+ return this.closedRoughlyIntersects(rect);
392
+ }
393
+ if (rect.containsRect(this.bbox)) {
394
+ return true;
395
+ }
396
+ // Does the rectangle intersect the bounding boxes of any of this' parts?
397
+ let startPoint = this.startPoint;
398
+ for (const part of this.parts) {
399
+ const bbox = Path.computeBBoxForSegment(startPoint, part).grownBy(strokeWidth);
400
+ if (part.kind === PathCommandType.LineTo || part.kind === PathCommandType.MoveTo) {
401
+ startPoint = part.point;
402
+ }
403
+ else {
404
+ startPoint = part.endPoint;
405
+ }
406
+ if (rect.intersects(bbox)) {
407
+ return true;
408
+ }
409
+ }
410
+ return false;
411
+ }
412
+ // Treats this as a closed path and returns true if part of `rect` is roughly within
413
+ // this path's interior.
414
+ //
415
+ // Note: Assumes that this is a closed, non-self-intersecting path.
416
+ closedRoughlyIntersects(rect) {
417
+ if (rect.containsRect(this.bbox)) {
418
+ return true;
419
+ }
420
+ // Choose a point outside of the path.
421
+ const startPt = this.bbox.topLeft.minus(Vec2.of(1, 1));
422
+ const testPts = rect.corners;
423
+ const polygon = this.polylineApproximation();
424
+ for (const point of testPts) {
425
+ const testLine = new LineSegment2(point, startPt);
426
+ let intersectionCount = 0;
427
+ for (const line of polygon) {
428
+ if (line.intersects(testLine)) {
429
+ intersectionCount++;
430
+ }
431
+ }
432
+ // Odd? The point is within the polygon!
433
+ if (intersectionCount % 2 === 1) {
434
+ return true;
435
+ }
436
+ }
437
+ // Grow the rectangle for possible additional precision.
438
+ const grownRect = rect.grownBy(Math.min(rect.size.x, rect.size.y));
439
+ const edges = [];
440
+ for (const subrect of grownRect.divideIntoGrid(4, 4)) {
441
+ edges.push(...subrect.getEdges());
442
+ }
443
+ for (const edge of edges) {
444
+ for (const line of polygon) {
445
+ if (edge.intersects(line)) {
446
+ return true;
447
+ }
448
+ }
449
+ }
450
+ // Even? Probably no intersection.
451
+ return false;
452
+ }
453
+ // Returns a path that outlines [rect]. If [lineWidth] is not given, the resultant path is
454
+ // the outline of [rect]. Otherwise, the resultant path represents a line of width [lineWidth]
455
+ // that traces [rect].
456
+ static fromRect(rect, lineWidth = null) {
457
+ const commands = [];
458
+ let corners;
459
+ let startPoint;
460
+ if (lineWidth !== null) {
461
+ // Vector from the top left corner or bottom right corner to the edge of the
462
+ // stroked region.
463
+ const cornerToEdge = Vec2.of(lineWidth, lineWidth).times(0.5);
464
+ const innerRect = Rect2.fromCorners(rect.topLeft.plus(cornerToEdge), rect.bottomRight.minus(cornerToEdge));
465
+ const outerRect = Rect2.fromCorners(rect.topLeft.minus(cornerToEdge), rect.bottomRight.plus(cornerToEdge));
466
+ corners = [
467
+ innerRect.corners[3],
468
+ ...innerRect.corners,
469
+ ...outerRect.corners.reverse(),
470
+ ];
471
+ startPoint = outerRect.corners[3];
472
+ }
473
+ else {
474
+ corners = rect.corners.slice(1);
475
+ startPoint = rect.corners[0];
476
+ }
477
+ for (const corner of corners) {
478
+ commands.push({
479
+ kind: PathCommandType.LineTo,
480
+ point: corner,
481
+ });
482
+ }
483
+ // Close the shape
484
+ commands.push({
485
+ kind: PathCommandType.LineTo,
486
+ point: startPoint,
487
+ });
488
+ return new Path(startPoint, commands);
489
+ }
490
+ toString(useNonAbsCommands) {
491
+ if (this.cachedStringVersion) {
492
+ return this.cachedStringVersion;
493
+ }
494
+ if (useNonAbsCommands === undefined) {
495
+ // Hueristic: Try to determine whether converting absolute to relative commands is worth it.
496
+ useNonAbsCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
497
+ }
498
+ const result = Path.toString(this.startPoint, this.parts, !useNonAbsCommands);
499
+ this.cachedStringVersion = result;
500
+ return result;
501
+ }
502
+ serialize() {
503
+ return this.toString();
504
+ }
505
+ // @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
506
+ // conversions can lead to smaller output strings, but also take time.
507
+ static toString(startPoint, parts, onlyAbsCommands) {
508
+ const result = [];
509
+ let prevPoint;
510
+ const addCommand = (command, ...points) => {
511
+ const absoluteCommandParts = [];
512
+ const relativeCommandParts = [];
513
+ const makeAbsCommand = !prevPoint || onlyAbsCommands;
514
+ const roundedPrevX = prevPoint ? toRoundedString(prevPoint.x) : '';
515
+ const roundedPrevY = prevPoint ? toRoundedString(prevPoint.y) : '';
516
+ for (const point of points) {
517
+ const xComponent = toRoundedString(point.x);
518
+ const yComponent = toRoundedString(point.y);
519
+ // Relative commands are often shorter as strings than absolute commands.
520
+ if (!makeAbsCommand) {
521
+ const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint.x, xComponent, roundedPrevX, roundedPrevY);
522
+ const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint.y, yComponent, roundedPrevX, roundedPrevY);
523
+ // No need for an additional separator if it starts with a '-'
524
+ if (yComponentRelative.charAt(0) === '-') {
525
+ relativeCommandParts.push(`${xComponentRelative}${yComponentRelative}`);
526
+ }
527
+ else {
528
+ relativeCommandParts.push(`${xComponentRelative},${yComponentRelative}`);
529
+ }
530
+ }
531
+ else {
532
+ absoluteCommandParts.push(`${xComponent},${yComponent}`);
533
+ }
534
+ }
535
+ let commandString;
536
+ if (makeAbsCommand) {
537
+ commandString = `${command}${absoluteCommandParts.join(' ')}`;
538
+ }
539
+ else {
540
+ commandString = `${command.toLowerCase()}${relativeCommandParts.join(' ')}`;
541
+ }
542
+ // Don't add no-ops.
543
+ if (commandString === 'l0,0' || commandString === 'm0,0') {
544
+ return;
545
+ }
546
+ result.push(commandString);
547
+ if (points.length > 0) {
548
+ prevPoint = points[points.length - 1];
549
+ }
550
+ };
551
+ // Don't add two moveTos in a row (this can happen if
552
+ // the start point corresponds to a moveTo _and_ the first command is
553
+ // also a moveTo)
554
+ if (parts[0]?.kind !== PathCommandType.MoveTo) {
555
+ addCommand('M', startPoint);
556
+ }
557
+ let exhaustivenessCheck;
558
+ for (let i = 0; i < parts.length; i++) {
559
+ const part = parts[i];
560
+ switch (part.kind) {
561
+ case PathCommandType.MoveTo:
562
+ addCommand('M', part.point);
563
+ break;
564
+ case PathCommandType.LineTo:
565
+ addCommand('L', part.point);
566
+ break;
567
+ case PathCommandType.CubicBezierTo:
568
+ addCommand('C', part.controlPoint1, part.controlPoint2, part.endPoint);
569
+ break;
570
+ case PathCommandType.QuadraticBezierTo:
571
+ addCommand('Q', part.controlPoint, part.endPoint);
572
+ break;
573
+ default:
574
+ exhaustivenessCheck = part;
575
+ return exhaustivenessCheck;
576
+ }
577
+ }
578
+ return result.join('');
579
+ }
580
+ /**
581
+ * Create a Path from a SVG path specification.
582
+ *
583
+ * ## To-do
584
+ * - TODO: Support a larger subset of SVG paths
585
+ * - Elliptical arcs are currently unsupported.
586
+ * - TODO: Support `s`,`t` commands shorthands.
587
+ */
588
+ static fromString(pathString) {
589
+ // See the MDN reference:
590
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
591
+ // and
592
+ // https://www.w3.org/TR/SVG2/paths.html
593
+ // Remove linebreaks
594
+ pathString = pathString.split('\n').join(' ');
595
+ let lastPos = Vec2.zero;
596
+ let firstPos = null;
597
+ let startPos = null;
598
+ let isFirstCommand = true;
599
+ const commands = [];
600
+ const moveTo = (point) => {
601
+ // The first moveTo/lineTo is already handled by the [startPoint] parameter of the Path constructor.
602
+ if (isFirstCommand) {
603
+ isFirstCommand = false;
604
+ return;
605
+ }
606
+ commands.push({
607
+ kind: PathCommandType.MoveTo,
608
+ point,
609
+ });
610
+ };
611
+ const lineTo = (point) => {
612
+ if (isFirstCommand) {
613
+ isFirstCommand = false;
614
+ return;
615
+ }
616
+ commands.push({
617
+ kind: PathCommandType.LineTo,
618
+ point,
619
+ });
620
+ };
621
+ const cubicBezierTo = (cp1, cp2, end) => {
622
+ commands.push({
623
+ kind: PathCommandType.CubicBezierTo,
624
+ controlPoint1: cp1,
625
+ controlPoint2: cp2,
626
+ endPoint: end,
627
+ });
628
+ };
629
+ const quadraticBeierTo = (controlPoint, endPoint) => {
630
+ commands.push({
631
+ kind: PathCommandType.QuadraticBezierTo,
632
+ controlPoint,
633
+ endPoint,
634
+ });
635
+ };
636
+ const commandArgCounts = {
637
+ 'm': 1,
638
+ 'l': 1,
639
+ 'c': 3,
640
+ 'q': 2,
641
+ 'z': 0,
642
+ 'h': 1,
643
+ 'v': 1,
644
+ };
645
+ // Each command: Command character followed by anything that isn't a command character
646
+ const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
647
+ let current;
648
+ while ((current = commandExp.exec(pathString)) !== null) {
649
+ const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(part => part.length > 0).reduce((accumualtor, current) => {
650
+ // As of 09/2022, iOS Safari doesn't support support lookbehind in regular
651
+ // expressions. As such, we need an alternative.
652
+ // Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
653
+ // we need special cases:
654
+ current = current.replace(/([^eE])[-]/g, '$1 -');
655
+ const parts = current.split(' -');
656
+ if (parts[0] !== '') {
657
+ accumualtor.push(parts[0]);
658
+ }
659
+ accumualtor.push(...parts.slice(1).map(part => `-${part}`));
660
+ return accumualtor;
661
+ }, []);
662
+ let numericArgs = argParts.map(arg => parseFloat(arg));
663
+ let commandChar = current[1].toLowerCase();
664
+ let uppercaseCommand = current[1] !== commandChar;
665
+ // Convert commands that don't take points into commands that do.
666
+ if (commandChar === 'v' || commandChar === 'h') {
667
+ numericArgs = numericArgs.reduce((accumulator, current) => {
668
+ if (commandChar === 'v') {
669
+ return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
670
+ }
671
+ else {
672
+ return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
673
+ }
674
+ }, []);
675
+ commandChar = 'l';
676
+ }
677
+ else if (commandChar === 'z') {
678
+ if (firstPos) {
679
+ numericArgs = [firstPos.x, firstPos.y];
680
+ firstPos = lastPos;
681
+ }
682
+ else {
683
+ continue;
684
+ }
685
+ // 'z' always acts like an uppercase lineTo(startPos)
686
+ uppercaseCommand = true;
687
+ commandChar = 'l';
688
+ }
689
+ const commandArgCount = commandArgCounts[commandChar] ?? 0;
690
+ const allArgs = numericArgs.reduce((accumulator, current, index, parts) => {
691
+ if (index % 2 !== 0) {
692
+ const currentAsFloat = current;
693
+ const prevAsFloat = parts[index - 1];
694
+ return accumulator.concat(Vec2.of(prevAsFloat, currentAsFloat));
695
+ }
696
+ else {
697
+ return accumulator;
698
+ }
699
+ }, []).map((coordinate, index) => {
700
+ // Lowercase commands are relative, uppercase commands use absolute
701
+ // positioning
702
+ let newPos;
703
+ if (uppercaseCommand) {
704
+ newPos = coordinate;
705
+ }
706
+ else {
707
+ newPos = lastPos.plus(coordinate);
708
+ }
709
+ if ((index + 1) % commandArgCount === 0) {
710
+ lastPos = newPos;
711
+ }
712
+ return newPos;
713
+ });
714
+ if (allArgs.length % commandArgCount !== 0) {
715
+ throw new Error([
716
+ `Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
717
+ `The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
718
+ `Command: ${current[0]}`,
719
+ ].join('\n'));
720
+ }
721
+ for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
722
+ const args = allArgs.slice(argPos, argPos + commandArgCount);
723
+ switch (commandChar.toLowerCase()) {
724
+ case 'm':
725
+ if (argPos === 0) {
726
+ moveTo(args[0]);
727
+ }
728
+ else {
729
+ lineTo(args[0]);
730
+ }
731
+ break;
732
+ case 'l':
733
+ lineTo(args[0]);
734
+ break;
735
+ case 'c':
736
+ cubicBezierTo(args[0], args[1], args[2]);
737
+ break;
738
+ case 'q':
739
+ quadraticBeierTo(args[0], args[1]);
740
+ break;
741
+ default:
742
+ throw new Error(`Unknown path command ${commandChar}`);
743
+ }
744
+ isFirstCommand = false;
745
+ }
746
+ if (allArgs.length > 0) {
747
+ firstPos ??= allArgs[0];
748
+ startPos ??= firstPos;
749
+ lastPos = allArgs[allArgs.length - 1];
750
+ }
751
+ }
752
+ const result = new Path(startPos ?? Vec2.zero, commands);
753
+ result.cachedStringVersion = pathString;
754
+ return result;
755
+ }
756
+ }
757
+ // @internal TODO: At present, this isn't really an empty path.
758
+ Path.empty = new Path(Vec2.zero, []);
759
+ export default Path;