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