svg-path-commander 2.1.11 → 2.2.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.
package/dist/util.js ADDED
@@ -0,0 +1,4277 @@
1
+ /*!
2
+ * SVGPathCommander v2.2.0 (http://thednp.github.io/svg-path-commander)
3
+ * Copyright 2026 © thednp
4
+ * Licensed under MIT (https://github.com/thednp/svg-path-commander/blob/master/LICENSE)
5
+ */
6
+ import CSSMatrix from "@thednp/dommatrix";
7
+ //#region src/math/midPoint.ts
8
+ /**
9
+ * Returns the coordinates of a specified distance
10
+ * ratio between two points.
11
+ *
12
+ * @param a the first point coordinates
13
+ * @param b the second point coordinates
14
+ * @param t the ratio
15
+ * @returns the midpoint coordinates
16
+ */
17
+ const midPoint = ([ax, ay], [bx, by], t) => {
18
+ return [ax + (bx - ax) * t, ay + (by - ay) * t];
19
+ };
20
+ //#endregion
21
+ //#region src/math/distanceSquareRoot.ts
22
+ /**
23
+ * Returns the square root of the distance
24
+ * between two given points.
25
+ *
26
+ * @param a the first point coordinates
27
+ * @param b the second point coordinates
28
+ * @returns the distance value
29
+ */
30
+ const distanceSquareRoot = (a, b) => {
31
+ return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]));
32
+ };
33
+ //#endregion
34
+ //#region src/math/lineTools.ts
35
+ /**
36
+ * Returns length for line segments (MoveTo, LineTo).
37
+ *
38
+ * @param x1 the starting point X
39
+ * @param y1 the starting point Y
40
+ * @param x2 the ending point X
41
+ * @param y2 the ending point Y
42
+ * @returns the line segment length
43
+ */
44
+ const getLineLength = (x1, y1, x2, y2) => {
45
+ return distanceSquareRoot([x1, y1], [x2, y2]);
46
+ };
47
+ /**
48
+ * Returns a point along the line segments (MoveTo, LineTo).
49
+ *
50
+ * @param x1 the starting point X
51
+ * @param y1 the starting point Y
52
+ * @param x2 the ending point X
53
+ * @param y2 the ending point Y
54
+ * @param distance the distance to point in [0-1] range
55
+ * @returns the point at length
56
+ */
57
+ const getPointAtLineLength = (x1, y1, x2, y2, distance) => {
58
+ let point = {
59
+ x: x1,
60
+ y: y1
61
+ };
62
+ if (typeof distance === "number") {
63
+ const length = distanceSquareRoot([x1, y1], [x2, y2]);
64
+ if (distance <= 0) point = {
65
+ x: x1,
66
+ y: y1
67
+ };
68
+ else if (distance >= length) point = {
69
+ x: x2,
70
+ y: y2
71
+ };
72
+ else {
73
+ const [x, y] = midPoint([x1, y1], [x2, y2], distance / length);
74
+ point = {
75
+ x,
76
+ y
77
+ };
78
+ }
79
+ }
80
+ return point;
81
+ };
82
+ /**
83
+ * Returns bounding box for line segments (MoveTo, LineTo).
84
+ *
85
+ * @param x1 the starting point X
86
+ * @param y1 the starting point Y
87
+ * @param x2 the ending point X
88
+ * @param y2 the ending point Y
89
+ * @returns the bounding box for line segments
90
+ */
91
+ const getLineBBox = (x1, y1, x2, y2) => {
92
+ const { min, max } = Math;
93
+ return [
94
+ min(x1, x2),
95
+ min(y1, y2),
96
+ max(x1, x2),
97
+ max(y1, y2)
98
+ ];
99
+ };
100
+ const lineTools = {
101
+ getLineBBox,
102
+ getLineLength,
103
+ getPointAtLineLength
104
+ };
105
+ //#endregion
106
+ //#region src/math/arcTools.ts
107
+ /**
108
+ * Returns the Arc segment length.
109
+ * @param rx radius along X axis
110
+ * @param ry radius along Y axis
111
+ * @param theta the angle in radians
112
+ * @returns the arc length
113
+ */
114
+ const arcLength = (rx, ry, theta) => {
115
+ const halfTheta = theta / 2;
116
+ const sinHalfTheta = Math.sin(halfTheta);
117
+ const cosHalfTheta = Math.cos(halfTheta);
118
+ const term1 = rx ** 2 * sinHalfTheta ** 2;
119
+ const term2 = ry ** 2 * cosHalfTheta ** 2;
120
+ const length = Math.sqrt(term1 + term2) * theta;
121
+ return Math.abs(length);
122
+ };
123
+ /**
124
+ * Find point on ellipse at given angle around ellipse (theta);
125
+ * @param cx the center X
126
+ * @param cy the center Y
127
+ * @param rx the radius X
128
+ * @param ry the radius Y
129
+ * @param alpha the arc rotation angle in radians
130
+ * @param theta the arc sweep angle in radians
131
+ * @returns a point around ellipse at given angle
132
+ */
133
+ const arcPoint = (cx, cy, rx, ry, alpha, theta) => {
134
+ const { sin, cos } = Math;
135
+ const cosA = cos(alpha);
136
+ const sinA = sin(alpha);
137
+ const x = rx * cos(theta);
138
+ const y = ry * sin(theta);
139
+ return [cx + cosA * x - sinA * y, cy + sinA * x + cosA * y];
140
+ };
141
+ /**
142
+ * Returns the angle between two points.
143
+ * @param v0 starting point
144
+ * @param v1 ending point
145
+ * @returns the angle in radian
146
+ */
147
+ const angleBetween = (v0, v1) => {
148
+ const { x: v0x, y: v0y } = v0;
149
+ const { x: v1x, y: v1y } = v1;
150
+ const p = v0x * v1x + v0y * v1y;
151
+ const n = Math.sqrt((v0x ** 2 + v0y ** 2) * (v1x ** 2 + v1y ** 2));
152
+ return (v0x * v1y - v0y * v1x < 0 ? -1 : 1) * Math.acos(p / n);
153
+ };
154
+ /**
155
+ * Returns the following properties for an Arc segment: center, start angle,
156
+ * end angle, and radiuses on X and Y axis.
157
+ *
158
+ * @param x1 the starting point X
159
+ * @param y1 the starting point Y
160
+ * @param RX the radius on X axis
161
+ * @param RY the radius on Y axis
162
+ * @param angle the ellipse rotation in degrees
163
+ * @param LAF the large arc flag
164
+ * @param SF the sweep flag
165
+ * @param x2 the ending point X
166
+ * @param y2 the ending point Y
167
+ * @returns properties specific to Arc segments
168
+ */
169
+ const getArcProps = (x1, y1, RX, RY, angle, LAF, SF, x, y) => {
170
+ const { abs, sin, cos, sqrt, PI } = Math;
171
+ let rx = abs(RX);
172
+ let ry = abs(RY);
173
+ const xRotRad = (angle % 360 + 360) % 360 * (PI / 180);
174
+ if (x1 === x && y1 === y) return {
175
+ rx,
176
+ ry,
177
+ startAngle: 0,
178
+ endAngle: 0,
179
+ center: {
180
+ x,
181
+ y
182
+ }
183
+ };
184
+ if (rx === 0 || ry === 0) return {
185
+ rx,
186
+ ry,
187
+ startAngle: 0,
188
+ endAngle: 0,
189
+ center: {
190
+ x: (x + x1) / 2,
191
+ y: (y + y1) / 2
192
+ }
193
+ };
194
+ const dx = (x1 - x) / 2;
195
+ const dy = (y1 - y) / 2;
196
+ const transformedPoint = {
197
+ x: cos(xRotRad) * dx + sin(xRotRad) * dy,
198
+ y: -sin(xRotRad) * dx + cos(xRotRad) * dy
199
+ };
200
+ const radiiCheck = transformedPoint.x ** 2 / rx ** 2 + transformedPoint.y ** 2 / ry ** 2;
201
+ if (radiiCheck > 1) {
202
+ rx *= sqrt(radiiCheck);
203
+ ry *= sqrt(radiiCheck);
204
+ }
205
+ let cRadicand = (rx ** 2 * ry ** 2 - rx ** 2 * transformedPoint.y ** 2 - ry ** 2 * transformedPoint.x ** 2) / (rx ** 2 * transformedPoint.y ** 2 + ry ** 2 * transformedPoint.x ** 2);
206
+ cRadicand = cRadicand < 0 ? 0 : cRadicand;
207
+ const cCoef = (LAF !== SF ? 1 : -1) * sqrt(cRadicand);
208
+ const transformedCenter = {
209
+ x: cCoef * (rx * transformedPoint.y / ry),
210
+ y: cCoef * (-(ry * transformedPoint.x) / rx)
211
+ };
212
+ const center = {
213
+ x: cos(xRotRad) * transformedCenter.x - sin(xRotRad) * transformedCenter.y + (x1 + x) / 2,
214
+ y: sin(xRotRad) * transformedCenter.x + cos(xRotRad) * transformedCenter.y + (y1 + y) / 2
215
+ };
216
+ const startVector = {
217
+ x: (transformedPoint.x - transformedCenter.x) / rx,
218
+ y: (transformedPoint.y - transformedCenter.y) / ry
219
+ };
220
+ const startAngle = angleBetween({
221
+ x: 1,
222
+ y: 0
223
+ }, startVector);
224
+ let sweepAngle = angleBetween(startVector, {
225
+ x: (-transformedPoint.x - transformedCenter.x) / rx,
226
+ y: (-transformedPoint.y - transformedCenter.y) / ry
227
+ });
228
+ if (!SF && sweepAngle > 0) sweepAngle -= 2 * PI;
229
+ else if (SF && sweepAngle < 0) sweepAngle += 2 * PI;
230
+ sweepAngle %= 2 * PI;
231
+ return {
232
+ center,
233
+ startAngle,
234
+ endAngle: startAngle + sweepAngle,
235
+ rx,
236
+ ry
237
+ };
238
+ };
239
+ /**
240
+ * Returns the length of an Arc segment.
241
+ *
242
+ * @param x1 the starting point X
243
+ * @param y1 the starting point Y
244
+ * @param c1x the first control point X
245
+ * @param c1y the first control point Y
246
+ * @param c2x the second control point X
247
+ * @param c2y the second control point Y
248
+ * @param x2 the ending point X
249
+ * @param y2 the ending point Y
250
+ * @returns the length of the Arc segment
251
+ */
252
+ const getArcLength = (x1, y1, RX, RY, angle, LAF, SF, x, y) => {
253
+ const { rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
254
+ return arcLength(rx, ry, endAngle - startAngle);
255
+ };
256
+ /**
257
+ * Returns a point along an Arc segment at a given distance.
258
+ *
259
+ * @param x1 the starting point X
260
+ * @param y1 the starting point Y
261
+ * @param RX the radius on X axis
262
+ * @param RY the radius on Y axis
263
+ * @param angle the ellipse rotation in degrees
264
+ * @param LAF the large arc flag
265
+ * @param SF the sweep flag
266
+ * @param x2 the ending point X
267
+ * @param y2 the ending point Y
268
+ * @param distance the distance along the arc
269
+ * @returns a point along the Arc segment
270
+ */
271
+ const getPointAtArcLength = (x1, y1, RX, RY, angle, LAF, SF, x, y, distance) => {
272
+ let point = {
273
+ x: x1,
274
+ y: y1
275
+ };
276
+ const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
277
+ if (typeof distance === "number") {
278
+ const length = arcLength(rx, ry, endAngle - startAngle);
279
+ if (distance <= 0) point = {
280
+ x: x1,
281
+ y: y1
282
+ };
283
+ else if (distance >= length) point = {
284
+ x,
285
+ y
286
+ };
287
+ else {
288
+ if (x1 === x && y1 === y) return {
289
+ x,
290
+ y
291
+ };
292
+ if (rx === 0 || ry === 0) return getPointAtLineLength(x1, y1, x, y, distance);
293
+ const { PI, cos, sin } = Math;
294
+ const sweepAngle = endAngle - startAngle;
295
+ const xRotRad = (angle % 360 + 360) % 360 * (PI / 180);
296
+ const alpha = startAngle + sweepAngle * (distance / length);
297
+ const ellipseComponentX = rx * cos(alpha);
298
+ const ellipseComponentY = ry * sin(alpha);
299
+ point = {
300
+ x: cos(xRotRad) * ellipseComponentX - sin(xRotRad) * ellipseComponentY + center.x,
301
+ y: sin(xRotRad) * ellipseComponentX + cos(xRotRad) * ellipseComponentY + center.y
302
+ };
303
+ }
304
+ }
305
+ return point;
306
+ };
307
+ /**
308
+ * Returns the extrema for an Arc segment in the following format:
309
+ * [MIN_X, MIN_Y, MAX_X, MAX_Y]
310
+ *
311
+ * @see https://github.com/herrstrietzel/svg-pathdata-getbbox
312
+ *
313
+ * @param x1 the starting point X
314
+ * @param y1 the starting point Y
315
+ * @param RX the radius on X axis
316
+ * @param RY the radius on Y axis
317
+ * @param angle the ellipse rotation in degrees
318
+ * @param LAF the large arc flag
319
+ * @param SF the sweep flag
320
+ * @param x2 the ending point X
321
+ * @param y2 the ending point Y
322
+ * @returns the extrema of the Arc segment
323
+ */
324
+ const getArcBBox = (x1, y1, RX, RY, angle, LAF, SF, x, y) => {
325
+ const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
326
+ const deltaAngle = endAngle - startAngle;
327
+ const { min, max, tan, atan2, PI } = Math;
328
+ const { x: cx, y: cy } = center;
329
+ const alpha = angle * PI / 180;
330
+ const tangent = tan(alpha);
331
+ /**
332
+ * find min/max from zeroes of directional derivative along x and y
333
+ * along x axis
334
+ */
335
+ const theta = atan2(-ry * tangent, rx);
336
+ const angle1 = theta;
337
+ const angle2 = theta + PI;
338
+ const angle3 = atan2(ry, rx * tangent);
339
+ const angle4 = angle3 + PI;
340
+ const xArray = [x];
341
+ const yArray = [y];
342
+ let xMin = min(x1, x);
343
+ let xMax = max(x1, x);
344
+ let yMin = min(y1, y);
345
+ let yMax = max(y1, y);
346
+ const pP2 = arcPoint(cx, cy, rx, ry, alpha, endAngle - deltaAngle * 1e-5);
347
+ const pP3 = arcPoint(cx, cy, rx, ry, alpha, endAngle - deltaAngle * .99999);
348
+ /**
349
+ * expected extremes
350
+ * if leaving inner bounding box
351
+ * (between segment start and end point)
352
+ * otherwise exclude elliptic extreme points
353
+ */
354
+ if (pP2[0] > xMax || pP3[0] > xMax) {
355
+ const p1 = arcPoint(cx, cy, rx, ry, alpha, angle1);
356
+ xArray.push(p1[0]);
357
+ yArray.push(p1[1]);
358
+ }
359
+ if (pP2[0] < xMin || pP3[0] < xMin) {
360
+ const p2 = arcPoint(cx, cy, rx, ry, alpha, angle2);
361
+ xArray.push(p2[0]);
362
+ yArray.push(p2[1]);
363
+ }
364
+ if (pP2[1] < yMin || pP3[1] < yMin) {
365
+ const p4 = arcPoint(cx, cy, rx, ry, alpha, angle4);
366
+ xArray.push(p4[0]);
367
+ yArray.push(p4[1]);
368
+ }
369
+ if (pP2[1] > yMax || pP3[1] > yMax) {
370
+ const p3 = arcPoint(cx, cy, rx, ry, alpha, angle3);
371
+ xArray.push(p3[0]);
372
+ yArray.push(p3[1]);
373
+ }
374
+ xMin = min.apply([], xArray);
375
+ yMin = min.apply([], yArray);
376
+ xMax = max.apply([], xArray);
377
+ yMax = max.apply([], yArray);
378
+ return [
379
+ xMin,
380
+ yMin,
381
+ xMax,
382
+ yMax
383
+ ];
384
+ };
385
+ const arcTools = {
386
+ angleBetween,
387
+ arcLength,
388
+ arcPoint,
389
+ getArcBBox,
390
+ getArcLength,
391
+ getArcProps,
392
+ getPointAtArcLength
393
+ };
394
+ //#endregion
395
+ //#region src/math/bezier.ts
396
+ /**
397
+ * Tools from bezier.js by Mike 'Pomax' Kamermans
398
+ * @see https://github.com/Pomax/bezierjs
399
+ */
400
+ const Tvalues = [
401
+ -.06405689286260563,
402
+ .06405689286260563,
403
+ -.1911188674736163,
404
+ .1911188674736163,
405
+ -.3150426796961634,
406
+ .3150426796961634,
407
+ -.4337935076260451,
408
+ .4337935076260451,
409
+ -.5454214713888396,
410
+ .5454214713888396,
411
+ -.6480936519369755,
412
+ .6480936519369755,
413
+ -.7401241915785544,
414
+ .7401241915785544,
415
+ -.820001985973903,
416
+ .820001985973903,
417
+ -.8864155270044011,
418
+ .8864155270044011,
419
+ -.9382745520027328,
420
+ .9382745520027328,
421
+ -.9747285559713095,
422
+ .9747285559713095,
423
+ -.9951872199970213,
424
+ .9951872199970213
425
+ ];
426
+ const Cvalues = [
427
+ .12793819534675216,
428
+ .12793819534675216,
429
+ .1258374563468283,
430
+ .1258374563468283,
431
+ .12167047292780339,
432
+ .12167047292780339,
433
+ .1155056680537256,
434
+ .1155056680537256,
435
+ .10744427011596563,
436
+ .10744427011596563,
437
+ .09761865210411388,
438
+ .09761865210411388,
439
+ .08619016153195327,
440
+ .08619016153195327,
441
+ .0733464814110803,
442
+ .0733464814110803,
443
+ .05929858491543678,
444
+ .05929858491543678,
445
+ .04427743881741981,
446
+ .04427743881741981,
447
+ .028531388628933663,
448
+ .028531388628933663,
449
+ .0123412297999872,
450
+ .0123412297999872
451
+ ];
452
+ /**
453
+ * @param points
454
+ * @returns
455
+ */
456
+ const deriveBezier = (points) => {
457
+ const dpoints = [];
458
+ for (let p = points, d = p.length, c = d - 1; d > 1; d -= 1, c -= 1) {
459
+ const list = [];
460
+ for (let j = 0; j < c; j += 1) list.push({
461
+ x: c * (p[j + 1].x - p[j].x),
462
+ y: c * (p[j + 1].y - p[j].y),
463
+ t: 0
464
+ });
465
+ dpoints.push(list);
466
+ p = list;
467
+ }
468
+ return dpoints;
469
+ };
470
+ /**
471
+ * @param points
472
+ * @param t
473
+ */
474
+ const computeBezier = (points, t) => {
475
+ if (t === 0) {
476
+ points[0].t = 0;
477
+ return points[0];
478
+ }
479
+ const order = points.length - 1;
480
+ if (t === 1) {
481
+ points[order].t = 1;
482
+ return points[order];
483
+ }
484
+ const mt = 1 - t;
485
+ let p = points;
486
+ if (order === 0) {
487
+ points[0].t = t;
488
+ return points[0];
489
+ }
490
+ if (order === 1) return {
491
+ x: mt * p[0].x + t * p[1].x,
492
+ y: mt * p[0].y + t * p[1].y,
493
+ t
494
+ };
495
+ const mt2 = mt * mt;
496
+ const t2 = t * t;
497
+ let a = 0;
498
+ let b = 0;
499
+ let c = 0;
500
+ let d = 0;
501
+ if (order === 2) {
502
+ p = [
503
+ p[0],
504
+ p[1],
505
+ p[2],
506
+ {
507
+ x: 0,
508
+ y: 0
509
+ }
510
+ ];
511
+ a = mt2;
512
+ b = mt * t * 2;
513
+ c = t2;
514
+ } else if (order === 3) {
515
+ a = mt2 * mt;
516
+ b = mt2 * t * 3;
517
+ c = mt * t2 * 3;
518
+ d = t * t2;
519
+ }
520
+ return {
521
+ x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x,
522
+ y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y,
523
+ t
524
+ };
525
+ };
526
+ const calculateBezier = (derivativeFn, t) => {
527
+ const d = derivativeFn(t);
528
+ const l = d.x * d.x + d.y * d.y;
529
+ return Math.sqrt(l);
530
+ };
531
+ const bezierLength = (derivativeFn) => {
532
+ const z = .5;
533
+ const len = Tvalues.length;
534
+ let sum = 0;
535
+ for (let i = 0, t; i < len; i++) {
536
+ t = z * Tvalues[i] + z;
537
+ sum += Cvalues[i] * calculateBezier(derivativeFn, t);
538
+ }
539
+ return z * sum;
540
+ };
541
+ /**
542
+ * Returns the length of CubicBezier / Quad segment.
543
+ * @param curve cubic / quad bezier segment
544
+ */
545
+ const getBezierLength = (curve) => {
546
+ const points = [];
547
+ for (let idx = 0, len = curve.length, step = 2; idx < len; idx += step) points.push({
548
+ x: curve[idx],
549
+ y: curve[idx + 1]
550
+ });
551
+ const dpoints = deriveBezier(points);
552
+ return bezierLength((t) => {
553
+ return computeBezier(dpoints[0], t);
554
+ });
555
+ };
556
+ const CBEZIER_MINMAX_EPSILON = 1e-8;
557
+ /**
558
+ * Returns the most extreme points in a Quad Bezier segment.
559
+ * @param A an array which consist of X/Y values
560
+ */
561
+ const minmaxQ = ([v1, cp, v2]) => {
562
+ const min = Math.min(v1, v2);
563
+ const max = Math.max(v1, v2);
564
+ if (cp >= v1 ? v2 >= cp : v2 <= cp) return [min, max];
565
+ const E = (v1 * v2 - cp * cp) / (v1 - 2 * cp + v2);
566
+ return E < min ? [E, max] : [min, E];
567
+ };
568
+ /**
569
+ * Returns the most extreme points in a Cubic Bezier segment.
570
+ * @param A an array which consist of X/Y values
571
+ * @see https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L127
572
+ */
573
+ const minmaxC = ([v1, cp1, cp2, v2]) => {
574
+ const K = v1 - 3 * cp1 + 3 * cp2 - v2;
575
+ if (Math.abs(K) < 1e-8) {
576
+ if (v1 === v2 && v1 === cp1) return [v1, v2];
577
+ return minmaxQ([
578
+ v1,
579
+ -.5 * v1 + 1.5 * cp1,
580
+ v1 - 3 * cp1 + 3 * cp2
581
+ ]);
582
+ }
583
+ const T = -v1 * cp2 + v1 * v2 - cp1 * cp2 - cp1 * v2 + cp1 * cp1 + cp2 * cp2;
584
+ if (T <= 0) return [Math.min(v1, v2), Math.max(v1, v2)];
585
+ const S = Math.sqrt(T);
586
+ let min = Math.min(v1, v2);
587
+ let max = Math.max(v1, v2);
588
+ const L = v1 - 2 * cp1 + cp2;
589
+ for (let R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) if (R > 0 && R < 1) {
590
+ const Q = v1 * (1 - R) * (1 - R) * (1 - R) + cp1 * 3 * (1 - R) * (1 - R) * R + cp2 * 3 * (1 - R) * R * R + v2 * R * R * R;
591
+ if (Q < min) min = Q;
592
+ if (Q > max) max = Q;
593
+ }
594
+ return [min, max];
595
+ };
596
+ const bezierTools = {
597
+ bezierLength,
598
+ calculateBezier,
599
+ CBEZIER_MINMAX_EPSILON,
600
+ computeBezier,
601
+ Cvalues,
602
+ deriveBezier,
603
+ getBezierLength,
604
+ minmaxC,
605
+ minmaxQ,
606
+ Tvalues
607
+ };
608
+ //#endregion
609
+ //#region src/math/cubicTools.ts
610
+ /**
611
+ * Returns a point at a given length of a CubicBezier segment.
612
+ *
613
+ * @param x1 the starting point X
614
+ * @param y1 the starting point Y
615
+ * @param c1x the first control point X
616
+ * @param c1y the first control point Y
617
+ * @param c2x the second control point X
618
+ * @param c2y the second control point Y
619
+ * @param x2 the ending point X
620
+ * @param y2 the ending point Y
621
+ * @param t a [0-1] ratio
622
+ * @returns the point at cubic-bezier segment length
623
+ */
624
+ const getPointAtCubicSegmentLength = ([x1, y1, c1x, c1y, c2x, c2y, x2, y2], t) => {
625
+ const t1 = 1 - t;
626
+ return {
627
+ x: t1 ** 3 * x1 + 3 * t1 ** 2 * t * c1x + 3 * t1 * t ** 2 * c2x + t ** 3 * x2,
628
+ y: t1 ** 3 * y1 + 3 * t1 ** 2 * t * c1y + 3 * t1 * t ** 2 * c2y + t ** 3 * y2
629
+ };
630
+ };
631
+ /**
632
+ * Returns the length of a CubicBezier segment.
633
+ *
634
+ * @param x1 the starting point X
635
+ * @param y1 the starting point Y
636
+ * @param c1x the first control point X
637
+ * @param c1y the first control point Y
638
+ * @param c2x the second control point X
639
+ * @param c2y the second control point Y
640
+ * @param x2 the ending point X
641
+ * @param y2 the ending point Y
642
+ * @returns the CubicBezier segment length
643
+ */
644
+ const getCubicLength = (x1, y1, c1x, c1y, c2x, c2y, x2, y2) => {
645
+ return getBezierLength([
646
+ x1,
647
+ y1,
648
+ c1x,
649
+ c1y,
650
+ c2x,
651
+ c2y,
652
+ x2,
653
+ y2
654
+ ]);
655
+ };
656
+ /**
657
+ * Returns the point along a CubicBezier segment at a given distance.
658
+ *
659
+ * @param x1 the starting point X
660
+ * @param y1 the starting point Y
661
+ * @param c1x the first control point X
662
+ * @param c1y the first control point Y
663
+ * @param c2x the second control point X
664
+ * @param c2y the second control point Y
665
+ * @param x2 the ending point X
666
+ * @param y2 the ending point Y
667
+ * @param distance the distance to look at
668
+ * @returns the point at CubicBezier length
669
+ */
670
+ const getPointAtCubicLength = (x1, y1, c1x, c1y, c2x, c2y, x2, y2, distance) => {
671
+ const distanceIsNumber = typeof distance === "number";
672
+ let point = {
673
+ x: x1,
674
+ y: y1
675
+ };
676
+ if (distanceIsNumber) {
677
+ const currentLength = getBezierLength([
678
+ x1,
679
+ y1,
680
+ c1x,
681
+ c1y,
682
+ c2x,
683
+ c2y,
684
+ x2,
685
+ y2
686
+ ]);
687
+ if (distance <= 0) {} else if (distance >= currentLength) point = {
688
+ x: x2,
689
+ y: y2
690
+ };
691
+ else point = getPointAtCubicSegmentLength([
692
+ x1,
693
+ y1,
694
+ c1x,
695
+ c1y,
696
+ c2x,
697
+ c2y,
698
+ x2,
699
+ y2
700
+ ], distance / currentLength);
701
+ }
702
+ return point;
703
+ };
704
+ /**
705
+ * Returns the bounding box of a CubicBezier segment in the following format:
706
+ * [MIN_X, MIN_Y, MAX_X, MAX_Y]
707
+ *
708
+ * @param x1 the starting point X
709
+ * @param y1 the starting point Y
710
+ * @param c1x the first control point X
711
+ * @param c1y the first control point Y
712
+ * @param c2x the second control point X
713
+ * @param c2y the second control point Y
714
+ * @param x2 the ending point X
715
+ * @param y2 the ending point Y
716
+ * @returns the extrema of the CubicBezier segment
717
+ */
718
+ const getCubicBBox = (x1, y1, c1x, c1y, c2x, c2y, x2, y2) => {
719
+ const cxMinMax = minmaxC([
720
+ x1,
721
+ c1x,
722
+ c2x,
723
+ x2
724
+ ]);
725
+ const cyMinMax = minmaxC([
726
+ y1,
727
+ c1y,
728
+ c2y,
729
+ y2
730
+ ]);
731
+ return [
732
+ cxMinMax[0],
733
+ cyMinMax[0],
734
+ cxMinMax[1],
735
+ cyMinMax[1]
736
+ ];
737
+ };
738
+ const cubicTools = {
739
+ getCubicBBox,
740
+ getCubicLength,
741
+ getPointAtCubicLength,
742
+ getPointAtCubicSegmentLength
743
+ };
744
+ //#endregion
745
+ //#region src/math/quadTools.ts
746
+ /**
747
+ * Returns the {x,y} coordinates of a point at a
748
+ * given length of a quadratic-bezier segment.
749
+ *
750
+ * @see https://github.com/substack/point-at-length
751
+ *
752
+ * @param x1 the starting point X
753
+ * @param y1 the starting point Y
754
+ * @param cx the control point X
755
+ * @param cy the control point Y
756
+ * @param x2 the ending point X
757
+ * @param y2 the ending point Y
758
+ * @param t a [0-1] ratio
759
+ * @returns the requested {x,y} coordinates
760
+ */
761
+ const getPointAtQuadSegmentLength = ([x1, y1, cx, cy, x2, y2], t) => {
762
+ const t1 = 1 - t;
763
+ return {
764
+ x: t1 ** 2 * x1 + 2 * t1 * t * cx + t ** 2 * x2,
765
+ y: t1 ** 2 * y1 + 2 * t1 * t * cy + t ** 2 * y2
766
+ };
767
+ };
768
+ /**
769
+ * Returns the length of a QuadraticBezier segment.
770
+ *
771
+ * @param x1 the starting point X
772
+ * @param y1 the starting point Y
773
+ * @param cx the control point X
774
+ * @param cy the control point Y
775
+ * @param x2 the ending point X
776
+ * @param y2 the ending point Y
777
+ * @returns the QuadraticBezier segment length
778
+ */
779
+ const getQuadLength = (x1, y1, cx, cy, x2, y2) => {
780
+ return getBezierLength([
781
+ x1,
782
+ y1,
783
+ cx,
784
+ cy,
785
+ x2,
786
+ y2
787
+ ]);
788
+ };
789
+ /**
790
+ * Returns the point along a QuadraticBezier segment at a given distance.
791
+ *
792
+ * @param x1 the starting point X
793
+ * @param y1 the starting point Y
794
+ * @param cx the control point X
795
+ * @param cy the control point Y
796
+ * @param x2 the ending point X
797
+ * @param y2 the ending point Y
798
+ * @param distance the distance to look at
799
+ * @returns the point at QuadraticBezier length
800
+ */
801
+ const getPointAtQuadLength = (x1, y1, cx, cy, x2, y2, distance) => {
802
+ const distanceIsNumber = typeof distance === "number";
803
+ let point = {
804
+ x: x1,
805
+ y: y1
806
+ };
807
+ if (distanceIsNumber) {
808
+ const currentLength = getBezierLength([
809
+ x1,
810
+ y1,
811
+ cx,
812
+ cy,
813
+ x2,
814
+ y2
815
+ ]);
816
+ if (distance <= 0) {} else if (distance >= currentLength) point = {
817
+ x: x2,
818
+ y: y2
819
+ };
820
+ else point = getPointAtQuadSegmentLength([
821
+ x1,
822
+ y1,
823
+ cx,
824
+ cy,
825
+ x2,
826
+ y2
827
+ ], distance / currentLength);
828
+ }
829
+ return point;
830
+ };
831
+ /**
832
+ * Returns the bounding box of a QuadraticBezier segment in the following format:
833
+ * [MIN_X, MIN_Y, MAX_X, MAX_Y]
834
+ *
835
+ * @param x1 the starting point X
836
+ * @param y1 the starting point Y
837
+ * @param cx the control point X
838
+ * @param cy the control point Y
839
+ * @param x2 the ending point X
840
+ * @param y2 the ending point Y
841
+ * @returns the extrema of the QuadraticBezier segment
842
+ */
843
+ const getQuadBBox = (x1, y1, cx, cy, x2, y2) => {
844
+ const cxMinMax = minmaxQ([
845
+ x1,
846
+ cx,
847
+ x2
848
+ ]);
849
+ const cyMinMax = minmaxQ([
850
+ y1,
851
+ cy,
852
+ y2
853
+ ]);
854
+ return [
855
+ cxMinMax[0],
856
+ cyMinMax[0],
857
+ cxMinMax[1],
858
+ cyMinMax[1]
859
+ ];
860
+ };
861
+ const quadTools = {
862
+ getPointAtQuadLength,
863
+ getPointAtQuadSegmentLength,
864
+ getQuadBBox,
865
+ getQuadLength
866
+ };
867
+ //#endregion
868
+ //#region src/math/polygonTools.ts
869
+ /**
870
+ * d3-polygon-area
871
+ * @see https://github.com/d3/d3-polygon
872
+ *
873
+ * Returns the area of a polygon.
874
+ *
875
+ * @param polygon Array of [x, y]
876
+ * @returns Signed area
877
+ */
878
+ const polygonArea = (polygon) => {
879
+ const n = polygon.length;
880
+ let i = -1;
881
+ let a;
882
+ let b = polygon[n - 1];
883
+ let area = 0;
884
+ while (++i < n) {
885
+ a = b;
886
+ b = polygon[i];
887
+ area += a[1] * b[0] - a[0] * b[1];
888
+ }
889
+ return area / 2;
890
+ };
891
+ /**
892
+ * d3-polygon-length
893
+ * https://github.com/d3/d3-polygon
894
+ *
895
+ * Returns the perimeter of a polygon.
896
+ *
897
+ * @param polygon an array of coordinates
898
+ * @returns the polygon length
899
+ */
900
+ const polygonLength = (polygon) => {
901
+ return polygon.reduce((length, point, i) => {
902
+ if (i) return length + distanceSquareRoot(polygon[i - 1], point);
903
+ return 0;
904
+ }, 0);
905
+ };
906
+ /**
907
+ * Computes the centroid (geometric center) of a polygon.
908
+ * Uses average of all endpoint coordinates (robust for polygons and curves).
909
+ *
910
+ * @param polygon A polygon with consists of [x, y] tuples
911
+ * @returns [x, y] centroid
912
+ */
913
+ const polygonCentroid = (polygon) => {
914
+ if (polygon.length === 0) return [0, 0];
915
+ let sumX = 0;
916
+ let sumY = 0;
917
+ for (const [x, y] of polygon) {
918
+ sumX += x;
919
+ sumY += y;
920
+ }
921
+ const count = polygon.length;
922
+ return [sumX / count, sumY / count];
923
+ };
924
+ const polygonTools = {
925
+ polygonArea,
926
+ polygonLength,
927
+ polygonCentroid
928
+ };
929
+ //#endregion
930
+ //#region src/math/rotateVector.ts
931
+ /**
932
+ * Returns an {x,y} vector rotated by a given
933
+ * angle in radian.
934
+ *
935
+ * @param x the initial vector x
936
+ * @param y the initial vector y
937
+ * @param rad the radian vector angle
938
+ * @returns the rotated vector
939
+ */
940
+ const rotateVector = (x, y, rad) => {
941
+ const { sin, cos } = Math;
942
+ return {
943
+ x: x * cos(rad) - y * sin(rad),
944
+ y: x * sin(rad) + y * cos(rad)
945
+ };
946
+ };
947
+ //#endregion
948
+ //#region src/math/roundTo.ts
949
+ /**
950
+ * Rounds a number to the specified number of decimal places.
951
+ *
952
+ * @param n - The number to round
953
+ * @param round - Number of decimal places
954
+ * @returns The rounded number
955
+ */
956
+ const roundTo = (n, round) => {
957
+ const pow = round >= 1 ? 10 ** round : 1;
958
+ return round > 0 ? Math.round(n * pow) / pow : Math.round(n);
959
+ };
960
+ //#endregion
961
+ //#region src/parser/paramsCount.ts
962
+ /** Segment params length */
963
+ const paramsCounts = {
964
+ a: 7,
965
+ c: 6,
966
+ h: 1,
967
+ l: 2,
968
+ m: 2,
969
+ r: 4,
970
+ q: 4,
971
+ s: 4,
972
+ t: 2,
973
+ v: 1,
974
+ z: 0
975
+ };
976
+ //#endregion
977
+ //#region src/parser/finalizeSegment.ts
978
+ /**
979
+ * Breaks the parsing of a pathString once a segment is finalized.
980
+ *
981
+ * @param path - The PathParser instance
982
+ */
983
+ const finalizeSegment = (path) => {
984
+ let pathCommand = path.pathValue[path.segmentStart];
985
+ let relativeCommand = pathCommand.toLowerCase();
986
+ const { data } = path;
987
+ while (data.length >= paramsCounts[relativeCommand]) {
988
+ if (relativeCommand === "m" && data.length > 2) {
989
+ path.segments.push([pathCommand].concat(data.splice(0, 2)));
990
+ relativeCommand = "l";
991
+ pathCommand = pathCommand === "m" ? "l" : "L";
992
+ } else path.segments.push([pathCommand].concat(data.splice(0, paramsCounts[relativeCommand])));
993
+ if (!paramsCounts[relativeCommand]) break;
994
+ }
995
+ };
996
+ //#endregion
997
+ //#region src/util/error.ts
998
+ /** Error prefix used in all SVGPathCommander TypeError messages. */
999
+ const error = "SVGPathCommanderError";
1000
+ //#endregion
1001
+ //#region src/parser/scanFlag.ts
1002
+ /**
1003
+ * Validates an A (arc-to) specific path command value.
1004
+ * Usually a `large-arc-flag` or `sweep-flag`.
1005
+ *
1006
+ * @param path - The PathParser instance
1007
+ */
1008
+ const scanFlag = (path) => {
1009
+ const { index, pathValue } = path;
1010
+ const code = pathValue.charCodeAt(index);
1011
+ if (code === 48) {
1012
+ path.param = 0;
1013
+ path.index += 1;
1014
+ return;
1015
+ }
1016
+ if (code === 49) {
1017
+ path.param = 1;
1018
+ path.index += 1;
1019
+ return;
1020
+ }
1021
+ path.err = `${error}: invalid Arc flag "${pathValue[index]}", expecting 0 or 1 at index ${index}`;
1022
+ };
1023
+ //#endregion
1024
+ //#region src/parser/isDigit.ts
1025
+ /**
1026
+ * Checks if a character is a digit.
1027
+ *
1028
+ * @param code the character to check
1029
+ * @returns check result
1030
+ */
1031
+ const isDigit = (code) => {
1032
+ return code >= 48 && code <= 57;
1033
+ };
1034
+ //#endregion
1035
+ //#region src/parser/invalidPathValue.ts
1036
+ /** Error message prefix used when a path string cannot be parsed. */
1037
+ const invalidPathValue = "Invalid path value";
1038
+ //#endregion
1039
+ //#region src/parser/scanParam.ts
1040
+ /**
1041
+ * Validates every character of the path string,
1042
+ * every path command, negative numbers or floating point numbers.
1043
+ *
1044
+ * @param path - The PathParser instance
1045
+ */
1046
+ const scanParam = (path) => {
1047
+ const { max, pathValue, index: start } = path;
1048
+ let index = start;
1049
+ let zeroFirst = false;
1050
+ let hasCeiling = false;
1051
+ let hasDecimal = false;
1052
+ let hasDot = false;
1053
+ let ch;
1054
+ if (index >= max) {
1055
+ path.err = `${error}: ${invalidPathValue} at index ${index}, "pathValue" is missing param`;
1056
+ return;
1057
+ }
1058
+ ch = pathValue.charCodeAt(index);
1059
+ if (ch === 43 || ch === 45) {
1060
+ index += 1;
1061
+ ch = pathValue.charCodeAt(index);
1062
+ }
1063
+ if (!isDigit(ch) && ch !== 46) {
1064
+ path.err = `${error}: ${invalidPathValue} at index ${index}, "${pathValue[index]}" is not a number`;
1065
+ return;
1066
+ }
1067
+ if (ch !== 46) {
1068
+ zeroFirst = ch === 48;
1069
+ index += 1;
1070
+ ch = pathValue.charCodeAt(index);
1071
+ if (zeroFirst && index < max) {
1072
+ if (ch && isDigit(ch)) {
1073
+ path.err = `${error}: ${invalidPathValue} at index ${start}, "${pathValue[start]}" illegal number`;
1074
+ return;
1075
+ }
1076
+ }
1077
+ while (index < max && isDigit(pathValue.charCodeAt(index))) {
1078
+ index += 1;
1079
+ hasCeiling = true;
1080
+ }
1081
+ ch = pathValue.charCodeAt(index);
1082
+ }
1083
+ if (ch === 46) {
1084
+ hasDot = true;
1085
+ index += 1;
1086
+ while (isDigit(pathValue.charCodeAt(index))) {
1087
+ index += 1;
1088
+ hasDecimal = true;
1089
+ }
1090
+ ch = pathValue.charCodeAt(index);
1091
+ }
1092
+ if (ch === 101 || ch === 69) {
1093
+ if (hasDot && !hasCeiling && !hasDecimal) {
1094
+ path.err = `${error}: ${invalidPathValue} at index ${index}, "${pathValue[index]}" invalid float exponent`;
1095
+ return;
1096
+ }
1097
+ index += 1;
1098
+ ch = pathValue.charCodeAt(index);
1099
+ if (ch === 43 || ch === 45) index += 1;
1100
+ if (index < max && isDigit(pathValue.charCodeAt(index))) while (index < max && isDigit(pathValue.charCodeAt(index))) index += 1;
1101
+ else {
1102
+ path.err = `${error}: ${invalidPathValue} at index ${index}, "${pathValue[index]}" invalid integer exponent`;
1103
+ return;
1104
+ }
1105
+ }
1106
+ path.index = index;
1107
+ path.param = +path.pathValue.slice(start, index);
1108
+ };
1109
+ //#endregion
1110
+ //#region src/parser/isSpace.ts
1111
+ /**
1112
+ * Checks if the character is a space.
1113
+ *
1114
+ * @param ch the character to check
1115
+ * @returns check result
1116
+ */
1117
+ const isSpace = (ch) => {
1118
+ return [
1119
+ 5760,
1120
+ 6158,
1121
+ 8192,
1122
+ 8193,
1123
+ 8194,
1124
+ 8195,
1125
+ 8196,
1126
+ 8197,
1127
+ 8198,
1128
+ 8199,
1129
+ 8200,
1130
+ 8201,
1131
+ 8202,
1132
+ 8239,
1133
+ 8287,
1134
+ 12288,
1135
+ 65279,
1136
+ 10,
1137
+ 13,
1138
+ 8232,
1139
+ 8233,
1140
+ 32,
1141
+ 9,
1142
+ 11,
1143
+ 12,
1144
+ 160
1145
+ ].includes(ch);
1146
+ };
1147
+ //#endregion
1148
+ //#region src/parser/skipSpaces.ts
1149
+ /**
1150
+ * Points the parser to the next character in the
1151
+ * path string every time it encounters any kind of
1152
+ * space character.
1153
+ *
1154
+ * @param path - The PathParser instance
1155
+ */
1156
+ const skipSpaces = (path) => {
1157
+ const { pathValue, max } = path;
1158
+ while (path.index < max && isSpace(pathValue.charCodeAt(path.index))) path.index += 1;
1159
+ };
1160
+ //#endregion
1161
+ //#region src/parser/isPathCommand.ts
1162
+ /**
1163
+ * Checks if the character is a path command.
1164
+ *
1165
+ * @param code the character to check
1166
+ * @returns check result
1167
+ */
1168
+ const isPathCommand = (code) => {
1169
+ switch (code | 32) {
1170
+ case 109:
1171
+ case 122:
1172
+ case 108:
1173
+ case 104:
1174
+ case 118:
1175
+ case 99:
1176
+ case 115:
1177
+ case 113:
1178
+ case 116:
1179
+ case 97: return true;
1180
+ default: return false;
1181
+ }
1182
+ };
1183
+ //#endregion
1184
+ //#region src/parser/isDigitStart.ts
1185
+ /**
1186
+ * Checks if the character is or belongs to a number.
1187
+ * [0-9]|+|-|.
1188
+ *
1189
+ * @param code the character to check
1190
+ * @returns check result
1191
+ */
1192
+ const isDigitStart = (code) => {
1193
+ return isDigit(code) || code === 43 || code === 45 || code === 46;
1194
+ };
1195
+ //#endregion
1196
+ //#region src/parser/isArcCommand.ts
1197
+ /**
1198
+ * Checks if the character is an A (arc-to) path command.
1199
+ *
1200
+ * @param code the character to check
1201
+ * @returns check result
1202
+ */
1203
+ const isArcCommand = (code) => {
1204
+ return (code | 32) === 97;
1205
+ };
1206
+ //#endregion
1207
+ //#region src/parser/isMoveCommand.ts
1208
+ /**
1209
+ * Checks if the character is a MoveTo command.
1210
+ *
1211
+ * @param code the character to check
1212
+ * @returns check result
1213
+ */
1214
+ const isMoveCommand = (code) => {
1215
+ switch (code | 32) {
1216
+ case 109:
1217
+ case 77: return true;
1218
+ default: return false;
1219
+ }
1220
+ };
1221
+ //#endregion
1222
+ //#region src/parser/scanSegment.ts
1223
+ /**
1224
+ * Scans every character in the path string to determine
1225
+ * where a segment starts and where it ends.
1226
+ *
1227
+ * @param path - The PathParser instance
1228
+ */
1229
+ const scanSegment = (path) => {
1230
+ const { max, pathValue, index, segments } = path;
1231
+ const cmdCode = pathValue.charCodeAt(index);
1232
+ const reqParams = paramsCounts[pathValue[index].toLowerCase()];
1233
+ path.segmentStart = index;
1234
+ if (!isPathCommand(cmdCode)) {
1235
+ path.err = `${error}: ${invalidPathValue} "${pathValue[index]}" is not a path command at index ${index}`;
1236
+ return;
1237
+ }
1238
+ const lastSegment = segments[segments.length - 1];
1239
+ if (!isMoveCommand(cmdCode) && lastSegment?.[0]?.toLocaleLowerCase() === "z") {
1240
+ path.err = `${error}: ${invalidPathValue} "${pathValue[index]}" is not a MoveTo path command at index ${index}`;
1241
+ return;
1242
+ }
1243
+ path.index += 1;
1244
+ skipSpaces(path);
1245
+ path.data = [];
1246
+ if (!reqParams) {
1247
+ finalizeSegment(path);
1248
+ return;
1249
+ }
1250
+ for (;;) {
1251
+ for (let i = reqParams; i > 0; i -= 1) {
1252
+ if (isArcCommand(cmdCode) && (i === 3 || i === 4)) scanFlag(path);
1253
+ else scanParam(path);
1254
+ if (path.err.length) return;
1255
+ path.data.push(path.param);
1256
+ skipSpaces(path);
1257
+ if (path.index < max && pathValue.charCodeAt(path.index) === 44) {
1258
+ path.index += 1;
1259
+ skipSpaces(path);
1260
+ }
1261
+ }
1262
+ if (path.index >= path.max) break;
1263
+ if (!isDigitStart(pathValue.charCodeAt(path.index))) break;
1264
+ }
1265
+ finalizeSegment(path);
1266
+ };
1267
+ //#endregion
1268
+ //#region src/parser/pathParser.ts
1269
+ /**
1270
+ * The `PathParser` is used by the `parsePathString` static method
1271
+ * to generate a `pathArray`.
1272
+ *
1273
+ * @param pathString - The SVG path string to parse
1274
+ */
1275
+ var PathParser = class {
1276
+ constructor(pathString) {
1277
+ this.segments = [];
1278
+ this.pathValue = pathString;
1279
+ this.max = pathString.length;
1280
+ this.index = 0;
1281
+ this.param = 0;
1282
+ this.segmentStart = 0;
1283
+ this.data = [];
1284
+ this.err = "";
1285
+ }
1286
+ };
1287
+ //#endregion
1288
+ //#region src/parser/parsePathString.ts
1289
+ /**
1290
+ * Parses a path string value and returns an array
1291
+ * of segments we like to call `PathArray`.
1292
+ *
1293
+ * If parameter value is already a `PathArray`,
1294
+ * return a clone of it.
1295
+
1296
+ * @example
1297
+ * parsePathString("M 0 0L50 50")
1298
+ * // => [["M",0,0],["L",50,50]]
1299
+ *
1300
+ * @param pathInput the string to be parsed
1301
+ * @returns the resulted `pathArray` or error string
1302
+ */
1303
+ const parsePathString = (pathInput) => {
1304
+ if (typeof pathInput !== "string") return pathInput.slice(0);
1305
+ const path = new PathParser(pathInput);
1306
+ skipSpaces(path);
1307
+ while (path.index < path.max && !path.err.length) scanSegment(path);
1308
+ if (!path.err.length) {
1309
+ if (path.segments.length)
1310
+ /**
1311
+ * force absolute first M
1312
+ * getPathBBox calculation requires first segment to be absolute
1313
+ * @see https://github.com/thednp/svg-path-commander/pull/49
1314
+ */
1315
+ path.segments[0][0] = "M";
1316
+ } else throw TypeError(path.err);
1317
+ return path.segments;
1318
+ };
1319
+ //#endregion
1320
+ //#region src/process/absolutizeSegment.ts
1321
+ /**
1322
+ * Returns an absolute segment of a `PathArray` object.
1323
+ *
1324
+ * @param segment the segment object
1325
+ * @param index the segment index
1326
+ * @param lastX the last known X value
1327
+ * @param lastY the last known Y value
1328
+ * @returns the absolute segment
1329
+ */
1330
+ const absolutizeSegment = (segment, index, lastX, lastY) => {
1331
+ const [pathCommand] = segment;
1332
+ const absCommand = pathCommand.toUpperCase();
1333
+ if (index === 0 || absCommand === pathCommand) return segment;
1334
+ if (absCommand === "A") return [
1335
+ absCommand,
1336
+ segment[1],
1337
+ segment[2],
1338
+ segment[3],
1339
+ segment[4],
1340
+ segment[5],
1341
+ segment[6] + lastX,
1342
+ segment[7] + lastY
1343
+ ];
1344
+ else if (absCommand === "V") return [absCommand, segment[1] + lastY];
1345
+ else if (absCommand === "H") return [absCommand, segment[1] + lastX];
1346
+ else if (absCommand === "L") return [
1347
+ absCommand,
1348
+ segment[1] + lastX,
1349
+ segment[2] + lastY
1350
+ ];
1351
+ else {
1352
+ const absValues = [];
1353
+ const seglen = segment.length;
1354
+ for (let j = 1; j < seglen; j += 1) absValues.push(segment[j] + (j % 2 ? lastX : lastY));
1355
+ return [absCommand].concat(absValues);
1356
+ }
1357
+ };
1358
+ //#endregion
1359
+ //#region src/process/iterate.ts
1360
+ /**
1361
+ * Iterates over a `PathArray`, executing a callback for each segment.
1362
+ * The callback can:
1363
+ * - Read current position (`x`, `y`)
1364
+ * - Modify the segment (return new segment)
1365
+ * - Stop early (return `false`)
1366
+ *
1367
+ * The iterator maintains accurate current point (`x`, `y`) and subpath start (`mx`, `my`)
1368
+ * while correctly handling relative/absolute commands, including H/V and Z.
1369
+ *
1370
+ * **Important**: If the callback returns a new segment with more coordinates (e.g., Q → C),
1371
+ * the path length may increase, and iteration will continue over new segments.
1372
+ *
1373
+ * @template T - Specific PathArray type (e.g., CurveArray, PolylineArray)
1374
+ * @param path - The source `PathArray` to iterate over
1375
+ * @param iterator - Callback function for each segment
1376
+ * @param iterator.segment - Current path segment
1377
+ * @param iterator.index - Index of current segment
1378
+ * @param iterator.x - Current X position (after applying relative offset)
1379
+ * @param iterator.y - Current Y position (after applying relative offset)
1380
+ * @returns The modified `path` (or original if no changes)
1381
+ *
1382
+ * @example
1383
+ * iterate(path, (seg, i, x, y) => {
1384
+ * if (seg[0] === 'L') return ['C', x, y, seg[1], seg[2], seg[1], seg[2]];
1385
+ * });
1386
+ */
1387
+ const iterate = (path, iterator) => {
1388
+ let x = 0;
1389
+ let y = 0;
1390
+ let mx = 0;
1391
+ let my = 0;
1392
+ let i = 0;
1393
+ while (i < path.length) {
1394
+ const segment = path[i];
1395
+ const [pathCommand] = segment;
1396
+ const absCommand = pathCommand.toUpperCase();
1397
+ const isRelative = absCommand !== pathCommand;
1398
+ const iteratorResult = iterator(segment, i, x, y);
1399
+ if (iteratorResult === false) break;
1400
+ if (absCommand === "Z") {
1401
+ x = mx;
1402
+ y = my;
1403
+ } else if (absCommand === "H") x = segment[1] + (isRelative ? x : 0);
1404
+ else if (absCommand === "V") y = segment[1] + (isRelative ? y : 0);
1405
+ else {
1406
+ const segLen = segment.length;
1407
+ x = segment[segLen - 2] + (isRelative ? x : 0);
1408
+ y = segment[segLen - 1] + (isRelative ? y : 0);
1409
+ if (absCommand === "M") {
1410
+ mx = x;
1411
+ my = y;
1412
+ }
1413
+ }
1414
+ if (iteratorResult) path[i] = iteratorResult;
1415
+ i += 1;
1416
+ }
1417
+ return path;
1418
+ };
1419
+ //#endregion
1420
+ //#region src/convert/pathToAbsolute.ts
1421
+ /**
1422
+ * Parses a path string value or object and returns an array
1423
+ * of segments, all converted to absolute values.
1424
+ *
1425
+ * @param pathInput - The path string or PathArray
1426
+ * @returns The resulted PathArray with absolute values
1427
+ *
1428
+ * @example
1429
+ * ```ts
1430
+ * pathToAbsolute('M10 10l80 80')
1431
+ * // => [['M', 10, 10], ['L', 90, 90]]
1432
+ * ```
1433
+ */
1434
+ const pathToAbsolute = (pathInput) => {
1435
+ return iterate(parsePathString(pathInput), absolutizeSegment);
1436
+ };
1437
+ //#endregion
1438
+ //#region src/process/relativizeSegment.ts
1439
+ /**
1440
+ * Returns a relative segment of a `PathArray` object.
1441
+ *
1442
+ * @param segment the segment object
1443
+ * @param index the segment index
1444
+ * @param lastX the last known X value
1445
+ * @param lastY the last known Y value
1446
+ * @returns the relative segment
1447
+ */
1448
+ const relativizeSegment = (segment, index, lastX, lastY) => {
1449
+ const [pathCommand] = segment;
1450
+ const relCommand = pathCommand.toLowerCase();
1451
+ if (index === 0 || pathCommand === relCommand) return segment;
1452
+ if (relCommand === "a") return [
1453
+ relCommand,
1454
+ segment[1],
1455
+ segment[2],
1456
+ segment[3],
1457
+ segment[4],
1458
+ segment[5],
1459
+ segment[6] - lastX,
1460
+ segment[7] - lastY
1461
+ ];
1462
+ else if (relCommand === "v") return [relCommand, segment[1] - lastY];
1463
+ else if (relCommand === "h") return [relCommand, segment[1] - lastX];
1464
+ else if (relCommand === "l") return [
1465
+ relCommand,
1466
+ segment[1] - lastX,
1467
+ segment[2] - lastY
1468
+ ];
1469
+ else {
1470
+ const relValues = [];
1471
+ const seglen = segment.length;
1472
+ for (let j = 1; j < seglen; j += 1) relValues.push(segment[j] - (j % 2 ? lastX : lastY));
1473
+ return [relCommand].concat(relValues);
1474
+ }
1475
+ };
1476
+ //#endregion
1477
+ //#region src/convert/pathToRelative.ts
1478
+ /**
1479
+ * Parses a path string value or object and returns an array
1480
+ * of segments, all converted to relative values.
1481
+ *
1482
+ * @param pathInput - The path string or PathArray
1483
+ * @returns The resulted PathArray with relative values
1484
+ *
1485
+ * @example
1486
+ * ```ts
1487
+ * pathToRelative('M10 10L90 90')
1488
+ * // => [['M', 10, 10], ['l', 80, 80]]
1489
+ * ```
1490
+ */
1491
+ const pathToRelative = (pathInput) => {
1492
+ return iterate(parsePathString(pathInput), relativizeSegment);
1493
+ };
1494
+ //#endregion
1495
+ //#region src/process/arcToCubic.ts
1496
+ /**
1497
+ * Converts A (arc-to) segments to C (cubic-bezier-to).
1498
+ *
1499
+ * For more information of where this math came from visit:
1500
+ * http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
1501
+ *
1502
+ * @param X1 the starting x position
1503
+ * @param Y1 the starting y position
1504
+ * @param RX x-radius of the arc
1505
+ * @param RY y-radius of the arc
1506
+ * @param angle x-axis-rotation of the arc
1507
+ * @param LAF large-arc-flag of the arc
1508
+ * @param SF sweep-flag of the arc
1509
+ * @param X2 the ending x position
1510
+ * @param Y2 the ending y position
1511
+ * @param recursive the parameters needed to split arc into 2 segments
1512
+ * @returns the resulting cubic-bezier segment(s)
1513
+ */
1514
+ const arcToCubic = (X1, Y1, RX, RY, angle, LAF, SF, X2, Y2, recursive) => {
1515
+ let x1 = X1;
1516
+ let y1 = Y1;
1517
+ let rx = RX;
1518
+ let ry = RY;
1519
+ let x2 = X2;
1520
+ let y2 = Y2;
1521
+ const d120 = Math.PI * 120 / 180;
1522
+ const rad = Math.PI / 180 * (+angle || 0);
1523
+ let res = [];
1524
+ let xy;
1525
+ let f1;
1526
+ let f2;
1527
+ let cx;
1528
+ let cy;
1529
+ if (!recursive) {
1530
+ xy = rotateVector(x1, y1, -rad);
1531
+ x1 = xy.x;
1532
+ y1 = xy.y;
1533
+ xy = rotateVector(x2, y2, -rad);
1534
+ x2 = xy.x;
1535
+ y2 = xy.y;
1536
+ const x = (x1 - x2) / 2;
1537
+ const y = (y1 - y2) / 2;
1538
+ let h = x * x / (rx * rx) + y * y / (ry * ry);
1539
+ if (h > 1) {
1540
+ h = Math.sqrt(h);
1541
+ rx *= h;
1542
+ ry *= h;
1543
+ }
1544
+ const rx2 = rx * rx;
1545
+ const ry2 = ry * ry;
1546
+ const k = (LAF === SF ? -1 : 1) * Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)));
1547
+ cx = k * rx * y / ry + (x1 + x2) / 2;
1548
+ cy = k * -ry * x / rx + (y1 + y2) / 2;
1549
+ f1 = Math.asin(((y1 - cy) / ry * 10 ** 9 >> 0) / 10 ** 9);
1550
+ f2 = Math.asin(((y2 - cy) / ry * 10 ** 9 >> 0) / 10 ** 9);
1551
+ f1 = x1 < cx ? Math.PI - f1 : f1;
1552
+ f2 = x2 < cx ? Math.PI - f2 : f2;
1553
+ if (f1 < 0) f1 = Math.PI * 2 + f1;
1554
+ if (f2 < 0) f2 = Math.PI * 2 + f2;
1555
+ if (SF && f1 > f2) f1 -= Math.PI * 2;
1556
+ if (!SF && f2 > f1) f2 -= Math.PI * 2;
1557
+ } else [f1, f2, cx, cy] = recursive;
1558
+ let df = f2 - f1;
1559
+ if (Math.abs(df) > d120) {
1560
+ const f2old = f2;
1561
+ const x2old = x2;
1562
+ const y2old = y2;
1563
+ f2 = f1 + d120 * (SF && f2 > f1 ? 1 : -1);
1564
+ x2 = cx + rx * Math.cos(f2);
1565
+ y2 = cy + ry * Math.sin(f2);
1566
+ res = arcToCubic(x2, y2, rx, ry, angle, 0, SF, x2old, y2old, [
1567
+ f2,
1568
+ f2old,
1569
+ cx,
1570
+ cy
1571
+ ]);
1572
+ }
1573
+ df = f2 - f1;
1574
+ const c1 = Math.cos(f1);
1575
+ const s1 = Math.sin(f1);
1576
+ const c2 = Math.cos(f2);
1577
+ const s2 = Math.sin(f2);
1578
+ const t = Math.tan(df / 4);
1579
+ const hx = 4 / 3 * rx * t;
1580
+ const hy = 4 / 3 * ry * t;
1581
+ const m1 = [x1, y1];
1582
+ const m2 = [x1 + hx * s1, y1 - hy * c1];
1583
+ const m3 = [x2 + hx * s2, y2 - hy * c2];
1584
+ const m4 = [x2, y2];
1585
+ m2[0] = 2 * m1[0] - m2[0];
1586
+ m2[1] = 2 * m1[1] - m2[1];
1587
+ if (recursive) return [
1588
+ m2[0],
1589
+ m2[1],
1590
+ m3[0],
1591
+ m3[1],
1592
+ m4[0],
1593
+ m4[1]
1594
+ ].concat(res);
1595
+ res = [
1596
+ m2[0],
1597
+ m2[1],
1598
+ m3[0],
1599
+ m3[1],
1600
+ m4[0],
1601
+ m4[1]
1602
+ ].concat(res);
1603
+ const newres = [];
1604
+ for (let i = 0, ii = res.length; i < ii; i += 1) newres[i] = i % 2 ? rotateVector(res[i - 1], res[i], rad).y : rotateVector(res[i], res[i + 1], rad).x;
1605
+ return newres;
1606
+ };
1607
+ //#endregion
1608
+ //#region src/process/quadToCubic.ts
1609
+ /**
1610
+ * Converts a Q (quadratic-bezier) segment to C (cubic-bezier).
1611
+ *
1612
+ * @param x1 curve start x
1613
+ * @param y1 curve start y
1614
+ * @param qx control point x
1615
+ * @param qy control point y
1616
+ * @param x2 curve end x
1617
+ * @param y2 curve end y
1618
+ * @returns the cubic-bezier segment
1619
+ */
1620
+ const quadToCubic = (x1, y1, qx, qy, x2, y2) => {
1621
+ const r13 = 1 / 3;
1622
+ const r23 = 2 / 3;
1623
+ return [
1624
+ r13 * x1 + r23 * qx,
1625
+ r13 * y1 + r23 * qy,
1626
+ r13 * x2 + r23 * qx,
1627
+ r13 * y2 + r23 * qy,
1628
+ x2,
1629
+ y2
1630
+ ];
1631
+ };
1632
+ //#endregion
1633
+ //#region src/process/lineToCubic.ts
1634
+ /**
1635
+ * Converts an L (line-to) segment to C (cubic-bezier).
1636
+ *
1637
+ * @param x1 line start x
1638
+ * @param y1 line start y
1639
+ * @param x2 line end x
1640
+ * @param y2 line end y
1641
+ * @returns the cubic-bezier segment
1642
+ */
1643
+ const lineToCubic = (x1, y1, x2, y2) => {
1644
+ const c1 = midPoint([x1, y1], [x2, y2], 1 / 3);
1645
+ const c2 = midPoint([x1, y1], [x2, y2], 2 / 3);
1646
+ return [
1647
+ c1[0],
1648
+ c1[1],
1649
+ c2[0],
1650
+ c2[1],
1651
+ x2,
1652
+ y2
1653
+ ];
1654
+ };
1655
+ //#endregion
1656
+ //#region src/process/segmentToCubic.ts
1657
+ /**
1658
+ * Converts any segment to C (cubic-bezier).
1659
+ *
1660
+ * @param segment the source segment
1661
+ * @param params the source segment parameters
1662
+ * @returns the cubic-bezier segment
1663
+ */
1664
+ const segmentToCubic = (segment, params) => {
1665
+ const pathCommand = segment[0];
1666
+ const values = segment.slice(1).map(Number);
1667
+ const [x, y] = values;
1668
+ const { x1: px1, y1: py1 } = params;
1669
+ if (!"TQ".includes(pathCommand)) {
1670
+ params.qx = null;
1671
+ params.qy = null;
1672
+ }
1673
+ if (pathCommand === "M") {
1674
+ params.mx = x;
1675
+ params.my = y;
1676
+ params.x = x;
1677
+ params.y = y;
1678
+ return segment;
1679
+ } else if (pathCommand === "A") return ["C"].concat(arcToCubic(px1, py1, values[0], values[1], values[2], values[3], values[4], values[5], values[6]));
1680
+ else if (pathCommand === "Q") {
1681
+ params.qx = x;
1682
+ params.qy = y;
1683
+ return ["C"].concat(quadToCubic(px1, py1, values[0], values[1], values[2], values[3]));
1684
+ } else if (pathCommand === "L") return ["C"].concat(lineToCubic(px1, py1, x, y));
1685
+ else if (pathCommand === "Z") return ["C"].concat(lineToCubic(px1, py1, params.mx, params.my));
1686
+ return segment;
1687
+ };
1688
+ //#endregion
1689
+ //#region src/process/normalizeSegment.ts
1690
+ /**
1691
+ * Normalizes a single segment of a `pathArray` object.
1692
+ *
1693
+ * @param segment the segment object
1694
+ * @param params the normalization parameters
1695
+ * @returns the normalized segment
1696
+ */
1697
+ const normalizeSegment = (segment, params) => {
1698
+ const [pathCommand] = segment;
1699
+ const absCommand = pathCommand.toUpperCase();
1700
+ const isRelative = pathCommand !== absCommand;
1701
+ const { x1: px1, y1: py1, x2: px2, y2: py2, x, y } = params;
1702
+ const values = segment.slice(1);
1703
+ let absValues = values.map((n, j) => n + (isRelative ? j % 2 ? y : x : 0));
1704
+ if (!"TQ".includes(absCommand)) {
1705
+ params.qx = null;
1706
+ params.qy = null;
1707
+ }
1708
+ if (absCommand === "A") {
1709
+ absValues = values.slice(0, -2).concat(values[5] + (isRelative ? x : 0), values[6] + (isRelative ? y : 0));
1710
+ return ["A"].concat(absValues);
1711
+ } else if (absCommand === "H") return [
1712
+ "L",
1713
+ segment[1] + (isRelative ? x : 0),
1714
+ py1
1715
+ ];
1716
+ else if (absCommand === "V") return [
1717
+ "L",
1718
+ px1,
1719
+ segment[1] + (isRelative ? y : 0)
1720
+ ];
1721
+ else if (absCommand === "L") return [
1722
+ "L",
1723
+ segment[1] + (isRelative ? x : 0),
1724
+ segment[2] + (isRelative ? y : 0)
1725
+ ];
1726
+ else if (absCommand === "M") return [
1727
+ "M",
1728
+ segment[1] + (isRelative ? x : 0),
1729
+ segment[2] + (isRelative ? y : 0)
1730
+ ];
1731
+ else if (absCommand === "C") return ["C"].concat(absValues);
1732
+ else if (absCommand === "S") {
1733
+ const x1 = px1 * 2 - px2;
1734
+ const y1 = py1 * 2 - py2;
1735
+ params.x1 = x1;
1736
+ params.y1 = y1;
1737
+ return [
1738
+ "C",
1739
+ x1,
1740
+ y1
1741
+ ].concat(absValues);
1742
+ } else if (absCommand === "T") {
1743
+ const qx = px1 * 2 - (params.qx ? params.qx : 0);
1744
+ const qy = py1 * 2 - (params.qy ? params.qy : 0);
1745
+ params.qx = qx;
1746
+ params.qy = qy;
1747
+ return [
1748
+ "Q",
1749
+ qx,
1750
+ qy
1751
+ ].concat(absValues);
1752
+ } else if (absCommand === "Q") {
1753
+ const [nqx, nqy] = absValues;
1754
+ params.qx = nqx;
1755
+ params.qy = nqy;
1756
+ return ["Q"].concat(absValues);
1757
+ } else if (absCommand === "Z") return ["Z"];
1758
+ return segment;
1759
+ };
1760
+ //#endregion
1761
+ //#region src/parser/paramsParser.ts
1762
+ /**
1763
+ * Default parser parameters object used to track position state
1764
+ * while iterating through path segments.
1765
+ */
1766
+ const paramsParser = {
1767
+ mx: 0,
1768
+ my: 0,
1769
+ x1: 0,
1770
+ y1: 0,
1771
+ x2: 0,
1772
+ y2: 0,
1773
+ x: 0,
1774
+ y: 0,
1775
+ qx: null,
1776
+ qy: null
1777
+ };
1778
+ //#endregion
1779
+ //#region src/convert/pathToCurve.ts
1780
+ /**
1781
+ * Parses a path string or PathArray and returns a new one
1782
+ * in which all segments are converted to cubic-bezier.
1783
+ *
1784
+ * @param pathInput - The path string or PathArray
1785
+ * @returns The resulted CurveArray with all segments as cubic beziers
1786
+ *
1787
+ * @example
1788
+ * ```ts
1789
+ * pathToCurve('M10 50q15 -25 30 0')
1790
+ * // => [['M', 10, 50], ['C', 25, 25, 40, 50, 40, 50]]
1791
+ * ```
1792
+ */
1793
+ const pathToCurve = (pathInput) => {
1794
+ const params = { ...paramsParser };
1795
+ const path = parsePathString(pathInput);
1796
+ return iterate(path, (seg, index, lastX, lastY) => {
1797
+ params.x = lastX;
1798
+ params.y = lastY;
1799
+ const normalSegment = normalizeSegment(seg, params);
1800
+ if (normalSegment[0] === "M") {
1801
+ params.mx = normalSegment[1];
1802
+ params.my = normalSegment[2];
1803
+ }
1804
+ let result = segmentToCubic(normalSegment, params);
1805
+ if (result[0] === "C" && result.length > 7) {
1806
+ path.splice(index + 1, 0, ["C"].concat(result.slice(7)));
1807
+ result = result.slice(0, 7);
1808
+ }
1809
+ const seglen = result.length;
1810
+ params.x1 = +result[seglen - 2];
1811
+ params.y1 = +result[seglen - 1];
1812
+ params.x2 = +result[seglen - 4] || params.x1;
1813
+ params.y2 = +result[seglen - 3] || params.y1;
1814
+ return result;
1815
+ });
1816
+ };
1817
+ //#endregion
1818
+ //#region src/options/options.ts
1819
+ /** SVGPathCommander default options */
1820
+ const defaultOptions = {
1821
+ origin: [
1822
+ 0,
1823
+ 0,
1824
+ 0
1825
+ ],
1826
+ round: 4
1827
+ };
1828
+ //#endregion
1829
+ //#region src/convert/pathToString.ts
1830
+ /**
1831
+ * Returns a valid `d` attribute string value created
1832
+ * by rounding values and concatenating the PathArray segments.
1833
+ *
1834
+ * @param path - The PathArray object
1835
+ * @param roundOption - Amount of decimals to round values to, or "off"
1836
+ * @returns The concatenated path string
1837
+ *
1838
+ * @example
1839
+ * ```ts
1840
+ * pathToString([['M', 10, 10], ['L', 90, 90]], 2)
1841
+ * // => 'M10 10L90 90'
1842
+ * ```
1843
+ */
1844
+ const pathToString = (path, roundOption) => {
1845
+ const pathLen = path.length;
1846
+ let { round } = defaultOptions;
1847
+ let segment = path[0];
1848
+ let result = "";
1849
+ round = roundOption === "off" ? roundOption : typeof roundOption === "number" && roundOption >= 0 ? roundOption : typeof round === "number" && round >= 0 ? round : "off";
1850
+ for (let i = 0; i < pathLen; i += 1) {
1851
+ segment = path[i];
1852
+ const [pathCommand] = segment;
1853
+ const values = segment.slice(1);
1854
+ result += pathCommand;
1855
+ if (round === "off") result += values.join(" ");
1856
+ else {
1857
+ let j = 0;
1858
+ const valLen = values.length;
1859
+ while (j < valLen) {
1860
+ result += roundTo(values[j], round);
1861
+ if (j !== valLen - 1) result += " ";
1862
+ j += 1;
1863
+ }
1864
+ }
1865
+ }
1866
+ return result;
1867
+ };
1868
+ //#endregion
1869
+ //#region src/util/getPathBBox.ts
1870
+ /**
1871
+ * Calculates the bounding box of a path.
1872
+ *
1873
+ * @param pathInput - The path string or PathArray
1874
+ * @returns An object with width, height, x, y, x2, y2, cx, cy, cz properties
1875
+ *
1876
+ * @example
1877
+ * ```ts
1878
+ * getPathBBox('M0 0L100 0L100 100L0 100Z')
1879
+ * // => { x: 0, y: 0, width: 100, height: 100, x2: 100, y2: 100, cx: 50, cy: 50, cz: 150 }
1880
+ * ```
1881
+ */
1882
+ const getPathBBox = (pathInput) => {
1883
+ if (!pathInput) return {
1884
+ x: 0,
1885
+ y: 0,
1886
+ width: 0,
1887
+ height: 0,
1888
+ x2: 0,
1889
+ y2: 0,
1890
+ cx: 0,
1891
+ cy: 0,
1892
+ cz: 0
1893
+ };
1894
+ const path = parsePathString(pathInput);
1895
+ let pathCommand = "M";
1896
+ let mx = 0;
1897
+ let my = 0;
1898
+ const { max, min } = Math;
1899
+ let xMin = Infinity;
1900
+ let yMin = Infinity;
1901
+ let xMax = -Infinity;
1902
+ let yMax = -Infinity;
1903
+ let minX = 0;
1904
+ let minY = 0;
1905
+ let maxX = 0;
1906
+ let maxY = 0;
1907
+ let paramX1 = 0;
1908
+ let paramY1 = 0;
1909
+ let paramX2 = 0;
1910
+ let paramY2 = 0;
1911
+ let paramQX = 0;
1912
+ let paramQY = 0;
1913
+ iterate(path, (seg, index, lastX, lastY) => {
1914
+ [pathCommand] = seg;
1915
+ const absCommand = pathCommand.toUpperCase();
1916
+ const absoluteSegment = absCommand !== pathCommand ? absolutizeSegment(seg, index, lastX, lastY) : seg.slice(0);
1917
+ const normalSegment = absCommand === "V" ? [
1918
+ "L",
1919
+ lastX,
1920
+ absoluteSegment[1]
1921
+ ] : absCommand === "H" ? [
1922
+ "L",
1923
+ absoluteSegment[1],
1924
+ lastY
1925
+ ] : absoluteSegment;
1926
+ [pathCommand] = normalSegment;
1927
+ if (!"TQ".includes(absCommand)) {
1928
+ paramQX = 0;
1929
+ paramQY = 0;
1930
+ }
1931
+ if (pathCommand === "M") {
1932
+ [, mx, my] = normalSegment;
1933
+ minX = mx;
1934
+ minY = my;
1935
+ maxX = mx;
1936
+ maxY = my;
1937
+ } else if (pathCommand === "L") [minX, minY, maxX, maxY] = getLineBBox(lastX, lastY, normalSegment[1], normalSegment[2]);
1938
+ else if (pathCommand === "A") [minX, minY, maxX, maxY] = getArcBBox(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4], normalSegment[5], normalSegment[6], normalSegment[7]);
1939
+ else if (pathCommand === "S") {
1940
+ const cp1x = paramX1 * 2 - paramX2;
1941
+ const cp1y = paramY1 * 2 - paramY2;
1942
+ [minX, minY, maxX, maxY] = getCubicBBox(lastX, lastY, cp1x, cp1y, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4]);
1943
+ } else if (pathCommand === "C") [minX, minY, maxX, maxY] = getCubicBBox(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4], normalSegment[5], normalSegment[6]);
1944
+ else if (pathCommand === "T") {
1945
+ paramQX = paramX1 * 2 - paramQX;
1946
+ paramQY = paramY1 * 2 - paramQY;
1947
+ [minX, minY, maxX, maxY] = getQuadBBox(lastX, lastY, paramQX, paramQY, normalSegment[1], normalSegment[2]);
1948
+ } else if (pathCommand === "Q") {
1949
+ paramQX = normalSegment[1];
1950
+ paramQY = normalSegment[2];
1951
+ [minX, minY, maxX, maxY] = getQuadBBox(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4]);
1952
+ } else if (pathCommand === "Z") [minX, minY, maxX, maxY] = getLineBBox(lastX, lastY, mx, my);
1953
+ xMin = min(minX, xMin);
1954
+ yMin = min(minY, yMin);
1955
+ xMax = max(maxX, xMax);
1956
+ yMax = max(maxY, yMax);
1957
+ [paramX1, paramY1] = pathCommand === "Z" ? [mx, my] : normalSegment.slice(-2);
1958
+ [paramX2, paramY2] = pathCommand === "C" ? [normalSegment[3], normalSegment[4]] : pathCommand === "S" ? [normalSegment[1], normalSegment[2]] : [paramX1, paramY1];
1959
+ });
1960
+ const width = xMax - xMin;
1961
+ const height = yMax - yMin;
1962
+ return {
1963
+ width,
1964
+ height,
1965
+ x: xMin,
1966
+ y: yMin,
1967
+ x2: xMax,
1968
+ y2: yMax,
1969
+ cx: xMin + width / 2,
1970
+ cy: yMin + height / 2,
1971
+ cz: Math.max(width, height) + Math.min(width, height) / 2
1972
+ };
1973
+ };
1974
+ //#endregion
1975
+ //#region src/util/getTotalLength.ts
1976
+ /**
1977
+ * Returns the total length of a path, equivalent to `shape.getTotalLength()`.
1978
+ *
1979
+ * @param pathInput - The target path string or PathArray
1980
+ * @returns The total length of the path
1981
+ *
1982
+ * @example
1983
+ * ```ts
1984
+ * getTotalLength('M0 0L100 0L100 100L0 100Z')
1985
+ * // => 300
1986
+ * ```
1987
+ */
1988
+ const getTotalLength = (pathInput) => {
1989
+ const path = parsePathString(pathInput);
1990
+ let paramX1 = 0;
1991
+ let paramY1 = 0;
1992
+ let paramX2 = 0;
1993
+ let paramY2 = 0;
1994
+ let paramQX = 0;
1995
+ let paramQY = 0;
1996
+ let pathCommand = "M";
1997
+ let mx = 0;
1998
+ let my = 0;
1999
+ let totalLength = 0;
2000
+ iterate(path, (seg, index, lastX, lastY) => {
2001
+ [pathCommand] = seg;
2002
+ const absCommand = pathCommand.toUpperCase();
2003
+ const absoluteSegment = absCommand !== pathCommand ? absolutizeSegment(seg, index, lastX, lastY) : seg.slice(0);
2004
+ const normalSegment = absCommand === "V" ? [
2005
+ "L",
2006
+ lastX,
2007
+ absoluteSegment[1]
2008
+ ] : absCommand === "H" ? [
2009
+ "L",
2010
+ absoluteSegment[1],
2011
+ lastY
2012
+ ] : absoluteSegment;
2013
+ [pathCommand] = normalSegment;
2014
+ if (!"TQ".includes(absCommand)) {
2015
+ paramQX = 0;
2016
+ paramQY = 0;
2017
+ }
2018
+ if (pathCommand === "M") [, mx, my] = normalSegment;
2019
+ else if (pathCommand === "L") totalLength += getLineLength(lastX, lastY, normalSegment[1], normalSegment[2]);
2020
+ else if (pathCommand === "A") totalLength += getArcLength(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4], normalSegment[5], normalSegment[6], normalSegment[7]);
2021
+ else if (pathCommand === "S") {
2022
+ const cp1x = paramX1 * 2 - paramX2;
2023
+ const cp1y = paramY1 * 2 - paramY2;
2024
+ totalLength += getCubicLength(lastX, lastY, cp1x, cp1y, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4]);
2025
+ } else if (pathCommand === "C") totalLength += getCubicLength(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4], normalSegment[5], normalSegment[6]);
2026
+ else if (pathCommand === "T") {
2027
+ paramQX = paramX1 * 2 - paramQX;
2028
+ paramQY = paramY1 * 2 - paramQY;
2029
+ totalLength += getQuadLength(lastX, lastY, paramQX, paramQY, normalSegment[1], normalSegment[2]);
2030
+ } else if (pathCommand === "Q") {
2031
+ paramQX = normalSegment[1];
2032
+ paramQY = normalSegment[2];
2033
+ totalLength += getQuadLength(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4]);
2034
+ } else if (pathCommand === "Z") totalLength += getLineLength(lastX, lastY, mx, my);
2035
+ [paramX1, paramY1] = pathCommand === "Z" ? [mx, my] : normalSegment.slice(-2);
2036
+ [paramX2, paramY2] = pathCommand === "C" ? [normalSegment[3], normalSegment[4]] : pathCommand === "S" ? [normalSegment[1], normalSegment[2]] : [paramX1, paramY1];
2037
+ });
2038
+ return totalLength;
2039
+ };
2040
+ //#endregion
2041
+ //#region src/util/distanceEpsilon.ts
2042
+ /** Small threshold value used for floating-point distance comparisons in path calculations. */
2043
+ const DISTANCE_EPSILON = 1e-5;
2044
+ //#endregion
2045
+ //#region src/process/normalizePath.ts
2046
+ /**
2047
+ * Parses a path string or PathArray, then iterates the result for:
2048
+ * * converting segments to absolute values
2049
+ * * converting shorthand commands to their non-shorthand notation
2050
+ *
2051
+ * @param pathInput - The path string or PathArray
2052
+ * @returns The normalized PathArray
2053
+ *
2054
+ * @example
2055
+ * ```ts
2056
+ * normalizePath('M10 90s20 -80 40 -80s20 80 40 80')
2057
+ * // => [['M', 10, 90], ['C', 30, 90, 25, 10, 50, 10], ['C', 75, 10, 70, 90, 90, 90]]
2058
+ * ```
2059
+ */
2060
+ const normalizePath = (pathInput) => {
2061
+ const path = parsePathString(pathInput);
2062
+ const params = { ...paramsParser };
2063
+ return iterate(path, (seg, _, lastX, lastY) => {
2064
+ params.x = lastX;
2065
+ params.y = lastY;
2066
+ const result = normalizeSegment(seg, params);
2067
+ const seglen = result.length;
2068
+ params.x1 = +result[seglen - 2];
2069
+ params.y1 = +result[seglen - 1];
2070
+ params.x2 = +result[seglen - 4] || params.x1;
2071
+ params.y2 = +result[seglen - 3] || params.y1;
2072
+ return result;
2073
+ });
2074
+ };
2075
+ //#endregion
2076
+ //#region src/util/getPointAtLength.ts
2077
+ /**
2078
+ * Returns [x,y] coordinates of a point at a given length along a path.
2079
+ *
2080
+ * @param pathInput - The PathArray or path string to look into
2081
+ * @param distance - The distance along the path
2082
+ * @returns The requested {x, y} point coordinates
2083
+ *
2084
+ * @example
2085
+ * ```ts
2086
+ * getPointAtLength('M0 0L100 0L100 100Z', 50)
2087
+ * // => { x: 50, y: 0 }
2088
+ * ```
2089
+ */
2090
+ const getPointAtLength = (pathInput, distance) => {
2091
+ const path = normalizePath(pathInput);
2092
+ let isM = false;
2093
+ let data = [];
2094
+ let x = 0;
2095
+ let y = 0;
2096
+ let [mx, my] = path[0].slice(1);
2097
+ const distanceIsNumber = typeof distance === "number";
2098
+ let point = {
2099
+ x: mx,
2100
+ y: my
2101
+ };
2102
+ let length = 0;
2103
+ let POINT = point;
2104
+ let totalLength = 0;
2105
+ if (!distanceIsNumber || distance < 1e-5) return point;
2106
+ iterate(path, (seg, _, lastX, lastY) => {
2107
+ const pathCommand = seg[0];
2108
+ isM = pathCommand === "M";
2109
+ data = !isM ? [lastX, lastY].concat(seg.slice(1)) : data;
2110
+ if (isM) {
2111
+ [, mx, my] = seg;
2112
+ point = {
2113
+ x: mx,
2114
+ y: my
2115
+ };
2116
+ length = 0;
2117
+ } else if (pathCommand === "L") {
2118
+ point = getPointAtLineLength(data[0], data[1], data[2], data[3], distance - totalLength);
2119
+ length = getLineLength(data[0], data[1], data[2], data[3]);
2120
+ } else if (pathCommand === "A") {
2121
+ point = getPointAtArcLength(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], distance - totalLength);
2122
+ length = getArcLength(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8]);
2123
+ } else if (pathCommand === "C") {
2124
+ point = getPointAtCubicLength(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], distance - totalLength);
2125
+ length = getCubicLength(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
2126
+ } else if (pathCommand === "Q") {
2127
+ point = getPointAtQuadLength(data[0], data[1], data[2], data[3], data[4], data[5], distance - totalLength);
2128
+ length = getQuadLength(data[0], data[1], data[2], data[3], data[4], data[5]);
2129
+ } else if (pathCommand === "Z") {
2130
+ data = [
2131
+ lastX,
2132
+ lastY,
2133
+ mx,
2134
+ my
2135
+ ];
2136
+ point = {
2137
+ x: mx,
2138
+ y: my
2139
+ };
2140
+ length = getLineLength(data[0], data[1], data[2], data[3]);
2141
+ }
2142
+ [x, y] = data.slice(-2);
2143
+ if (totalLength < distance) POINT = point;
2144
+ else return false;
2145
+ totalLength += length;
2146
+ });
2147
+ if (distance > totalLength - 1e-5) return {
2148
+ x,
2149
+ y
2150
+ };
2151
+ return POINT;
2152
+ };
2153
+ //#endregion
2154
+ //#region src/util/getPropertiesAtLength.ts
2155
+ /**
2156
+ * Returns the segment, its index and length as well as
2157
+ * the length to that segment at a given length in a path.
2158
+ *
2159
+ * @param pathInput target `pathArray`
2160
+ * @param distance the given length
2161
+ * @returns the requested properties
2162
+ */
2163
+ const getPropertiesAtLength = (pathInput, distance) => {
2164
+ const pathArray = parsePathString(pathInput);
2165
+ let pathTemp = pathArray.slice(0);
2166
+ let pathLength = getTotalLength(pathTemp);
2167
+ let index = pathTemp.length - 1;
2168
+ let lengthAtSegment = 0;
2169
+ let length = 0;
2170
+ let segment = pathArray[0];
2171
+ if (index <= 0 || !distance || !Number.isFinite(distance)) return {
2172
+ segment,
2173
+ index: 0,
2174
+ length,
2175
+ lengthAtSegment
2176
+ };
2177
+ if (distance >= pathLength) {
2178
+ pathTemp = pathArray.slice(0, -1);
2179
+ lengthAtSegment = getTotalLength(pathTemp);
2180
+ length = pathLength - lengthAtSegment;
2181
+ segment = pathArray[index];
2182
+ return {
2183
+ segment,
2184
+ index,
2185
+ length,
2186
+ lengthAtSegment
2187
+ };
2188
+ }
2189
+ const segments = [];
2190
+ while (index > 0) {
2191
+ segment = pathTemp[index];
2192
+ pathTemp = pathTemp.slice(0, -1);
2193
+ lengthAtSegment = getTotalLength(pathTemp);
2194
+ length = pathLength - lengthAtSegment;
2195
+ pathLength = lengthAtSegment;
2196
+ segments.push({
2197
+ segment,
2198
+ index,
2199
+ length,
2200
+ lengthAtSegment
2201
+ });
2202
+ index -= 1;
2203
+ }
2204
+ return segments.find(({ lengthAtSegment: l }) => l <= distance);
2205
+ };
2206
+ //#endregion
2207
+ //#region src/util/getPropertiesAtPoint.ts
2208
+ /**
2209
+ * Returns the point and segment in path closest to a given point as well as
2210
+ * the distance to the path stroke.
2211
+ *
2212
+ * @see https://bl.ocks.org/mbostock/8027637
2213
+ *
2214
+ * @param pathInput target `pathArray`
2215
+ * @param point the given point
2216
+ * @returns the requested properties
2217
+ */
2218
+ const getPropertiesAtPoint = (pathInput, point) => {
2219
+ const path = parsePathString(pathInput);
2220
+ const normalPath = normalizePath(path);
2221
+ const pathLength = getTotalLength(normalPath);
2222
+ const distanceTo = (p) => {
2223
+ const dx = p.x - point.x;
2224
+ const dy = p.y - point.y;
2225
+ return dx * dx + dy * dy;
2226
+ };
2227
+ let precision = 8;
2228
+ let scan;
2229
+ let closest = {
2230
+ x: 0,
2231
+ y: 0
2232
+ };
2233
+ let scanDistance = 0;
2234
+ let bestLength = 0;
2235
+ let bestDistance = Infinity;
2236
+ for (let scanLength = 0; scanLength <= pathLength; scanLength += precision) {
2237
+ scan = getPointAtLength(normalPath, scanLength);
2238
+ scanDistance = distanceTo(scan);
2239
+ if (scanDistance < bestDistance) {
2240
+ closest = scan;
2241
+ bestLength = scanLength;
2242
+ bestDistance = scanDistance;
2243
+ }
2244
+ }
2245
+ precision /= 2;
2246
+ let before;
2247
+ let after;
2248
+ let beforeLength = 0;
2249
+ let afterLength = 0;
2250
+ let beforeDistance = 0;
2251
+ let afterDistance = 0;
2252
+ while (precision > 1e-6) {
2253
+ beforeLength = bestLength - precision;
2254
+ before = getPointAtLength(normalPath, beforeLength);
2255
+ beforeDistance = distanceTo(before);
2256
+ afterLength = bestLength + precision;
2257
+ after = getPointAtLength(normalPath, afterLength);
2258
+ afterDistance = distanceTo(after);
2259
+ if (beforeLength >= 0 && beforeDistance < bestDistance) {
2260
+ closest = before;
2261
+ bestLength = beforeLength;
2262
+ bestDistance = beforeDistance;
2263
+ } else if (afterLength <= pathLength && afterDistance < bestDistance) {
2264
+ closest = after;
2265
+ bestLength = afterLength;
2266
+ bestDistance = afterDistance;
2267
+ } else precision /= 2;
2268
+ if (precision < 1e-5) break;
2269
+ }
2270
+ const segment = getPropertiesAtLength(path, bestLength);
2271
+ return {
2272
+ closest,
2273
+ distance: Math.sqrt(bestDistance),
2274
+ segment
2275
+ };
2276
+ };
2277
+ //#endregion
2278
+ //#region src/util/getClosestPoint.ts
2279
+ /**
2280
+ * Returns the point in path closest to a given point.
2281
+ *
2282
+ * @param pathInput target `pathArray`
2283
+ * @param point the given point
2284
+ * @returns the best match
2285
+ */
2286
+ const getClosestPoint = (pathInput, point) => {
2287
+ return getPropertiesAtPoint(pathInput, point).closest;
2288
+ };
2289
+ //#endregion
2290
+ //#region src/util/getPathArea.ts
2291
+ /**
2292
+ * Returns the area of a single cubic-bezier segment.
2293
+ *
2294
+ * http://objectmix.com/graphics/133553-area-closed-bezier-curve.html
2295
+ *
2296
+ * @param x1 the starting point X
2297
+ * @param y1 the starting point Y
2298
+ * @param c1x the first control point X
2299
+ * @param c1y the first control point Y
2300
+ * @param c2x the second control point X
2301
+ * @param c2y the second control point Y
2302
+ * @param x2 the ending point X
2303
+ * @param y2 the ending point Y
2304
+ * @returns the area of the cubic-bezier segment
2305
+ */
2306
+ const getCubicSegArea = (x1, y1, c1x, c1y, c2x, c2y, x2, y2) => {
2307
+ return 3 * ((y2 - y1) * (c1x + c2x) - (x2 - x1) * (c1y + c2y) + c1y * (x1 - c2x) - c1x * (y1 - c2y) + y2 * (c2x + x1 / 3) - x2 * (c2y + y1 / 3)) / 20;
2308
+ };
2309
+ /**
2310
+ * Returns the signed area of a shape.
2311
+ *
2312
+ * @author Jürg Lehni & Jonathan Puckey
2313
+ *
2314
+ * @see https://github.com/paperjs/paper.js/blob/develop/src/path/Path.js
2315
+ *
2316
+ * @param path - The shape PathArray
2317
+ * @returns The signed area of the shape (positive for clockwise, negative for counter-clockwise)
2318
+ *
2319
+ * @example
2320
+ * ```ts
2321
+ * getPathArea([['M', 0, 0], ['L', 100, 0], ['L', 100, 100], ['L', 0, 100], ['Z']])
2322
+ * // => -10000 (counter-clockwise square)
2323
+ * ```
2324
+ */
2325
+ const getPathArea = (path) => {
2326
+ let x = 0;
2327
+ let y = 0;
2328
+ let len = 0;
2329
+ return pathToCurve(path).map((seg) => {
2330
+ switch (seg[0]) {
2331
+ case "M":
2332
+ [, x, y] = seg;
2333
+ return 0;
2334
+ default:
2335
+ len = getCubicSegArea(x, y, seg[1], seg[2], seg[3], seg[4], seg[5], seg[6]);
2336
+ [x, y] = seg.slice(-2);
2337
+ return len;
2338
+ }
2339
+ }).reduce((a, b) => a + b, 0);
2340
+ };
2341
+ //#endregion
2342
+ //#region src/util/getDrawDirection.ts
2343
+ /**
2344
+ * Check if a path is drawn clockwise and returns true if so,
2345
+ * false otherwise.
2346
+ *
2347
+ * @param path the path string or `pathArray`
2348
+ * @returns true when clockwise or false if not
2349
+ */
2350
+ const getDrawDirection = (path) => {
2351
+ return getPathArea(pathToCurve(path)) >= 0;
2352
+ };
2353
+ //#endregion
2354
+ //#region src/util/getSegmentAtLength.ts
2355
+ /**
2356
+ * Returns the segment at a given length.
2357
+ *
2358
+ * @param pathInput the target `pathArray`
2359
+ * @param distance the distance in path to look at
2360
+ * @returns the requested segment
2361
+ */
2362
+ const getSegmentAtLength = (pathInput, distance) => {
2363
+ return getPropertiesAtLength(pathInput, distance).segment;
2364
+ };
2365
+ //#endregion
2366
+ //#region src/util/getSegmentOfPoint.ts
2367
+ /**
2368
+ * Returns the path segment which contains a given point.
2369
+ *
2370
+ * @param path the `pathArray` to look into
2371
+ * @param point the point of the shape to look for
2372
+ * @returns the requested segment
2373
+ */
2374
+ const getSegmentOfPoint = (path, point) => {
2375
+ return getPropertiesAtPoint(path, point).segment;
2376
+ };
2377
+ //#endregion
2378
+ //#region src/util/isPathArray.ts
2379
+ /**
2380
+ * Iterates an array to check if it's an actual `pathArray`.
2381
+ *
2382
+ * @param path the `pathArray` to be checked
2383
+ * @returns iteration result
2384
+ */
2385
+ const isPathArray = (path) => {
2386
+ return Array.isArray(path) && path.every((seg) => {
2387
+ const lk = seg[0].toLowerCase();
2388
+ return paramsCounts[lk] === seg.length - 1 && "achlmqstvz".includes(lk) && seg.slice(1).every(Number.isFinite);
2389
+ }) && path.length > 0;
2390
+ };
2391
+ //#endregion
2392
+ //#region src/util/isAbsoluteArray.ts
2393
+ /**
2394
+ * Iterates an array to check if it's a `pathArray`
2395
+ * with all absolute values.
2396
+ *
2397
+ * @param path the `pathArray` to be checked
2398
+ * @returns iteration result
2399
+ */
2400
+ const isAbsoluteArray = (path) => {
2401
+ return isPathArray(path) && path.every(([x]) => x === x.toUpperCase());
2402
+ };
2403
+ //#endregion
2404
+ //#region src/util/isNormalizedArray.ts
2405
+ /**
2406
+ * Iterates an array to check if it's a `pathArray`
2407
+ * with all segments in non-shorthand notation
2408
+ * with absolute values.
2409
+ *
2410
+ * @param path - the array to be checked
2411
+ * @returns true if the array is a normalized path array
2412
+ */
2413
+ const isNormalizedArray = (path) => {
2414
+ return isAbsoluteArray(path) && path.every(([pc]) => "ACLMQZ".includes(pc));
2415
+ };
2416
+ //#endregion
2417
+ //#region src/util/isCurveArray.ts
2418
+ /**
2419
+ * Iterates an array to check if it's a `pathArray`
2420
+ * with all C (cubic bezier) segments.
2421
+ *
2422
+ * @param path the `Array` to be checked
2423
+ * @returns iteration result
2424
+ */
2425
+ const isCurveArray = (path) => {
2426
+ return isNormalizedArray(path) && path.every(([pc]) => "MC".includes(pc));
2427
+ };
2428
+ //#endregion
2429
+ //#region src/util/isPointInStroke.ts
2430
+ /**
2431
+ * Checks if a given point is in the stroke of a path.
2432
+ *
2433
+ * @param pathInput target path
2434
+ * @param point the given `{x,y}` point
2435
+ * @returns the query result
2436
+ */
2437
+ const isPointInStroke = (pathInput, point) => {
2438
+ const { distance } = getPropertiesAtPoint(pathInput, point);
2439
+ return Math.abs(distance) < DISTANCE_EPSILON;
2440
+ };
2441
+ //#endregion
2442
+ //#region src/util/isPolygonArray.ts
2443
+ /**
2444
+ * Checks if a path is a polygon (only M, L, H, V, Z commands).
2445
+ * @param pathArray PathArray (pre-normalize if needed)
2446
+ * @returns boolean
2447
+ */
2448
+ const isPolygonArray = (path) => {
2449
+ return isNormalizedArray(path) && path.every(([pc]) => "MLVHZ".includes(pc));
2450
+ };
2451
+ //#endregion
2452
+ //#region src/util/isPolylineArray.ts
2453
+ /**
2454
+ * Checks if a path is a polyline (only M, L, H, V commands).
2455
+ * @param pathArray PathArray (pre-normalize if needed)
2456
+ * @returns boolean
2457
+ */
2458
+ function isPolylineArray(path) {
2459
+ return isNormalizedArray(path) && path.every(([pc]) => "MLVH".includes(pc));
2460
+ }
2461
+ //#endregion
2462
+ //#region src/util/isRelativeArray.ts
2463
+ /**
2464
+ * Iterates an array to check if it's a `pathArray`
2465
+ * with relative values.
2466
+ *
2467
+ * @param path the `pathArray` to be checked
2468
+ * @returns iteration result
2469
+ */
2470
+ const isRelativeArray = (path) => {
2471
+ return isPathArray(path) && path.slice(1).every(([pc]) => pc === pc.toLowerCase());
2472
+ };
2473
+ //#endregion
2474
+ //#region src/util/isValidPath.ts
2475
+ /**
2476
+ * Parses a path string value to determine its validity
2477
+ * then returns true if it's valid or false otherwise.
2478
+ *
2479
+ * @param pathString the path string to be parsed
2480
+ * @returns the path string validity
2481
+ */
2482
+ const isValidPath = (pathString) => {
2483
+ if (typeof pathString !== "string" || !pathString.length) return false;
2484
+ const path = new PathParser(pathString);
2485
+ skipSpaces(path);
2486
+ while (path.index < path.max && !path.err.length) scanSegment(path);
2487
+ return !path.err.length && "mM".includes(path.segments[0][0]);
2488
+ };
2489
+ //#endregion
2490
+ //#region src/util/isMultiPath.ts
2491
+ /**
2492
+ * Determines if an SVG path contains multiple subpaths.
2493
+ * Accepts path string or PathArray.
2494
+ * @param path - 'M10,10 L20,20 Z M30,30 L40,40' → true
2495
+ * @returns boolean
2496
+ */
2497
+ const isMultiPath = (path) => {
2498
+ if (typeof path === "string") {
2499
+ const matches = path.match(/[Mm]/g);
2500
+ return matches ? matches.length > 1 : false;
2501
+ }
2502
+ if (isPathArray(path)) {
2503
+ let moveCount = 0;
2504
+ for (const segment of path) if (segment[0].toUpperCase() === "M") {
2505
+ moveCount++;
2506
+ if (moveCount > 1) return true;
2507
+ }
2508
+ return false;
2509
+ }
2510
+ throw new TypeError(error + ": expected string or PathArray");
2511
+ };
2512
+ //#endregion
2513
+ //#region src/util/isClosedPath.ts
2514
+ /**
2515
+ * Check if a PathArray is closed, which means its last segment is a Z.
2516
+ * @param path
2517
+ * @returns true if the path is closed
2518
+ */
2519
+ const isClosedPath = (path) => {
2520
+ return path[path.length - 1][0].toUpperCase() === "Z";
2521
+ };
2522
+ //#endregion
2523
+ //#region src/util/shapeParams.ts
2524
+ /**
2525
+ * Supported shapes and their specific parameters.
2526
+ */
2527
+ const shapeParams = {
2528
+ line: [
2529
+ "x1",
2530
+ "y1",
2531
+ "x2",
2532
+ "y2"
2533
+ ],
2534
+ circle: [
2535
+ "cx",
2536
+ "cy",
2537
+ "r"
2538
+ ],
2539
+ ellipse: [
2540
+ "cx",
2541
+ "cy",
2542
+ "rx",
2543
+ "ry"
2544
+ ],
2545
+ rect: [
2546
+ "width",
2547
+ "height",
2548
+ "x",
2549
+ "y",
2550
+ "rx",
2551
+ "ry"
2552
+ ],
2553
+ polygon: ["points"],
2554
+ polyline: ["points"],
2555
+ glyph: ["d"]
2556
+ };
2557
+ //#endregion
2558
+ //#region src/util/isElement.ts
2559
+ /**
2560
+ * Checks if a value is a DOM Element.
2561
+ *
2562
+ * @param node - The value to check
2563
+ * @returns True if the value is a DOM Element (nodeType === 1)
2564
+ */
2565
+ const isElement = (node) => node !== void 0 && node !== null && typeof node === "object" && node.nodeType === 1;
2566
+ //#endregion
2567
+ //#region src/util/shapeToPathArray.ts
2568
+ /**
2569
+ * Returns a new PathArray from line attributes.
2570
+ *
2571
+ * @param attr - Shape configuration with x1, y1, x2, y2
2572
+ * @returns A new line PathArray
2573
+ *
2574
+ * @example
2575
+ * ```ts
2576
+ * getLinePath({ x1: 0, y1: 0, x2: 100, y2: 100 })
2577
+ * // => [['M', 0, 0], ['L', 100, 100]]
2578
+ * ```
2579
+ */
2580
+ const getLinePath = (attr) => {
2581
+ let { x1, y1, x2, y2 } = attr;
2582
+ [x1, y1, x2, y2] = [
2583
+ x1,
2584
+ y1,
2585
+ x2,
2586
+ y2
2587
+ ].map((a) => +a);
2588
+ return [[
2589
+ "M",
2590
+ x1,
2591
+ y1
2592
+ ], [
2593
+ "L",
2594
+ x2,
2595
+ y2
2596
+ ]];
2597
+ };
2598
+ /**
2599
+ * Returns a new PathArray from polyline/polygon attributes.
2600
+ *
2601
+ * @param attr - Shape configuration with points string
2602
+ * @returns A new polygon/polyline PathArray
2603
+ *
2604
+ * @example
2605
+ * ```ts
2606
+ * getPolyPath({ type: 'polygon', points: '0,0 100,0 100,100 0,100' })
2607
+ * // => [['M', 0, 0], ['L', 100, 0], ['L', 100, 100], ['L', 0, 100], ['z']]
2608
+ * ```
2609
+ */
2610
+ const getPolyPath = (attr) => {
2611
+ const pathArray = [];
2612
+ const points = (attr.points || "").trim().split(/[\s|,]/).map((a) => +a);
2613
+ let index = 0;
2614
+ while (index < points.length) {
2615
+ pathArray.push([
2616
+ index ? "L" : "M",
2617
+ points[index],
2618
+ points[index + 1]
2619
+ ]);
2620
+ index += 2;
2621
+ }
2622
+ return attr.type === "polygon" ? [...pathArray, ["z"]] : pathArray;
2623
+ };
2624
+ /**
2625
+ * Returns a new PathArray from circle attributes.
2626
+ *
2627
+ * @param attr - Shape configuration with cx, cy, r
2628
+ * @returns A circle PathArray
2629
+ *
2630
+ * @example
2631
+ * ```ts
2632
+ * getCirclePath({ cx: 50, cy: 50, r: 25 })
2633
+ * // => [['M', 25, 50], ['a', 25, 25, 0, 1, 0, 50, 0], ['a', 25, 25, 0, 1, 0, -50, 0]]
2634
+ * ```
2635
+ */
2636
+ const getCirclePath = (attr) => {
2637
+ let { cx, cy, r } = attr;
2638
+ [cx, cy, r] = [
2639
+ cx,
2640
+ cy,
2641
+ r
2642
+ ].map((a) => +a);
2643
+ return [
2644
+ [
2645
+ "M",
2646
+ cx - r,
2647
+ cy
2648
+ ],
2649
+ [
2650
+ "a",
2651
+ r,
2652
+ r,
2653
+ 0,
2654
+ 1,
2655
+ 0,
2656
+ 2 * r,
2657
+ 0
2658
+ ],
2659
+ [
2660
+ "a",
2661
+ r,
2662
+ r,
2663
+ 0,
2664
+ 1,
2665
+ 0,
2666
+ -2 * r,
2667
+ 0
2668
+ ]
2669
+ ];
2670
+ };
2671
+ /**
2672
+ * Returns a new PathArray from ellipse attributes.
2673
+ *
2674
+ * @param attr - Shape configuration with cx, cy, rx, ry
2675
+ * @returns An ellipse PathArray
2676
+ *
2677
+ * @example
2678
+ * ```ts
2679
+ * getEllipsePath({ cx: 50, cy: 50, rx: 30, ry: 20 })
2680
+ * // => [['M', 20, 50], ['a', 30, 20, 0, 1, 0, 60, 0], ['a', 30, 20, 0, 1, 0, -60, 0]]
2681
+ * ```
2682
+ */
2683
+ const getEllipsePath = (attr) => {
2684
+ let { cx, cy } = attr;
2685
+ let rx = attr.rx || 0;
2686
+ let ry = attr.ry || rx;
2687
+ [cx, cy, rx, ry] = [
2688
+ cx,
2689
+ cy,
2690
+ rx,
2691
+ ry
2692
+ ].map((a) => +a);
2693
+ return [
2694
+ [
2695
+ "M",
2696
+ cx - rx,
2697
+ cy
2698
+ ],
2699
+ [
2700
+ "a",
2701
+ rx,
2702
+ ry,
2703
+ 0,
2704
+ 1,
2705
+ 0,
2706
+ 2 * rx,
2707
+ 0
2708
+ ],
2709
+ [
2710
+ "a",
2711
+ rx,
2712
+ ry,
2713
+ 0,
2714
+ 1,
2715
+ 0,
2716
+ -2 * rx,
2717
+ 0
2718
+ ]
2719
+ ];
2720
+ };
2721
+ /**
2722
+ * Returns a new PathArray from rect attributes.
2723
+ *
2724
+ * @param attr - Object with x, y, width, height, and optional rx/ry
2725
+ * @returns A new PathArray from `<rect>` attributes
2726
+ *
2727
+ * @example
2728
+ * ```ts
2729
+ * getRectanglePath({ x: 0, y: 0, width: 100, height: 50, ry: 10 })
2730
+ * // => [['M', 10, 0], ['h', 80], ['a', 10, 10, 0, 0, 1, 10, 10], ...]
2731
+ * ```
2732
+ */
2733
+ const getRectanglePath = (attr) => {
2734
+ const x = +attr.x || 0;
2735
+ const y = +attr.y || 0;
2736
+ const w = +attr.width;
2737
+ const h = +attr.height;
2738
+ let rx = +(attr.rx || 0);
2739
+ let ry = +(attr.ry || rx);
2740
+ if (rx || ry) {
2741
+ if (rx * 2 > w) rx -= (rx * 2 - w) / 2;
2742
+ if (ry * 2 > h) ry -= (ry * 2 - h) / 2;
2743
+ return [
2744
+ [
2745
+ "M",
2746
+ x + rx,
2747
+ y
2748
+ ],
2749
+ ["h", w - rx * 2],
2750
+ [
2751
+ "s",
2752
+ rx,
2753
+ 0,
2754
+ rx,
2755
+ ry
2756
+ ],
2757
+ ["v", h - ry * 2],
2758
+ [
2759
+ "s",
2760
+ 0,
2761
+ ry,
2762
+ -rx,
2763
+ ry
2764
+ ],
2765
+ ["h", -w + rx * 2],
2766
+ [
2767
+ "s",
2768
+ -rx,
2769
+ 0,
2770
+ -rx,
2771
+ -ry
2772
+ ],
2773
+ ["v", -h + ry * 2],
2774
+ [
2775
+ "s",
2776
+ 0,
2777
+ -ry,
2778
+ rx,
2779
+ -ry
2780
+ ]
2781
+ ];
2782
+ }
2783
+ return [
2784
+ [
2785
+ "M",
2786
+ x,
2787
+ y
2788
+ ],
2789
+ ["h", w],
2790
+ ["v", h],
2791
+ ["H", x],
2792
+ ["Z"]
2793
+ ];
2794
+ };
2795
+ /**
2796
+ * Returns a new `pathArray` created from attributes of a `<line>`, `<polyline>`,
2797
+ * `<polygon>`, `<rect>`, `<ellipse>`, `<circle>`, <path> or `<glyph>`.
2798
+ *
2799
+ * It can also work with an options object, see the type below
2800
+ * @see ShapeOps
2801
+ *
2802
+ * @param element target shape
2803
+ * @returns the newly created `<path>` element
2804
+ */
2805
+ const shapeToPathArray = (element) => {
2806
+ const supportedShapes = Object.keys(shapeParams);
2807
+ const targetIsElement = isElement(element);
2808
+ const tagName = targetIsElement ? element.tagName : null;
2809
+ if (tagName && [...supportedShapes, "path"].every((s) => tagName !== s)) throw TypeError(`${error}: "${tagName}" is not SVGElement`);
2810
+ const type = targetIsElement ? tagName : element.type;
2811
+ const shapeAttrs = shapeParams[type];
2812
+ const config = { type };
2813
+ if (targetIsElement) shapeAttrs.forEach((p) => {
2814
+ config[p] = element.getAttribute(p);
2815
+ });
2816
+ else Object.assign(config, element);
2817
+ let pathArray = [];
2818
+ if (type === "circle") pathArray = getCirclePath(config);
2819
+ else if (type === "ellipse") pathArray = getEllipsePath(config);
2820
+ else if (["polyline", "polygon"].includes(type)) pathArray = getPolyPath(config);
2821
+ else if (type === "rect") pathArray = getRectanglePath(config);
2822
+ else if (type === "line") pathArray = getLinePath(config);
2823
+ else if (["glyph", "path"].includes(type)) pathArray = parsePathString(targetIsElement ? element.getAttribute("d") || "" : element.d || "");
2824
+ if (isPathArray(pathArray) && pathArray.length) return pathArray;
2825
+ return false;
2826
+ };
2827
+ //#endregion
2828
+ //#region src/util/shapeToPath.ts
2829
+ /**
2830
+ * Returns a new `<path>` element created from attributes of a `<line>`, `<polyline>`,
2831
+ * `<polygon>`, `<rect>`, `<ellipse>`, `<circle>` or `<glyph>`. If `replace` parameter
2832
+ * is `true`, it will replace the target. The default `ownerDocument` is your current
2833
+ * `document` browser page, if you want to use in server-side using `jsdom`, you can
2834
+ * pass the `jsdom` `document` to `ownDocument`.
2835
+ *
2836
+ * It can also work with an options object, see the type below
2837
+ * @see ShapeOps
2838
+ *
2839
+ * The newly created `<path>` element keeps all non-specific
2840
+ * attributes like `class`, `fill`, etc.
2841
+ *
2842
+ * @param element - Target shape element or shape options object
2843
+ * @param replace - Option to replace target element
2844
+ * @param ownerDocument - Document for creating the element
2845
+ * @returns The newly created `<path>` element, or false if the path is invalid
2846
+ *
2847
+ * @example
2848
+ * ```ts
2849
+ * const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
2850
+ * circle.setAttribute('cx', '50')
2851
+ * circle.setAttribute('cy', '50')
2852
+ * circle.setAttribute('r', '25')
2853
+ * const path = shapeToPath(circle)
2854
+ * path.getAttribute('d')
2855
+ * // => 'M50 25A25 25 0 1 1 50 75A25 25 0 1 1 50 25Z'
2856
+ * ```
2857
+ */
2858
+ const shapeToPath = (element, replace, ownerDocument) => {
2859
+ const doc = ownerDocument || document;
2860
+ const supportedShapes = Object.keys(shapeParams);
2861
+ const targetIsElement = isElement(element);
2862
+ const tagName = targetIsElement ? element.tagName : null;
2863
+ if (tagName === "path") throw TypeError(`${error}: "${tagName}" is already SVGPathElement`);
2864
+ if (tagName && supportedShapes.every((s) => tagName !== s)) throw TypeError(`${error}: "${tagName}" is not SVGElement`);
2865
+ const path = doc.createElementNS("http://www.w3.org/2000/svg", "path");
2866
+ const type = targetIsElement ? tagName : element.type;
2867
+ const shapeAttrs = shapeParams[type];
2868
+ const config = { type };
2869
+ const round = defaultOptions.round;
2870
+ const pathArray = shapeToPathArray(element);
2871
+ const description = pathArray && pathArray.length ? pathToString(pathArray, round) : "";
2872
+ if (targetIsElement) {
2873
+ shapeAttrs.forEach((p) => {
2874
+ config[p] = element.getAttribute(p);
2875
+ });
2876
+ for (let i = 0; i < element.attributes.length; i++) {
2877
+ const attr = element.attributes[i];
2878
+ if (attr && !shapeAttrs.includes(attr.name)) path.setAttribute(attr.name, attr.value);
2879
+ }
2880
+ } else {
2881
+ Object.assign(config, element);
2882
+ Object.keys(config).forEach((k) => {
2883
+ if (!shapeAttrs.includes(k) && k !== "type") path.setAttribute(k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`), config[k]);
2884
+ });
2885
+ }
2886
+ if (isValidPath(description)) {
2887
+ path.setAttribute("d", description);
2888
+ if (replace && targetIsElement) {
2889
+ element.before(path, element);
2890
+ element.remove();
2891
+ }
2892
+ return path;
2893
+ }
2894
+ return false;
2895
+ };
2896
+ //#endregion
2897
+ //#region src/process/shortenSegment.ts
2898
+ /**
2899
+ * Shorten a single segment of a `pathArray` object.
2900
+ *
2901
+ * @param segment the `absoluteSegment` object
2902
+ * @param normalSegment the `normalSegment` object
2903
+ * @param params the coordinates of the previous segment
2904
+ * @param prevCommand the path command of the previous segment
2905
+ * @returns the shortened segment
2906
+ */
2907
+ const shortenSegment = (segment, normalSegment, params, prevCommand) => {
2908
+ const [pathCommand] = segment;
2909
+ const { round: defaultRound } = defaultOptions;
2910
+ const round = typeof defaultRound === "number" ? defaultRound : 4;
2911
+ const normalValues = normalSegment.slice(1);
2912
+ const { x1, y1, x2, y2, x, y } = params;
2913
+ const [nx, ny] = normalValues.slice(-2);
2914
+ const result = segment;
2915
+ if (!"TQ".includes(pathCommand)) {
2916
+ params.qx = null;
2917
+ params.qy = null;
2918
+ }
2919
+ if (pathCommand === "L") {
2920
+ if (roundTo(x, round) === roundTo(nx, round)) return ["V", ny];
2921
+ else if (roundTo(y, round) === roundTo(ny, round)) return ["H", nx];
2922
+ } else if (pathCommand === "C") {
2923
+ const [nx1, ny1] = normalValues;
2924
+ params.x1 = nx1;
2925
+ params.y1 = ny1;
2926
+ if ("CS".includes(prevCommand) && (roundTo(nx1, round) === roundTo(x1 * 2 - x2, round) && roundTo(ny1, round) === roundTo(y1 * 2 - y2, round) || roundTo(x1, round) === roundTo(x2 * 2 - x, round) && roundTo(y1, round) === roundTo(y2 * 2 - y, round))) return [
2927
+ "S",
2928
+ normalValues[2],
2929
+ normalValues[3],
2930
+ normalValues[4],
2931
+ normalValues[5]
2932
+ ];
2933
+ } else if (pathCommand === "Q") {
2934
+ const [qx, qy] = normalValues;
2935
+ params.qx = qx;
2936
+ params.qy = qy;
2937
+ if ("QT".includes(prevCommand) && roundTo(qx, round) === roundTo(x1 * 2 - x2, round) && roundTo(qy, round) === roundTo(y1 * 2 - y2, round)) return [
2938
+ "T",
2939
+ normalValues[2],
2940
+ normalValues[3]
2941
+ ];
2942
+ }
2943
+ return result;
2944
+ };
2945
+ //#endregion
2946
+ //#region src/process/roundSegment.ts
2947
+ /**
2948
+ * Rounds the numeric values of a path segment to the specified precision.
2949
+ *
2950
+ * @param segment - The path segment to round
2951
+ * @param roundOption - Number of decimal places
2952
+ * @returns The rounded segment
2953
+ */
2954
+ const roundSegment = (segment, roundOption) => {
2955
+ const values = segment.slice(1).map((n) => roundTo(n, roundOption));
2956
+ return [segment[0]].concat(values);
2957
+ };
2958
+ //#endregion
2959
+ //#region src/process/optimizePath.ts
2960
+ /**
2961
+ * Optimizes a PathArray:
2962
+ * * converts segments to shorthand if possible
2963
+ * * selects shortest representation from absolute and relative forms
2964
+ *
2965
+ * @param pathInput - A path string or PathArray
2966
+ * @param roundOption - Number of decimal places for rounding
2967
+ * @returns The optimized PathArray
2968
+ *
2969
+ * @example
2970
+ * ```ts
2971
+ * optimizePath('M10 10L10 10L90 90', 2)
2972
+ * // => [['M', 10, 10], ['l', 0, 0], ['l', 80, 80]]
2973
+ * ```
2974
+ */
2975
+ const optimizePath = (pathInput, roundOption) => {
2976
+ const path = pathToAbsolute(pathInput);
2977
+ const round = typeof roundOption === "number" && roundOption >= 0 ? roundOption : 2;
2978
+ const optimParams = { ...paramsParser };
2979
+ const allPathCommands = [];
2980
+ let pathCommand = "M";
2981
+ let prevCommand = "Z";
2982
+ return iterate(path, (seg, i, lastX, lastY) => {
2983
+ optimParams.x = lastX;
2984
+ optimParams.y = lastY;
2985
+ const normalizedSegment = normalizeSegment(seg, optimParams);
2986
+ let result = seg;
2987
+ pathCommand = seg[0];
2988
+ allPathCommands[i] = pathCommand;
2989
+ if (i) {
2990
+ prevCommand = allPathCommands[i - 1];
2991
+ const shortSegment = shortenSegment(seg, normalizedSegment, optimParams, prevCommand);
2992
+ const absSegment = roundSegment(shortSegment, round);
2993
+ const absString = absSegment.join("");
2994
+ const relSegment = roundSegment(relativizeSegment(shortSegment, i, lastX, lastY), round);
2995
+ const relString = relSegment.join("");
2996
+ result = absString.length < relString.length ? absSegment : relSegment;
2997
+ }
2998
+ const seglen = normalizedSegment.length;
2999
+ optimParams.x1 = +normalizedSegment[seglen - 2];
3000
+ optimParams.y1 = +normalizedSegment[seglen - 1];
3001
+ optimParams.x2 = +normalizedSegment[seglen - 4] || optimParams.x1;
3002
+ optimParams.y2 = +normalizedSegment[seglen - 3] || optimParams.y1;
3003
+ return result;
3004
+ });
3005
+ };
3006
+ //#endregion
3007
+ //#region src/process/reversePath.ts
3008
+ /**
3009
+ * Reverses all segments of a PathArray and returns a new PathArray
3010
+ * with absolute values.
3011
+ *
3012
+ * @param pathInput - The source PathArray
3013
+ * @returns The reversed PathArray
3014
+ *
3015
+ * @example
3016
+ * ```ts
3017
+ * reversePath([['M', 0, 0], ['L', 100, 0], ['L', 100, 100], ['L', 0, 100], ['Z']])
3018
+ * // => [['M', 0, 100], ['L', 0, 0], ['L', 100, 0], ['L', 100, 100], ['Z']]
3019
+ * ```
3020
+ */
3021
+ const reversePath = (pathInput) => {
3022
+ const absolutePath = pathToAbsolute(pathInput);
3023
+ const normalizedPath = normalizePath(absolutePath);
3024
+ const pLen = absolutePath.length;
3025
+ const isClosed = absolutePath[pLen - 1][0] === "Z";
3026
+ const reversedPath = iterate(absolutePath, (segment, i) => {
3027
+ const normalizedSegment = normalizedPath[i];
3028
+ const prevSeg = i && absolutePath[i - 1];
3029
+ const prevCommand = prevSeg && prevSeg[0];
3030
+ const nextSeg = absolutePath[i + 1];
3031
+ const nextCommand = nextSeg && nextSeg[0];
3032
+ const pathCommand = segment[0];
3033
+ const [x, y] = normalizedPath[i ? i - 1 : pLen - 1].slice(-2);
3034
+ let result = segment;
3035
+ switch (pathCommand) {
3036
+ case "M":
3037
+ result = isClosed ? ["Z"] : [
3038
+ pathCommand,
3039
+ x,
3040
+ y
3041
+ ];
3042
+ break;
3043
+ case "A":
3044
+ result = [
3045
+ pathCommand,
3046
+ segment[1],
3047
+ segment[2],
3048
+ segment[3],
3049
+ segment[4],
3050
+ segment[5] === 1 ? 0 : 1,
3051
+ x,
3052
+ y
3053
+ ];
3054
+ break;
3055
+ case "C":
3056
+ if (nextSeg && nextCommand === "S") result = [
3057
+ "S",
3058
+ segment[1],
3059
+ segment[2],
3060
+ x,
3061
+ y
3062
+ ];
3063
+ else result = [
3064
+ pathCommand,
3065
+ segment[3],
3066
+ segment[4],
3067
+ segment[1],
3068
+ segment[2],
3069
+ x,
3070
+ y
3071
+ ];
3072
+ break;
3073
+ case "S":
3074
+ if (prevCommand && "CS".includes(prevCommand) && (!nextSeg || nextCommand !== "S")) result = [
3075
+ "C",
3076
+ normalizedSegment[3],
3077
+ normalizedSegment[4],
3078
+ normalizedSegment[1],
3079
+ normalizedSegment[2],
3080
+ x,
3081
+ y
3082
+ ];
3083
+ else result = [
3084
+ pathCommand,
3085
+ normalizedSegment[1],
3086
+ normalizedSegment[2],
3087
+ x,
3088
+ y
3089
+ ];
3090
+ break;
3091
+ case "Q":
3092
+ if (nextSeg && nextCommand === "T") result = [
3093
+ "T",
3094
+ x,
3095
+ y
3096
+ ];
3097
+ else result = [
3098
+ pathCommand,
3099
+ segment[1],
3100
+ segment[2],
3101
+ x,
3102
+ y
3103
+ ];
3104
+ break;
3105
+ case "T":
3106
+ if (prevCommand && "QT".includes(prevCommand) && (!nextSeg || nextCommand !== "T")) result = [
3107
+ "Q",
3108
+ normalizedSegment[1],
3109
+ normalizedSegment[2],
3110
+ x,
3111
+ y
3112
+ ];
3113
+ else result = [
3114
+ pathCommand,
3115
+ x,
3116
+ y
3117
+ ];
3118
+ break;
3119
+ case "Z":
3120
+ result = [
3121
+ "M",
3122
+ x,
3123
+ y
3124
+ ];
3125
+ break;
3126
+ case "H":
3127
+ result = [pathCommand, x];
3128
+ break;
3129
+ case "V":
3130
+ result = [pathCommand, y];
3131
+ break;
3132
+ default: result = [pathCommand].concat(segment.slice(1, -2), x, y);
3133
+ }
3134
+ return result;
3135
+ });
3136
+ return isClosed ? reversedPath.reverse() : [reversedPath[0]].concat(reversedPath.slice(1).reverse());
3137
+ };
3138
+ //#endregion
3139
+ //#region src/process/splitPath.ts
3140
+ /**
3141
+ * Split a path string or PathArray into an array of sub-paths.
3142
+ *
3143
+ * In the process, values are converted to absolute
3144
+ * for visual consistency.
3145
+ *
3146
+ * @param pathInput - The source path string or PathArray
3147
+ * @returns An array of sub-path PathArrays
3148
+ *
3149
+ * @example
3150
+ * ```ts
3151
+ * splitPath('M0 0L100 0ZM200 0L300 0Z')
3152
+ * // => [
3153
+ * // [['M', 0, 0], ['L', 100, 0], ['Z']],
3154
+ * // [['M', 200, 0], ['L', 300, 0], ['Z']]
3155
+ * // ]
3156
+ * ```
3157
+ */
3158
+ const splitPath = (pathInput) => {
3159
+ const composite = [];
3160
+ const parsedPath = parsePathString(pathInput);
3161
+ let path = [];
3162
+ let pi = -1;
3163
+ let x = 0;
3164
+ let y = 0;
3165
+ let mx = 0;
3166
+ let my = 0;
3167
+ iterate(parsedPath, (seg, _, prevX, prevY) => {
3168
+ const cmd = seg[0];
3169
+ const absCommand = cmd.toUpperCase();
3170
+ const isRelative = cmd === cmd.toLowerCase();
3171
+ const values = seg.slice(1);
3172
+ if (absCommand === "M") {
3173
+ pi += 1;
3174
+ [x, y] = values;
3175
+ x += isRelative ? prevX : 0;
3176
+ y += isRelative ? prevY : 0;
3177
+ mx = x;
3178
+ my = y;
3179
+ path = [isRelative ? [
3180
+ absCommand,
3181
+ mx,
3182
+ my
3183
+ ] : seg];
3184
+ } else {
3185
+ if (absCommand === "Z") {
3186
+ x = mx;
3187
+ y = my;
3188
+ } else if (absCommand === "H") {
3189
+ [, x] = seg;
3190
+ x += isRelative ? prevX : 0;
3191
+ } else if (absCommand === "V") {
3192
+ [, y] = seg;
3193
+ y += isRelative ? prevY : 0;
3194
+ } else {
3195
+ [x, y] = seg.slice(-2);
3196
+ x += isRelative ? prevX : 0;
3197
+ y += isRelative ? prevY : 0;
3198
+ }
3199
+ path.push(seg);
3200
+ }
3201
+ composite[pi] = path;
3202
+ });
3203
+ return composite;
3204
+ };
3205
+ //#endregion
3206
+ //#region src/process/getSVGMatrix.ts
3207
+ /**
3208
+ * Returns a transformation matrix to apply to `<path>` elements.
3209
+ *
3210
+ * @see TransformObjectValues
3211
+ *
3212
+ * @param transform the `transformObject`
3213
+ * @returns a new transformation matrix
3214
+ */
3215
+ const getSVGMatrix = (transform) => {
3216
+ let matrix = new CSSMatrix();
3217
+ const { origin } = transform;
3218
+ const [originX, originY] = origin;
3219
+ const { translate } = transform;
3220
+ const { rotate } = transform;
3221
+ const { skew } = transform;
3222
+ const { scale } = transform;
3223
+ if (Array.isArray(translate) && translate.length >= 2 && translate.every((x) => !Number.isNaN(+x)) && translate.some((x) => x !== 0)) matrix = matrix.translate(...translate);
3224
+ else if (typeof translate === "number" && !Number.isNaN(translate)) matrix = matrix.translate(translate);
3225
+ if (rotate || skew || scale) {
3226
+ matrix = matrix.translate(originX, originY);
3227
+ if (Array.isArray(rotate) && rotate.length >= 2 && rotate.every((x) => !Number.isNaN(+x)) && rotate.some((x) => x !== 0)) matrix = matrix.rotate(...rotate);
3228
+ else if (typeof rotate === "number" && !Number.isNaN(rotate)) matrix = matrix.rotate(rotate);
3229
+ if (Array.isArray(skew) && skew.length === 2 && skew.every((x) => !Number.isNaN(+x)) && skew.some((x) => x !== 0)) {
3230
+ matrix = skew[0] ? matrix.skewX(skew[0]) : matrix;
3231
+ matrix = skew[1] ? matrix.skewY(skew[1]) : matrix;
3232
+ } else if (typeof skew === "number" && !Number.isNaN(skew)) matrix = matrix.skewX(skew);
3233
+ if (Array.isArray(scale) && scale.length >= 2 && scale.every((x) => !Number.isNaN(+x)) && scale.some((x) => x !== 1)) matrix = matrix.scale(...scale);
3234
+ else if (typeof scale === "number" && !Number.isNaN(scale)) matrix = matrix.scale(scale);
3235
+ matrix = matrix.translate(-originX, -originY);
3236
+ }
3237
+ return matrix;
3238
+ };
3239
+ //#endregion
3240
+ //#region src/process/projection2d.ts
3241
+ /**
3242
+ * Transforms a specified point using a matrix, returning a new
3243
+ * Tuple *Object* comprising of the transformed point.
3244
+ * Neither the matrix nor the original point are altered.
3245
+ *
3246
+ * @copyright thednp © 2021
3247
+ *
3248
+ * @param cssm CSSMatrix instance
3249
+ * @param v Tuple
3250
+ * @returns the resulting Tuple
3251
+ */
3252
+ const translatePoint = (cssm, v) => {
3253
+ let m = CSSMatrix.Translate(v[0], v[1], v[2]);
3254
+ [, , , m.m44] = v;
3255
+ m = cssm.multiply(m);
3256
+ return [
3257
+ m.m41,
3258
+ m.m42,
3259
+ m.m43,
3260
+ m.m44
3261
+ ];
3262
+ };
3263
+ /**
3264
+ * Returns the [x,y] projected coordinates for a given an [x,y] point
3265
+ * and an [x,y,z] perspective origin point.
3266
+ *
3267
+ * Equation found here =>
3268
+ * http://en.wikipedia.org/wiki/3D_projection#Diagram
3269
+ * Details =>
3270
+ * https://stackoverflow.com/questions/23792505/predicted-rendering-of-css-3d-transformed-pixel
3271
+ *
3272
+ * @param m the transformation matrix
3273
+ * @param point2D the initial [x,y] coordinates
3274
+ * @param origin the [x,y,z] transform origin
3275
+ * @returns the projected [x,y] coordinates
3276
+ */
3277
+ const projection2d = (m, point2D, origin) => {
3278
+ const [originX, originY, originZ] = origin;
3279
+ const [x, y, z] = translatePoint(m, [
3280
+ point2D[0],
3281
+ point2D[1],
3282
+ 0,
3283
+ 1
3284
+ ]);
3285
+ const relativePositionX = x - originX;
3286
+ const relativePositionY = y - originY;
3287
+ const relativePositionZ = z - originZ;
3288
+ return [relativePositionX * (Math.abs(originZ) / Math.abs(relativePositionZ) || 1) + originX, relativePositionY * (Math.abs(originZ) / Math.abs(relativePositionZ) || 1) + originY];
3289
+ };
3290
+ //#endregion
3291
+ //#region src/process/transformPath.ts
3292
+ /**
3293
+ * Apply a 2D / 3D transformation to a PathArray.
3294
+ *
3295
+ * Since SVGElement doesn't support 3D transformation, this function
3296
+ * creates a 2D projection of the path element.
3297
+ *
3298
+ * @param pathInput - The PathArray or path string to transform
3299
+ * @param transform - The transform functions object (translate, rotate, skew, scale, origin)
3300
+ * @returns The transformed PathArray
3301
+ *
3302
+ * @example
3303
+ * ```ts
3304
+ * transformPath('M0 0L100 0L100 100L0 100Z', { translate: [10, 20], scale: 2 })
3305
+ * // => [['M', 10, 20], ['L', 210, 20], ['L', 210, 220], ['L', 10, 220], ['Z']]
3306
+ * ```
3307
+ */
3308
+ const transformPath = (pathInput, transform) => {
3309
+ let x = 0;
3310
+ let y = 0;
3311
+ let lx = 0;
3312
+ let ly = 0;
3313
+ let j = 0;
3314
+ let jj = 0;
3315
+ const path = parsePathString(pathInput);
3316
+ const transformProps = transform && Object.keys(transform);
3317
+ if (!transform || transformProps && !transformProps.length) return path.slice(0);
3318
+ if (!transform.origin) Object.assign(transform, { origin: defaultOptions.origin });
3319
+ const origin = transform.origin;
3320
+ const matrixInstance = getSVGMatrix(transform);
3321
+ if (matrixInstance.isIdentity) return path.slice(0);
3322
+ return iterate(path, (seg, index, lastX, lastY) => {
3323
+ let [pathCommand] = seg;
3324
+ const absCommand = pathCommand.toUpperCase();
3325
+ const absoluteSegment = absCommand !== pathCommand ? absolutizeSegment(seg, index, lastX, lastY) : seg.slice(0);
3326
+ let result = absCommand === "A" ? ["C"].concat(arcToCubic(lastX, lastY, absoluteSegment[1], absoluteSegment[2], absoluteSegment[3], absoluteSegment[4], absoluteSegment[5], absoluteSegment[6], absoluteSegment[7])) : absCommand === "V" ? [
3327
+ "L",
3328
+ lastX,
3329
+ absoluteSegment[1]
3330
+ ] : absCommand === "H" ? [
3331
+ "L",
3332
+ absoluteSegment[1],
3333
+ lastY
3334
+ ] : absoluteSegment;
3335
+ pathCommand = result[0];
3336
+ const isLongArc = pathCommand === "C" && result.length > 7;
3337
+ const tempSegment = isLongArc ? result.slice(0, 7) : result.slice(0);
3338
+ if (isLongArc) {
3339
+ path.splice(index + 1, 0, ["C"].concat(result.slice(7)));
3340
+ result = tempSegment;
3341
+ }
3342
+ if (pathCommand === "L") {
3343
+ [lx, ly] = projection2d(matrixInstance, [result[1], result[2]], origin);
3344
+ if (x !== lx && y !== ly) result = [
3345
+ "L",
3346
+ lx,
3347
+ ly
3348
+ ];
3349
+ else if (y === ly) result = ["H", lx];
3350
+ else if (x === lx) result = ["V", ly];
3351
+ } else for (j = 1, jj = result.length; j < jj; j += 2) {
3352
+ [lx, ly] = projection2d(matrixInstance, [+result[j], +result[j + 1]], origin);
3353
+ result[j] = lx;
3354
+ result[j + 1] = ly;
3355
+ }
3356
+ x = lx;
3357
+ y = ly;
3358
+ return result;
3359
+ });
3360
+ };
3361
+ //#endregion
3362
+ //#region src/process/reverseCurve.ts
3363
+ /**
3364
+ * Reverses all segments of a `pathArray`
3365
+ * which consists of only C (cubic-bezier) path commands.
3366
+ *
3367
+ * @param path the source `pathArray`
3368
+ * @returns the reversed `pathArray`
3369
+ */
3370
+ const reverseCurve = (path) => {
3371
+ const rotatedCurve = path.slice(1).map((x, i, curveOnly) => !i ? path[0].slice(1).concat(x.slice(1)) : curveOnly[i - 1].slice(-2).concat(x.slice(1))).map((x) => x.map((_, i) => x[x.length - i - 2 * (1 - i % 2)])).reverse();
3372
+ return [["M"].concat(rotatedCurve[0].slice(0, 2))].concat(rotatedCurve.map((x) => ["C"].concat(x.slice(2))));
3373
+ };
3374
+ //#endregion
3375
+ //#region src/process/roundPath.ts
3376
+ /**
3377
+ * Rounds the values of a `pathArray` instance to
3378
+ * a specified amount of decimals and returns it.
3379
+ *
3380
+ * @param path the source `pathArray`
3381
+ * @param roundOption the amount of decimals to round numbers to
3382
+ * @returns the resulted `pathArray` with rounded values
3383
+ */
3384
+ const roundPath = (path, roundOption) => {
3385
+ let { round } = defaultOptions;
3386
+ round = roundOption === "off" ? roundOption : typeof roundOption === "number" && roundOption >= 0 ? roundOption : typeof round === "number" && round >= 0 ? round : "off";
3387
+ if (round === "off") return path.slice(0);
3388
+ return iterate(path, (segment) => {
3389
+ return roundSegment(segment, round);
3390
+ });
3391
+ };
3392
+ //#endregion
3393
+ //#region src/morph/fixPath.ts
3394
+ /**
3395
+ * Checks a `PathArray` for an unnecessary `Z` segment
3396
+ * and returns a new `PathArray` without it.
3397
+ * In short, if the segment before `Z` extends to `M`,
3398
+ * the `Z` segment must be removed.
3399
+ *
3400
+ * The `pathInput` must be a single path, without
3401
+ * sub-paths. For multi-path `<path>` elements,
3402
+ * use `splitPath` first and apply this utility on each
3403
+ * sub-path separately.
3404
+ *
3405
+ * @param pathInput the `pathArray` source
3406
+ * @returns void
3407
+ */
3408
+ const fixPath = (pathInput) => {
3409
+ const pathArray = parsePathString(pathInput);
3410
+ if (isClosedPath(pathArray)) {
3411
+ const normalArray = normalizePath(pathArray);
3412
+ const length = pathArray.length;
3413
+ const segBeforeZ = length - 2;
3414
+ const [mx, my] = normalArray[0].slice(1);
3415
+ const [x, y] = normalArray[segBeforeZ].slice(-2);
3416
+ if (mx === x && my === y) pathArray.splice(length - 1, 1);
3417
+ }
3418
+ };
3419
+ //#endregion
3420
+ //#region src/morph/splitCubicSegment.ts
3421
+ /**
3422
+ * Split a cubic Bézier into two cubics at parameter t [0–1].
3423
+ *
3424
+ * @param x1 - Start point X
3425
+ * @param y1 - Start point Y
3426
+ * @param x2 - First control point X
3427
+ * @param y2 - First control point Y
3428
+ * @param x3 - Second control point X
3429
+ * @param y3 - Second control point Y
3430
+ * @param x4 - End point X
3431
+ * @param y4 - End point Y
3432
+ * @param t - Parameter in range [0, 1] at which to split
3433
+ * @returns Array of two cubic segments, each as [x1,y1, x2,y2, x3,y3, x4,y4]
3434
+ */
3435
+ function splitCubicSegment(x1, y1, x2, y2, x3, y3, x4, y4, t) {
3436
+ const [px01, py01] = midPoint([x1, y1], [x2, y2], t);
3437
+ const [px12, py12] = midPoint([x2, y2], [x3, y3], t);
3438
+ const [px23, py23] = midPoint([x3, y3], [x4, y4], t);
3439
+ const [cx0, cy0] = midPoint([px01, py01], [px12, py12], t);
3440
+ const [cx1, cy1] = midPoint([px12, py12], [px23, py23], t);
3441
+ const [px, py] = midPoint([cx0, cy0], [cx1, cy1], t);
3442
+ return [[
3443
+ x1,
3444
+ y1,
3445
+ px01,
3446
+ py01,
3447
+ cx0,
3448
+ cy0,
3449
+ px,
3450
+ py
3451
+ ], [
3452
+ px,
3453
+ py,
3454
+ cx1,
3455
+ cy1,
3456
+ px23,
3457
+ py23,
3458
+ x4,
3459
+ y4
3460
+ ]];
3461
+ }
3462
+ //#endregion
3463
+ //#region src/morph/pathToPolyline.ts
3464
+ /**
3465
+ * Converts any `PolyLineArray`/`PolygonArray` path (closed or open) to an explicit polyline (M + L*).
3466
+ * If the path is closed (has Z), the Z is replaced with an explicit L back to the initial M point.
3467
+ * This allows uniform processing without special-casing Z.
3468
+ *
3469
+ * @param path string or PathArray
3470
+ * @returns PolylineArray (M + L*) — never contains Z
3471
+ */
3472
+ const pathToPolyline = (path) => {
3473
+ const normal = normalizePath(path);
3474
+ if (!isPolygonArray(normal) && !isPolylineArray(normal)) throw TypeError(`${error}: pathValue is not a polyline/polygon`);
3475
+ if (!isClosedPath(normal)) return normal;
3476
+ const result = [normal[0]];
3477
+ const [mx, my] = normal[0].slice(1);
3478
+ for (let i = 1; i < normal.length; i++) {
3479
+ const seg = normal[i];
3480
+ if (seg[0].toUpperCase() === "Z") result.push([
3481
+ "L",
3482
+ mx,
3483
+ my
3484
+ ]);
3485
+ else result.push(seg);
3486
+ }
3487
+ return result;
3488
+ };
3489
+ //#endregion
3490
+ //#region src/morph/splitLineToCount.ts
3491
+ /**
3492
+ * Split a line segment into `count` smaller segments of equal length
3493
+ * using the same repeated front-cutting strategy as splitCubicToCount.
3494
+ *
3495
+ * Does NOT mutate input.
3496
+ *
3497
+ * @param x1 - Start point X
3498
+ * @param y1 - Start point Y
3499
+ * @param x2 - End point X
3500
+ * @param y2 - End point Y
3501
+ * @param count - Number of segments to split into
3502
+ * @returns Array of `count` line segments, each as [x1, y1, x2, y2]
3503
+ */
3504
+ function splitLineToCount(x1, y1, x2, y2, count) {
3505
+ if (count <= 1) return [[
3506
+ x1,
3507
+ y1,
3508
+ x2,
3509
+ y2
3510
+ ]];
3511
+ const result = [];
3512
+ const dx = x2 - x1;
3513
+ const dy = y2 - y1;
3514
+ let currentX = x1;
3515
+ let currentY = y1;
3516
+ let i = 0;
3517
+ while (i < count) {
3518
+ const t = 1 / (count - i);
3519
+ const nextX = x1 + t * dx;
3520
+ const nextY = y1 + t * dy;
3521
+ result.push([
3522
+ currentX,
3523
+ currentY,
3524
+ nextX,
3525
+ nextY
3526
+ ]);
3527
+ currentX = nextX;
3528
+ currentY = nextY;
3529
+ i++;
3530
+ }
3531
+ return result;
3532
+ }
3533
+ //#endregion
3534
+ //#region src/morph/getPathSplits.ts
3535
+ /**
3536
+ * Determine the right amount of splits for each segment in a given PathArray
3537
+ * and for a target total amount of sub-segments.
3538
+ * For a triangle path "M0,0 L600,300 L0,600 Z" we have 3 equal lines,
3539
+ * we can easily do 4 splits per line and go to town, however, most triangles
3540
+ * are not even so we need to take side lengths into account.
3541
+ * @param path The target PathArray
3542
+ * @param target The total amount of sub-segments
3543
+ * @returns an array of numbers reprezenting the sub-segment count for each segment
3544
+ */
3545
+ function getPathSplits(path, target) {
3546
+ if (target <= 1) throw new TypeError(`${error}: target must be >= 2`);
3547
+ const totalLength = getTotalLength(path);
3548
+ if (totalLength === 0) return Array(path.length).fill(1);
3549
+ const idealSegLen = totalLength / target;
3550
+ const isPoly = isPolylineArray(path);
3551
+ const splits = [1];
3552
+ const lengths = [0];
3553
+ iterate(path, (seg, i, prevX, prevY) => {
3554
+ if (i > 0) {
3555
+ const [endX, endY] = seg.slice(-2);
3556
+ const segLen = isPoly ? getLineLength(prevX, prevY, endX, endY) : getCubicLength(prevX, prevY, seg[1], seg[2], seg[3], seg[4], seg[5], seg[6]);
3557
+ lengths.push(segLen);
3558
+ splits.push(1);
3559
+ }
3560
+ });
3561
+ let totalAllocated = 1;
3562
+ for (let i = 1; i < lengths.length; i++) {
3563
+ const segLen = lengths[i];
3564
+ const desired = segLen > idealSegLen ? Math.round(segLen / idealSegLen) : 1;
3565
+ splits[i] = desired;
3566
+ totalAllocated += desired;
3567
+ }
3568
+ let diff = target - totalAllocated;
3569
+ if (diff !== 0) {
3570
+ const candidates = [];
3571
+ for (let i = 1; i < lengths.length; i++) if (lengths[i] > 0) candidates.push([i, lengths[i]]);
3572
+ const cLen = candidates.length;
3573
+ if (diff < 0) {
3574
+ candidates.sort((a, b) => a[1] - b[1]);
3575
+ for (let i = 0; i < cLen; i++) {
3576
+ const idx = candidates[i][0];
3577
+ if (splits[idx] > 1 && candidates[i][1] > 0) {
3578
+ splits[idx]--;
3579
+ diff++;
3580
+ }
3581
+ if (diff === 0) break;
3582
+ else if (i === cLen - 1) i = 0;
3583
+ }
3584
+ } else if (diff > 0) {
3585
+ candidates.sort((a, b) => b[1] - a[1]);
3586
+ for (let i = 0; i < cLen; i++) {
3587
+ const idx = candidates[i][0];
3588
+ if (candidates[i][1] > 0) {
3589
+ splits[idx]++;
3590
+ diff--;
3591
+ }
3592
+ if (diff === 0) break;
3593
+ else if (i === cLen - 1) i = 0;
3594
+ }
3595
+ }
3596
+ }
3597
+ return splits;
3598
+ }
3599
+ //#endregion
3600
+ //#region src/morph/splitLinePathToCount.ts
3601
+ /**
3602
+ * Splits a PolylineArray so that it has exactly `target` line segments.
3603
+ *
3604
+ * @param path - The polyline array to split
3605
+ * @param target - The desired number of line segments
3606
+ * @returns The modified polyline array with the target segment count
3607
+ */
3608
+ function splitLinePathToCount(path, target) {
3609
+ if (path.length < 2 || target <= 1) return path;
3610
+ const splits = getPathSplits(path, target);
3611
+ let totalAdded = 0;
3612
+ const newPath = [path[0]];
3613
+ const pathLen = path.length;
3614
+ let currentX = path[0][1];
3615
+ let currentY = path[0][2];
3616
+ for (let i = 1; i < pathLen; i++) {
3617
+ const [endX, endY] = path[i].slice(1);
3618
+ const count = splits[i];
3619
+ if (count >= 1) {
3620
+ const subLines = splitLineToCount(currentX, currentY, endX, endY, count);
3621
+ for (const sub of subLines) {
3622
+ newPath.push([
3623
+ "L",
3624
+ sub[2],
3625
+ sub[3]
3626
+ ]);
3627
+ totalAdded++;
3628
+ }
3629
+ }
3630
+ currentX = endX;
3631
+ currentY = endY;
3632
+ }
3633
+ if (newPath.length !== target) console.warn(`${error}: requested ${target} segments, got ${newPath.length}. Adjusted on last segment.`);
3634
+ return newPath;
3635
+ }
3636
+ //#endregion
3637
+ //#region src/morph/splitCubicToCount.ts
3638
+ /**
3639
+ * Split a cubic Bézier into `count` segments of roughly equal parameter length.
3640
+ * Does NOT mutate input parameters.
3641
+ *
3642
+ * @param x1 - Start point X
3643
+ * @param y1 - Start point Y
3644
+ * @param x2 - First control point X
3645
+ * @param y2 - First control point Y
3646
+ * @param x3 - Second control point X
3647
+ * @param y3 - Second control point Y
3648
+ * @param x4 - End point X
3649
+ * @param y4 - End point Y
3650
+ * @param count - Number of segments to split into
3651
+ * @returns Array of `count` cubic segments, each as [x1,y1,x2,y2,x3,y3,x4,y4]
3652
+ */
3653
+ function splitCubicToCount(x1, y1, x2, y2, x3, y3, x4, y4, count) {
3654
+ if (count <= 1) return [[
3655
+ x1,
3656
+ y1,
3657
+ x2,
3658
+ y2,
3659
+ x3,
3660
+ y3,
3661
+ x4,
3662
+ y4
3663
+ ]];
3664
+ const result = [];
3665
+ let cx1 = x1;
3666
+ let cy1 = y1;
3667
+ let cx2 = x2;
3668
+ let cy2 = y2;
3669
+ let cx3 = x3;
3670
+ let cy3 = y3;
3671
+ let cx4 = x4;
3672
+ let cy4 = y4;
3673
+ let i = 0;
3674
+ while (i < count) {
3675
+ const t = 1 / (count - i);
3676
+ const [first, second] = splitCubicSegment(cx1, cy1, cx2, cy2, cx3, cy3, cx4, cy4, t);
3677
+ result.push(first);
3678
+ [cx1, cy1, cx2, cy2, cx3, cy3, cx4, cy4] = second;
3679
+ i++;
3680
+ }
3681
+ return result;
3682
+ }
3683
+ //#endregion
3684
+ //#region src/morph/splitCurvePathToCount.ts
3685
+ /**
3686
+ * Splits a CurveArray so that it has exactly `target` cubic segments.
3687
+ *
3688
+ * @param path - The curve array to split
3689
+ * @param target - The desired number of cubic segments
3690
+ * @returns The modified curve array with the target segment count
3691
+ */
3692
+ function splitCurvePathToCount(path, target) {
3693
+ if (path.length < 2 || target <= 1) return path;
3694
+ const splits = getPathSplits(path, target);
3695
+ let totalAdded = 0;
3696
+ const newPath = [path[0]];
3697
+ const pathLen = path.length;
3698
+ let currentX = path[0][1];
3699
+ let currentY = path[0][2];
3700
+ for (let i = 1; i < pathLen; i++) {
3701
+ const seg = path[i];
3702
+ const [endX, endY] = seg.slice(-2);
3703
+ const count = splits[i];
3704
+ if (count >= 1) {
3705
+ const subs = splitCubicToCount(currentX, currentY, seg[1], seg[2], seg[3], seg[4], seg[5], seg[6], count);
3706
+ for (const sub of subs) {
3707
+ newPath.push([
3708
+ "C",
3709
+ sub[2],
3710
+ sub[3],
3711
+ sub[4],
3712
+ sub[5],
3713
+ sub[6],
3714
+ sub[7]
3715
+ ]);
3716
+ totalAdded++;
3717
+ }
3718
+ }
3719
+ currentX = endX;
3720
+ currentY = endY;
3721
+ }
3722
+ if (newPath.length !== target) console.warn(`${error}: requested ${target} segments, got ${newPath.length}.`);
3723
+ return newPath;
3724
+ }
3725
+ //#endregion
3726
+ //#region src/morph/samplePolygon.ts
3727
+ /**
3728
+ * Samples points from a path to form a polygon approximation.
3729
+ * Collects endpoints of each segment (M start + ends of L/C/etc).
3730
+ *
3731
+ * If `sampleSize` parameter is provided, it will return a polygon
3732
+ * equivalent to the original `PathArray`.
3733
+ * @param path `PolygonPathArray` or `CurvePathArray`
3734
+ * @returns Array of [x, y] points
3735
+ */
3736
+ function samplePolygon(path) {
3737
+ const points = [];
3738
+ let [mx, my] = [0, 0];
3739
+ iterate(path, (seg) => {
3740
+ const cmd = seg[0];
3741
+ if (cmd === "M") {
3742
+ [mx, my] = [seg[1], seg[2]];
3743
+ points.push([mx, my]);
3744
+ } else if (cmd === "L") points.push([seg[1], seg[2]]);
3745
+ else if (cmd === "C") points.push([seg[5], seg[6]]);
3746
+ else if (cmd === "A") points.push([seg[6], seg[7]]);
3747
+ else if (cmd === "Z") points.push([mx, my]);
3748
+ else throw new TypeError(`${error}: path command "${cmd}" is not supported`);
3749
+ });
3750
+ return points;
3751
+ }
3752
+ //#endregion
3753
+ //#region src/morph/getRotatedPath.ts
3754
+ /**
3755
+ * Returns all possible rotations of a path (line or curve) by shifting the start point.
3756
+ * Each rotation is a new PathArray starting at a different original segment.
3757
+ *
3758
+ * @param path PathArray (M + L/C + optional Z) — must be single subpath, normalized
3759
+ * @returns PathArray[] — array of all possible rotations
3760
+ */
3761
+ function getRotations(a) {
3762
+ const pathLen = a.length;
3763
+ const pointCount = pathLen - 1;
3764
+ let path;
3765
+ const result = [];
3766
+ for (let idx = 0; idx < pathLen; idx++) {
3767
+ path = [];
3768
+ for (let i = 0; i < pathLen; i++) {
3769
+ let oldSegIdx = idx + i;
3770
+ let seg;
3771
+ if (i === 0 || a[oldSegIdx] && a[oldSegIdx][0] === "M") {
3772
+ seg = a[oldSegIdx];
3773
+ path.push(["M", ...seg.slice(-2)]);
3774
+ continue;
3775
+ }
3776
+ if (oldSegIdx >= pathLen) oldSegIdx -= pointCount;
3777
+ path.push(a[oldSegIdx]);
3778
+ }
3779
+ result.push(path);
3780
+ }
3781
+ return result;
3782
+ }
3783
+ /**
3784
+ * Finds the best rotation of pathA to match pathB by minimizing sum of squared distances
3785
+ * between corresponding endpoints.
3786
+ *
3787
+ * Works with both polygon (L) and curve (C) arrays.
3788
+ *
3789
+ * @param pathA PathArray to rotate (will be modified in rotation options)
3790
+ * @param pathB PathArray reference (fixed)
3791
+ * @returns PathArray — best rotation of pathA
3792
+ */
3793
+ function getRotatedPath(pathA, pathB, computedRotations) {
3794
+ const rotations = computedRotations || getRotations(pathA);
3795
+ if (pathA.length !== pathB.length) throw new TypeError(error + ": paths must have the same number of segments after equalization");
3796
+ let bestIndex = 0;
3797
+ let minDistanceSq = Infinity;
3798
+ for (let ri = 0; ri < rotations.length; ri++) {
3799
+ const rotation = rotations[ri];
3800
+ const rLen = rotation.length;
3801
+ let sumDistSq = 0;
3802
+ for (let i = 0; i < rLen; i++) {
3803
+ const segA = rotation[i];
3804
+ const segB = pathB[i];
3805
+ const endA = segA.slice(-2);
3806
+ const endB = segB.slice(-2);
3807
+ const dx = endA[0] - endB[0];
3808
+ const dy = endA[1] - endB[1];
3809
+ sumDistSq += dx * dx + dy * dy;
3810
+ }
3811
+ if (sumDistSq < minDistanceSq) {
3812
+ minDistanceSq = sumDistSq;
3813
+ bestIndex = ri;
3814
+ }
3815
+ }
3816
+ return rotations[bestIndex];
3817
+ }
3818
+ //#endregion
3819
+ //#region src/morph/equalizeSegments.ts
3820
+ const equalizeSegmentsDefaults = {
3821
+ mode: "auto",
3822
+ sampleSize: 10,
3823
+ roundValues: 4,
3824
+ reverse: true,
3825
+ close: false,
3826
+ target: void 0
3827
+ };
3828
+ /**
3829
+ * Equalizes two paths for morphing (single subpath only).
3830
+ *
3831
+ * @see https://minus-ze.ro/posts/morphing-arbitrary-paths-in-svg/
3832
+ * @param path1 - First path string or PathArray
3833
+ * @param path2 - Second path string or PathArray
3834
+ * @param initialCfg - Equalization options
3835
+ * @returns Tuple of two equalized MorphPathArrays
3836
+ *
3837
+ * @example
3838
+ * ```ts
3839
+ * const [eq1, eq2] = equalizeSegments('M0 0L100 0L50 100Z', 'M0 0L100 0L100 100L0 100Z')
3840
+ * // eq1.length === eq2.length
3841
+ * ```
3842
+ */
3843
+ const equalizeSegments = (path1, path2, initialCfg = {}) => {
3844
+ const { close, mode, reverse, roundValues, target: initialTarget } = Object.assign(equalizeSegmentsDefaults, initialCfg);
3845
+ let p1 = normalizePath(path1);
3846
+ let p2 = normalizePath(path2);
3847
+ fixPath(p1);
3848
+ fixPath(p2);
3849
+ let bothPoly = (isPolygonArray(p1) || isPolylineArray(p1)) && (isPolygonArray(p2) || isPolylineArray(p2));
3850
+ if (bothPoly && mode === "auto") {
3851
+ p1 = pathToPolyline(p1);
3852
+ p2 = pathToPolyline(p2);
3853
+ } else {
3854
+ bothPoly = false;
3855
+ p1 = pathToCurve(p1);
3856
+ p2 = pathToCurve(p2);
3857
+ }
3858
+ const area1 = polygonArea(samplePolygon(p1));
3859
+ const area2 = polygonArea(samplePolygon(p2));
3860
+ if (reverse !== false && Math.sign(area1) !== Math.sign(area2)) p2 = reversePath(p2);
3861
+ const segCount1 = p1.length;
3862
+ const segCount2 = p2.length;
3863
+ const minTarget = Math.max(segCount1, segCount2);
3864
+ let target = minTarget;
3865
+ if (typeof initialTarget !== "number") {
3866
+ const avgLen = (getTotalLength(p1) + getTotalLength(p2)) / 2;
3867
+ const avgSegLen = avgLen / Math.max(segCount1, segCount2);
3868
+ const idealSegCount = Math.max(minTarget, Math.round(avgLen / Math.max(avgSegLen, 1)));
3869
+ target = Math.min(idealSegCount, Math.max(segCount1, segCount2) * 3);
3870
+ } else if (initialTarget >= minTarget) target = initialTarget;
3871
+ else console.warn("equalizeSegments \"target\" option: " + initialTarget + ", expected >= " + minTarget);
3872
+ let equalP1 = p1;
3873
+ let equalP2 = p2;
3874
+ if (bothPoly) {
3875
+ equalP1 = splitLinePathToCount(p1, target);
3876
+ equalP2 = splitLinePathToCount(p2, target);
3877
+ } else {
3878
+ equalP1 = splitCurvePathToCount(p1, target);
3879
+ equalP2 = splitCurvePathToCount(p2, target);
3880
+ }
3881
+ equalP2 = getRotatedPath(equalP2, equalP1);
3882
+ if (typeof roundValues === "number" && roundValues !== 4) {
3883
+ equalP1 = roundPath(equalP1, roundValues);
3884
+ equalP2 = roundPath(equalP2, roundValues);
3885
+ }
3886
+ if (close) {
3887
+ equalP1.push(["Z"]);
3888
+ equalP2.push(["Z"]);
3889
+ }
3890
+ return [equalP1, equalP2];
3891
+ };
3892
+ //#endregion
3893
+ //#region src/util/pathIntersection.ts
3894
+ const intersect = (x1, y1, x2, y2, x3, y3, x4, y4) => {
3895
+ if (Math.max(x1, x2) < Math.min(x3, x4) || Math.min(x1, x2) > Math.max(x3, x4) || Math.max(y1, y2) < Math.min(y3, y4) || Math.min(y1, y2) > Math.max(y3, y4)) return;
3896
+ const nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4), ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4), denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
3897
+ if (!denominator) return;
3898
+ const px = nx / denominator, py = ny / denominator, px2 = roundTo(px, 2), py2 = roundTo(py, 2);
3899
+ if (px2 < roundTo(Math.min(x1, x2), 2) || px2 > roundTo(Math.max(x1, x2), 2) || px2 < roundTo(Math.min(x3, x4), 2) || px2 > roundTo(Math.max(x3, x4), 2) || py2 < roundTo(Math.min(y1, y2), 2) || py2 > roundTo(Math.max(y1, y2), 2) || py2 < roundTo(Math.min(y3, y4), 2) || py2 > roundTo(Math.max(y3, y4), 2)) return;
3900
+ return {
3901
+ x: px,
3902
+ y: py
3903
+ };
3904
+ };
3905
+ /**
3906
+ * Checks if a point is inside a bounding box.
3907
+ *
3908
+ * @param bbox - The bounding box as [minX, minY, maxX, maxY]
3909
+ * @param point - The point as [x, y]
3910
+ * @returns True if the point is inside or on the edge of the bounding box
3911
+ */
3912
+ const isPointInsideBBox = (bbox, [x, y]) => {
3913
+ const [minX, minY, maxX, maxY] = bbox;
3914
+ return x >= minX && x <= maxX && y >= minY && y <= maxY;
3915
+ };
3916
+ /**
3917
+ * Checks if two bounding boxes intersect.
3918
+ *
3919
+ * @param a - First bounding box as [minX, minY, maxX, maxY]
3920
+ * @param b - Second bounding box as [minX, minY, maxX, maxY]
3921
+ * @returns True if the bounding boxes overlap
3922
+ */
3923
+ const boundingBoxIntersect = (a, b) => {
3924
+ const [ax1, ay1, ax2, ay2] = a;
3925
+ const [bx1, by1, bx2, by2] = b;
3926
+ return isPointInsideBBox(b, [ax1, ay1]) || isPointInsideBBox(b, [ax2, ay1]) || isPointInsideBBox(b, [ax1, ay2]) || isPointInsideBBox(b, [ax2, ay2]) || isPointInsideBBox(a, [bx1, by1]) || isPointInsideBBox(a, [bx2, by1]) || isPointInsideBBox(a, [bx1, by2]) || isPointInsideBBox(a, [bx2, by2]) || (ax1 < bx2 && ax1 > bx1 || bx1 < ax2 && bx1 > ax1) && (ay1 < by2 && ay1 > by1 || by1 < ay2 && by1 > ay1);
3927
+ };
3928
+ const interHelper = (bez1, bez2, config) => {
3929
+ const bbox1 = getCubicBBox(...bez1);
3930
+ const bbox2 = getCubicBBox(...bez2);
3931
+ const { justCount, epsilon } = Object.assign({
3932
+ justCount: true,
3933
+ epsilon: DISTANCE_EPSILON
3934
+ }, config);
3935
+ if (!boundingBoxIntersect(bbox1, bbox2)) return justCount ? 0 : [];
3936
+ const l1 = getCubicLength(...bez1), l2 = getCubicLength(...bez2), n1 = Math.max(l1 / 5 >> 0, 1), n2 = Math.max(l2 / 5 >> 0, 1), points1 = [], points2 = [], xy = {};
3937
+ let res = justCount ? 0 : [];
3938
+ for (let i = 0; i < n1 + 1; i++) {
3939
+ const p = getPointAtCubicLength(...bez1, i / n1 * l1);
3940
+ points1.push({
3941
+ x: p.x,
3942
+ y: p.y,
3943
+ t: i / n1
3944
+ });
3945
+ }
3946
+ for (let i = 0; i < n2 + 1; i++) {
3947
+ const p = getPointAtCubicLength(...bez2, i / n2 * l2);
3948
+ points2.push({
3949
+ x: p.x,
3950
+ y: p.y,
3951
+ t: i / n2
3952
+ });
3953
+ }
3954
+ for (let i = 0; i < n1; i++) for (let j = 0; j < n2; j++) {
3955
+ const maxLimit = 1 + epsilon, di = points1[i], di1 = points1[i + 1], dj = points2[j], dj1 = points2[j + 1], ci = Math.abs(di1.x - di.x) < .001 ? "y" : "x", cj = Math.abs(dj1.x - dj.x) < .001 ? "y" : "x", is = intersect(di.x, di.y, di1.x, di1.y, dj.x, dj.y, dj1.x, dj1.y);
3956
+ if (is) {
3957
+ if (xy[is.x.toFixed(4)] == is.y.toFixed(4)) continue;
3958
+ xy[is.x.toFixed(4)] = is.y.toFixed(4);
3959
+ const t1 = di.t + Math.abs((is[ci] - di[ci]) / (di1[ci] - di[ci])) * (di1.t - di.t), t2 = dj.t + Math.abs((is[cj] - dj[cj]) / (dj1[cj] - dj[cj])) * (dj1.t - dj.t);
3960
+ if (t1 >= 0 && t1 <= maxLimit && t2 >= 0 && t2 <= maxLimit) if (justCount) res++;
3961
+ else res.push({
3962
+ x: is.x,
3963
+ y: is.y,
3964
+ t1: Math.min(t1, 1),
3965
+ t2: Math.min(t2, 1)
3966
+ });
3967
+ }
3968
+ }
3969
+ return res;
3970
+ };
3971
+ /**
3972
+ * Finds intersection points between two paths.
3973
+ *
3974
+ * @param pathInput1 - First path string or PathArray
3975
+ * @param pathInput2 - Second path string or PathArray
3976
+ * @param justCount - If true, returns the count of intersections; if false, returns the intersection points
3977
+ * @returns The number of intersections (when justCount is true) or an array of IntersectionPoint objects
3978
+ *
3979
+ * @example
3980
+ * ```ts
3981
+ * pathsIntersection('M0 50C0 0,100 0,100 50', 'M50 0C100 0,100 100,50 100', true)
3982
+ * // => 1
3983
+ * pathsIntersection('M0 50C0 0,100 0,100 50', 'M50 0C100 0,100 100,50 100', false)
3984
+ * // => [{ x: 50, y: 25, t1: 0.5, t2: 0.5 }]
3985
+ * ```
3986
+ */
3987
+ const pathsIntersection = (pathInput1, pathInput2, justCount = true) => {
3988
+ const path1 = pathToCurve(pathInput1);
3989
+ const path2 = pathToCurve(pathInput2);
3990
+ let x1 = 0, y1 = 0, x2 = 0, y2 = 0, x1m = 0, y1m = 0, x2m = 0, y2m = 0, bez1 = [
3991
+ x1,
3992
+ y1,
3993
+ x1,
3994
+ y1,
3995
+ x1m,
3996
+ y1m,
3997
+ x1m,
3998
+ y1m
3999
+ ], bez2 = [
4000
+ x2,
4001
+ y2,
4002
+ x2,
4003
+ y2,
4004
+ x2m,
4005
+ y2m,
4006
+ x2m,
4007
+ y2m
4008
+ ], countResult = 0;
4009
+ const pointsResult = [];
4010
+ const pathLen1 = path1.length;
4011
+ const pathLen2 = path2.length;
4012
+ for (let i = 0; i < pathLen1; i++) {
4013
+ const seg1 = path1[i];
4014
+ if (seg1[0] == "M") {
4015
+ x1 = seg1[1];
4016
+ y1 = seg1[2];
4017
+ x1m = x1;
4018
+ y1m = y1;
4019
+ } else {
4020
+ if (seg1[0] == "C") {
4021
+ bez1 = [
4022
+ x1,
4023
+ y1,
4024
+ seg1[1],
4025
+ seg1[2],
4026
+ seg1[3],
4027
+ seg1[4],
4028
+ seg1[5],
4029
+ seg1[6]
4030
+ ];
4031
+ x1 = bez1[6];
4032
+ y1 = bez1[7];
4033
+ } else {
4034
+ bez1 = [
4035
+ x1,
4036
+ y1,
4037
+ x1,
4038
+ y1,
4039
+ x1m,
4040
+ y1m,
4041
+ x1m,
4042
+ y1m
4043
+ ];
4044
+ x1 = x1m;
4045
+ y1 = y1m;
4046
+ }
4047
+ for (let j = 0; j < pathLen2; j++) {
4048
+ const seg2 = path2[j];
4049
+ if (seg2[0] == "M") {
4050
+ x2 = seg2[1];
4051
+ y2 = seg2[2];
4052
+ x2m = x2;
4053
+ y2m = y2;
4054
+ } else if (seg2[0] == "C") {
4055
+ bez2 = [
4056
+ x2,
4057
+ y2,
4058
+ seg2[1],
4059
+ seg2[2],
4060
+ seg2[3],
4061
+ seg2[4],
4062
+ seg2[5],
4063
+ seg2[6]
4064
+ ];
4065
+ x2 = bez2[6];
4066
+ y2 = bez2[7];
4067
+ }
4068
+ const intr = interHelper(bez1, bez2, { justCount });
4069
+ if (justCount) countResult += intr;
4070
+ else pointsResult.push(...intr);
4071
+ }
4072
+ }
4073
+ }
4074
+ return justCount ? countResult : pointsResult;
4075
+ };
4076
+ //#endregion
4077
+ //#region src/morph/createPlaceholder.ts
4078
+ /**
4079
+ * Create a degenerate `PathArray` at a given coordinate
4080
+ * to serve as a pair for another `PathArray`.
4081
+ * @param param0 An [x, y] tuple for the coordinate
4082
+ * @returns A new degenerate `PathArray`
4083
+ */
4084
+ const createPlaceholder = ([atx, aty]) => {
4085
+ const r = .001;
4086
+ return [
4087
+ [
4088
+ "M",
4089
+ atx,
4090
+ aty
4091
+ ],
4092
+ [
4093
+ "L",
4094
+ atx + r,
4095
+ aty
4096
+ ],
4097
+ [
4098
+ "L",
4099
+ atx + r,
4100
+ aty + r
4101
+ ],
4102
+ [
4103
+ "L",
4104
+ atx,
4105
+ aty + r
4106
+ ],
4107
+ [
4108
+ "L",
4109
+ atx,
4110
+ aty
4111
+ ],
4112
+ ["Z"]
4113
+ ];
4114
+ };
4115
+ //#endregion
4116
+ //#region src/morph/matchPaths.ts
4117
+ function getBestMatch(target, candidates) {
4118
+ const targetBBox = target.bbox;
4119
+ const potentialCandidates = [];
4120
+ for (let i = 0; i < candidates.length; i++) {
4121
+ const { bbox, size } = candidates[i];
4122
+ const dx = targetBBox.cx - bbox.cx;
4123
+ const dy = targetBBox.cy - bbox.cy;
4124
+ const centeredDistance = Math.sqrt(dx * dx + dy * dy);
4125
+ const sizeDifference = Math.abs(target.size - size) / Math.max(target.size, size, 1e-6);
4126
+ const hasOverlap = isPointInsideBBox([
4127
+ targetBBox.x,
4128
+ targetBBox.y,
4129
+ targetBBox.x2,
4130
+ targetBBox.y2
4131
+ ], [bbox.cx, bbox.cy]) || isPointInsideBBox([
4132
+ bbox.x,
4133
+ bbox.y,
4134
+ bbox.x2,
4135
+ bbox.y2
4136
+ ], [targetBBox.cx, targetBBox.cy]);
4137
+ const boxIntersect = boundingBoxIntersect([
4138
+ targetBBox.x,
4139
+ targetBBox.y,
4140
+ targetBBox.x2,
4141
+ targetBBox.y2
4142
+ ], [
4143
+ bbox.x,
4144
+ bbox.y,
4145
+ bbox.x2,
4146
+ bbox.y2
4147
+ ]);
4148
+ potentialCandidates.push({
4149
+ index: i,
4150
+ hasOverlap,
4151
+ boxIntersect,
4152
+ sizeDifference,
4153
+ centeredDistance
4154
+ });
4155
+ }
4156
+ const overlaping = potentialCandidates.filter((c) => c.hasOverlap && c.boxIntersect);
4157
+ if (overlaping.length > 0) {
4158
+ let best = overlaping[0];
4159
+ for (let i = 1; i < overlaping.length; i++) if (overlaping[i].centeredDistance < best.centeredDistance) best = overlaping[i];
4160
+ return candidates.splice(best.index, 1)[0];
4161
+ }
4162
+ return null;
4163
+ }
4164
+ /**
4165
+ * Matches paths from two sets by proximity and size similarity.
4166
+ * Unmatched paths receive placeholder paths at their centroid.
4167
+ *
4168
+ * @param fromPaths - Source path features to match from
4169
+ * @param toPaths - Target path features to match to
4170
+ * @returns Array of paired NormalArrays [from, to]
4171
+ */
4172
+ function matchPaths(fromPaths, toPaths) {
4173
+ const pairs = [];
4174
+ fromPaths.sort((a, b) => b.size - a.size);
4175
+ toPaths.sort((a, b) => b.size - a.size);
4176
+ while (fromPaths.length > 0) {
4177
+ const from = fromPaths.shift();
4178
+ const bestTo = getBestMatch(from, toPaths);
4179
+ if (bestTo) pairs.push([from.path, bestTo.path]);
4180
+ else {
4181
+ const fromCentroid = [from.bbox.cx, from.bbox.cy];
4182
+ pairs.push([from.path, createPlaceholder(fromCentroid)]);
4183
+ }
4184
+ }
4185
+ while (toPaths.length > 0) {
4186
+ const to = toPaths.shift();
4187
+ const toCentroid = [to.bbox.cx, to.bbox.cy];
4188
+ pairs.push([createPlaceholder(toCentroid), to.path]);
4189
+ }
4190
+ return pairs;
4191
+ }
4192
+ //#endregion
4193
+ //#region src/morph/classifyPaths.ts
4194
+ /**
4195
+ * Classifies paths into outer (containing) and inner (hole) paths.
4196
+ *
4197
+ * @param paths - Array of normalized path arrays to classify
4198
+ * @returns Object with `outers` (containing shapes) and `inners` (holes)
4199
+ */
4200
+ const classifyPaths = (paths) => {
4201
+ const outers = [];
4202
+ const inners = [];
4203
+ for (const path of paths) {
4204
+ const signedArea = polygonArea(samplePolygon(path));
4205
+ const bbox = getPathBBox(path);
4206
+ const feature = {
4207
+ isPoly: isPolygonArray(path) || isPolylineArray(path),
4208
+ size: bbox.width * bbox.height,
4209
+ path,
4210
+ signedArea,
4211
+ area: Math.abs(signedArea),
4212
+ bbox
4213
+ };
4214
+ if (signedArea > 0) outers.push(feature);
4215
+ else inners.push(feature);
4216
+ }
4217
+ return {
4218
+ outers,
4219
+ inners
4220
+ };
4221
+ };
4222
+ //#endregion
4223
+ //#region src/morph/equalizePaths.ts
4224
+ const equalizePathsDefaults = {
4225
+ mode: "auto",
4226
+ roundValues: 4,
4227
+ close: false,
4228
+ sampleSize: 10
4229
+ };
4230
+ /**
4231
+ * Equalizes two paths for morphing (single/multi subpath).
4232
+ *
4233
+ * @see https://minus-ze.ro/posts/morphing-arbitrary-paths-in-svg/
4234
+ * @param pathInput1 - First path string or PathArray
4235
+ * @param pathInput2 - Second path string or PathArray
4236
+ * @param initialCfg - Configuration options for equalization
4237
+ * @returns Tuple of two equalized MorphPathArrays
4238
+ *
4239
+ * @example
4240
+ * ```ts
4241
+ * const [eq1, eq2] = equalizePaths('M0 0L100 0L50 100Z', 'M0 0L100 0L100 100L0 100Z')
4242
+ * // eq1.length === eq2.length — ready for morphing
4243
+ * ```
4244
+ */
4245
+ const equalizePaths = (pathInput1, pathInput2, initialCfg = {}) => {
4246
+ const cfg = Object.assign(equalizePathsDefaults, initialCfg);
4247
+ const p1 = normalizePath(pathInput1);
4248
+ const p2 = normalizePath(pathInput2);
4249
+ const multi1 = isMultiPath(p1);
4250
+ const multi2 = isMultiPath(p2);
4251
+ if (!multi1 && !multi2) return equalizeSegments(p1, p2, cfg);
4252
+ const globalArea1 = polygonArea(samplePolygon(p1));
4253
+ const globalArea2 = polygonArea(samplePolygon(p2));
4254
+ let path1 = p1;
4255
+ let path2 = p2;
4256
+ if (Math.sign(globalArea1) < 0) path1 = reversePath(path1);
4257
+ if (Math.sign(globalArea2) < 0) path2 = reversePath(path2);
4258
+ const multiPath1 = splitPath(path1);
4259
+ const multiPath2 = splitPath(path2);
4260
+ const { outers: outers1, inners: inners1 } = classifyPaths(multiPath1);
4261
+ const { outers: outers2, inners: inners2 } = classifyPaths(multiPath2);
4262
+ const outerPairs = matchPaths(outers1, outers2);
4263
+ const innerPairs = matchPaths(inners1, inners2);
4264
+ const equalizedPairs = [];
4265
+ for (const [from, to] of [...outerPairs, ...innerPairs]) {
4266
+ const [eqFrom, eqTo] = equalizeSegments(from, to, {
4267
+ ...cfg,
4268
+ reverse: false
4269
+ });
4270
+ equalizedPairs.push([eqFrom, eqTo]);
4271
+ }
4272
+ return [equalizedPairs.map((p) => p[0]).flat(), equalizedPairs.map((p) => p[1]).flat()];
4273
+ };
4274
+ //#endregion
4275
+ export { PathParser, absolutizeSegment, arcToCubic, arcTools, bezierTools, boundingBoxIntersect, cubicTools, DISTANCE_EPSILON as distanceEpsilon, distanceSquareRoot, equalizePaths, equalizeSegments, finalizeSegment, fixPath, getClosestPoint, getDrawDirection, getPathArea, getPathBBox, getPointAtLength, getPropertiesAtLength, getPropertiesAtPoint, getSVGMatrix, getSegmentAtLength, getSegmentOfPoint, getTotalLength, invalidPathValue, isAbsoluteArray, isArcCommand, isClosedPath, isCurveArray, isDigit, isDigitStart, isMoveCommand, isMultiPath, isNormalizedArray, isPathArray, isPathCommand, isPointInStroke, isPointInsideBBox, isPolygonArray, isPolylineArray, isRelativeArray, isSpace, isValidPath, iterate, lineToCubic, lineTools, midPoint, normalizePath, normalizeSegment, optimizePath, paramsCounts, paramsParser, parsePathString, pathToAbsolute, pathToCurve, pathToRelative, pathToString, pathsIntersection, polygonTools, projection2d, quadToCubic, quadTools, relativizeSegment, reverseCurve, reversePath, rotateVector, roundPath, roundSegment, roundTo, scanFlag, scanParam, scanSegment, segmentToCubic, shapeParams, shapeToPath, shapeToPathArray, shortenSegment, skipSpaces, splitCubicSegment, splitPath, transformPath };
4276
+
4277
+ //# sourceMappingURL=util.js.map