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