@js-draw/math 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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;