modern-path2d 1.7.0 → 1.8.1
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.cjs +527 -130
- package/dist/index.d.cts +141 -2
- package/dist/index.d.mts +141 -2
- package/dist/index.d.ts +141 -2
- package/dist/index.js +3 -2
- package/dist/index.mjs +523 -131
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const earcut = require('earcut');
|
|
4
|
+
const polygonClipping = require('polygon-clipping');
|
|
4
5
|
|
|
5
6
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
6
7
|
|
|
7
8
|
const earcut__default = /*#__PURE__*/_interopDefaultCompat(earcut);
|
|
9
|
+
const polygonClipping__default = /*#__PURE__*/_interopDefaultCompat(polygonClipping);
|
|
8
10
|
|
|
9
11
|
function drawPoint(ctx, x, y, options = {}) {
|
|
10
12
|
const { radius = 1 } = options;
|
|
@@ -303,6 +305,63 @@ class BoundingBox {
|
|
|
303
305
|
}
|
|
304
306
|
}
|
|
305
307
|
|
|
308
|
+
function flatRingToPairs(r) {
|
|
309
|
+
const ring = [];
|
|
310
|
+
for (let i = 0; i < r.length; i += 2) {
|
|
311
|
+
ring.push([r[i], r[i + 1]]);
|
|
312
|
+
}
|
|
313
|
+
const first = ring[0];
|
|
314
|
+
const last = ring[ring.length - 1];
|
|
315
|
+
if (first && last && (first[0] !== last[0] || first[1] !== last[1])) {
|
|
316
|
+
ring.push([first[0], first[1]]);
|
|
317
|
+
}
|
|
318
|
+
return ring;
|
|
319
|
+
}
|
|
320
|
+
function ringsToGeom(rings) {
|
|
321
|
+
const valid = rings.filter((r) => r.length >= 6).map(flatRingToPairs);
|
|
322
|
+
if (!valid.length) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
let geom = [[valid[0]]];
|
|
326
|
+
for (let i = 1; i < valid.length; i++) {
|
|
327
|
+
geom = polygonClipping__default.xor(geom, [[valid[i]]]);
|
|
328
|
+
}
|
|
329
|
+
return geom;
|
|
330
|
+
}
|
|
331
|
+
function geomToRings(geom) {
|
|
332
|
+
const out = [];
|
|
333
|
+
for (const poly of geom) {
|
|
334
|
+
for (const ring of poly) {
|
|
335
|
+
const flat = [];
|
|
336
|
+
for (const [x, y] of ring) {
|
|
337
|
+
flat.push(x, y);
|
|
338
|
+
}
|
|
339
|
+
out.push(flat);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
function polygonBoolean(op, ringsA, ringsB) {
|
|
345
|
+
const a = ringsToGeom(ringsA);
|
|
346
|
+
const b = ringsToGeom(ringsB);
|
|
347
|
+
let res;
|
|
348
|
+
switch (op) {
|
|
349
|
+
case "union":
|
|
350
|
+
res = polygonClipping__default.union(a, b);
|
|
351
|
+
break;
|
|
352
|
+
case "intersection":
|
|
353
|
+
res = polygonClipping__default.intersection(a, b);
|
|
354
|
+
break;
|
|
355
|
+
case "difference":
|
|
356
|
+
res = polygonClipping__default.difference(a, b);
|
|
357
|
+
break;
|
|
358
|
+
case "xor":
|
|
359
|
+
res = polygonClipping__default.xor(a, b);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
return geomToRings(res);
|
|
363
|
+
}
|
|
364
|
+
|
|
306
365
|
function catmullRom(t, p0, p1, p2, p3) {
|
|
307
366
|
const v0 = (p2 - p0) * 0.5;
|
|
308
367
|
const v1 = (p3 - p1) * 0.5;
|
|
@@ -433,6 +492,195 @@ function cubicBezier(t, p0, p1, p2, p3) {
|
|
|
433
492
|
return cubicBezierP0(t, p0) + cubicBezierP1(t, p1) + cubicBezierP2(t, p2) + cubicBezierP3(t, p3);
|
|
434
493
|
}
|
|
435
494
|
|
|
495
|
+
function isLeft(ax, ay, bx, by, px, py) {
|
|
496
|
+
return (bx - ax) * (py - ay) - (px - ax) * (by - ay);
|
|
497
|
+
}
|
|
498
|
+
function windingNumber$1(px, py, vertices) {
|
|
499
|
+
const len = vertices.length;
|
|
500
|
+
let wn = 0;
|
|
501
|
+
for (let i = 0; i < len; i += 2) {
|
|
502
|
+
const ax = vertices[i];
|
|
503
|
+
const ay = vertices[i + 1];
|
|
504
|
+
const k = (i + 2) % len;
|
|
505
|
+
const bx = vertices[k];
|
|
506
|
+
const by = vertices[k + 1];
|
|
507
|
+
if (ay <= py) {
|
|
508
|
+
if (by > py && isLeft(ax, ay, bx, by, px, py) > 0) {
|
|
509
|
+
wn++;
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
if (by <= py && isLeft(ax, ay, bx, by, px, py) < 0) {
|
|
513
|
+
wn--;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return wn;
|
|
518
|
+
}
|
|
519
|
+
function crossingNumber(px, py, vertices) {
|
|
520
|
+
const len = vertices.length;
|
|
521
|
+
let cn = 0;
|
|
522
|
+
for (let i = 0; i < len; i += 2) {
|
|
523
|
+
const ax = vertices[i];
|
|
524
|
+
const ay = vertices[i + 1];
|
|
525
|
+
const k = (i + 2) % len;
|
|
526
|
+
const bx = vertices[k];
|
|
527
|
+
const by = vertices[k + 1];
|
|
528
|
+
if (ay <= py && by > py || ay > py && by <= py) {
|
|
529
|
+
const t = (py - ay) / (by - ay);
|
|
530
|
+
if (px < ax + t * (bx - ax)) {
|
|
531
|
+
cn++;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return cn;
|
|
536
|
+
}
|
|
537
|
+
function segmentDistance(px, py, ax, ay, bx, by) {
|
|
538
|
+
const dx = bx - ax;
|
|
539
|
+
const dy = by - ay;
|
|
540
|
+
const lenSq = dx * dx + dy * dy;
|
|
541
|
+
let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq;
|
|
542
|
+
if (t < 0) {
|
|
543
|
+
t = 0;
|
|
544
|
+
} else if (t > 1) {
|
|
545
|
+
t = 1;
|
|
546
|
+
}
|
|
547
|
+
const cx = ax + t * dx;
|
|
548
|
+
const cy = ay + t * dy;
|
|
549
|
+
return Math.hypot(px - cx, py - cy);
|
|
550
|
+
}
|
|
551
|
+
function pointInPolygon(point, vertices, fillRule = "nonzero") {
|
|
552
|
+
if (vertices.length < 6) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
if (fillRule === "evenodd") {
|
|
556
|
+
return (crossingNumber(point.x, point.y, vertices) & 1) === 1;
|
|
557
|
+
}
|
|
558
|
+
return windingNumber$1(point.x, point.y, vertices) !== 0;
|
|
559
|
+
}
|
|
560
|
+
function pointInPolygons(point, polygons, fillRule = "nonzero") {
|
|
561
|
+
const { x, y } = point;
|
|
562
|
+
if (fillRule === "evenodd") {
|
|
563
|
+
let cn = 0;
|
|
564
|
+
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
565
|
+
const ring = polygons[i];
|
|
566
|
+
if (ring.length >= 6) {
|
|
567
|
+
cn += crossingNumber(x, y, ring);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return (cn & 1) === 1;
|
|
571
|
+
}
|
|
572
|
+
let wn = 0;
|
|
573
|
+
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
574
|
+
const ring = polygons[i];
|
|
575
|
+
if (ring.length >= 6) {
|
|
576
|
+
wn += windingNumber$1(x, y, ring);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return wn !== 0;
|
|
580
|
+
}
|
|
581
|
+
function pointToSegmentDistance(point, a, b) {
|
|
582
|
+
return segmentDistance(point.x, point.y, a.x, a.y, b.x, b.y);
|
|
583
|
+
}
|
|
584
|
+
function pointToPolylineDistance(point, vertices, closed = false) {
|
|
585
|
+
const len = vertices.length;
|
|
586
|
+
if (len < 2) {
|
|
587
|
+
return Infinity;
|
|
588
|
+
}
|
|
589
|
+
const { x: px, y: py } = point;
|
|
590
|
+
if (len === 2) {
|
|
591
|
+
return Math.hypot(px - vertices[0], py - vertices[1]);
|
|
592
|
+
}
|
|
593
|
+
let min = Infinity;
|
|
594
|
+
for (let i = 0; i < len - 2; i += 2) {
|
|
595
|
+
const d = segmentDistance(px, py, vertices[i], vertices[i + 1], vertices[i + 2], vertices[i + 3]);
|
|
596
|
+
if (d < min) {
|
|
597
|
+
min = d;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (closed && len >= 6) {
|
|
601
|
+
const d = segmentDistance(px, py, vertices[len - 2], vertices[len - 1], vertices[0], vertices[1]);
|
|
602
|
+
if (d < min) {
|
|
603
|
+
min = d;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return min;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function boundsOf(ring) {
|
|
610
|
+
if (ring.length < 6) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
let minX = Infinity;
|
|
614
|
+
let minY = Infinity;
|
|
615
|
+
let maxX = -Infinity;
|
|
616
|
+
let maxY = -Infinity;
|
|
617
|
+
for (let i = 0; i < ring.length; i += 2) {
|
|
618
|
+
const x = ring[i];
|
|
619
|
+
const y = ring[i + 1];
|
|
620
|
+
if (x < minX)
|
|
621
|
+
minX = x;
|
|
622
|
+
if (y < minY)
|
|
623
|
+
minY = y;
|
|
624
|
+
if (x > maxX)
|
|
625
|
+
maxX = x;
|
|
626
|
+
if (y > maxY)
|
|
627
|
+
maxY = y;
|
|
628
|
+
}
|
|
629
|
+
return { minX, minY, maxX, maxY };
|
|
630
|
+
}
|
|
631
|
+
function bboxInside(inner, outer) {
|
|
632
|
+
return inner.minX >= outer.minX && inner.maxX <= outer.maxX && inner.minY >= outer.minY && inner.maxY <= outer.maxY;
|
|
633
|
+
}
|
|
634
|
+
function ringInsideRing(inner, outer) {
|
|
635
|
+
const n = inner.length / 2;
|
|
636
|
+
const step = Math.max(1, Math.floor(n / 9));
|
|
637
|
+
let tested = 0;
|
|
638
|
+
let inside = 0;
|
|
639
|
+
for (let i = 0; i < n; i += step) {
|
|
640
|
+
tested++;
|
|
641
|
+
if (pointInPolygon({ x: inner[i * 2], y: inner[i * 2 + 1] }, outer, "evenodd")) {
|
|
642
|
+
inside++;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return tested > 0 && inside * 2 > tested;
|
|
646
|
+
}
|
|
647
|
+
function evenoddFillRule(paths) {
|
|
648
|
+
const len = paths.length;
|
|
649
|
+
const bboxes = paths.map(boundsOf);
|
|
650
|
+
const depth = Array.from({ length: len }).fill(0);
|
|
651
|
+
const containers = paths.map(() => []);
|
|
652
|
+
for (let i = 0; i < len; i++) {
|
|
653
|
+
const bi = bboxes[i];
|
|
654
|
+
if (!bi) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
for (let j = 0; j < len; j++) {
|
|
658
|
+
if (i === j) {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const bj = bboxes[j];
|
|
662
|
+
if (!bj || !bboxInside(bi, bj)) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (ringInsideRing(paths[i], paths[j])) {
|
|
666
|
+
depth[i]++;
|
|
667
|
+
containers[i].push(j);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return paths.map((_, i) => {
|
|
672
|
+
let parentIndex = -1;
|
|
673
|
+
let bestDepth = -1;
|
|
674
|
+
for (const j of containers[i]) {
|
|
675
|
+
if (depth[j] > bestDepth) {
|
|
676
|
+
bestDepth = depth[j];
|
|
677
|
+
parentIndex = j;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { index: i, depth: depth[i], parentIndex };
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
436
684
|
function fillTriangulate(pointArray, options = {}) {
|
|
437
685
|
let {
|
|
438
686
|
vertices = [],
|
|
@@ -595,7 +843,7 @@ function getDirectedArea(vertices) {
|
|
|
595
843
|
function cross(ax, ay, bx, by, cx, cy) {
|
|
596
844
|
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
597
845
|
}
|
|
598
|
-
function windingNumber
|
|
846
|
+
function windingNumber(px, py, polygon) {
|
|
599
847
|
const polygonLen = polygon.length;
|
|
600
848
|
let wn = 0;
|
|
601
849
|
for (let i = 0, j = polygonLen - 2; i < polygonLen; j = i, i += 2) {
|
|
@@ -716,7 +964,7 @@ function nonzeroFillRule(paths) {
|
|
|
716
964
|
const wnList = [];
|
|
717
965
|
for (let p = 0, pLen = testPoints.length; p < pLen; p++) {
|
|
718
966
|
const [x, y] = testPoints[p];
|
|
719
|
-
const winding = windingNumber
|
|
967
|
+
const winding = windingNumber(x, y, paths[j]);
|
|
720
968
|
wnMap[winding] = (wnMap[winding] ?? 0) + 1;
|
|
721
969
|
wnList.push(winding);
|
|
722
970
|
}
|
|
@@ -739,120 +987,6 @@ function nonzeroFillRule(paths) {
|
|
|
739
987
|
return results;
|
|
740
988
|
}
|
|
741
989
|
|
|
742
|
-
function isLeft(ax, ay, bx, by, px, py) {
|
|
743
|
-
return (bx - ax) * (py - ay) - (px - ax) * (by - ay);
|
|
744
|
-
}
|
|
745
|
-
function windingNumber(px, py, vertices) {
|
|
746
|
-
const len = vertices.length;
|
|
747
|
-
let wn = 0;
|
|
748
|
-
for (let i = 0; i < len; i += 2) {
|
|
749
|
-
const ax = vertices[i];
|
|
750
|
-
const ay = vertices[i + 1];
|
|
751
|
-
const k = (i + 2) % len;
|
|
752
|
-
const bx = vertices[k];
|
|
753
|
-
const by = vertices[k + 1];
|
|
754
|
-
if (ay <= py) {
|
|
755
|
-
if (by > py && isLeft(ax, ay, bx, by, px, py) > 0) {
|
|
756
|
-
wn++;
|
|
757
|
-
}
|
|
758
|
-
} else {
|
|
759
|
-
if (by <= py && isLeft(ax, ay, bx, by, px, py) < 0) {
|
|
760
|
-
wn--;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return wn;
|
|
765
|
-
}
|
|
766
|
-
function crossingNumber(px, py, vertices) {
|
|
767
|
-
const len = vertices.length;
|
|
768
|
-
let cn = 0;
|
|
769
|
-
for (let i = 0; i < len; i += 2) {
|
|
770
|
-
const ax = vertices[i];
|
|
771
|
-
const ay = vertices[i + 1];
|
|
772
|
-
const k = (i + 2) % len;
|
|
773
|
-
const bx = vertices[k];
|
|
774
|
-
const by = vertices[k + 1];
|
|
775
|
-
if (ay <= py && by > py || ay > py && by <= py) {
|
|
776
|
-
const t = (py - ay) / (by - ay);
|
|
777
|
-
if (px < ax + t * (bx - ax)) {
|
|
778
|
-
cn++;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
return cn;
|
|
783
|
-
}
|
|
784
|
-
function segmentDistance(px, py, ax, ay, bx, by) {
|
|
785
|
-
const dx = bx - ax;
|
|
786
|
-
const dy = by - ay;
|
|
787
|
-
const lenSq = dx * dx + dy * dy;
|
|
788
|
-
let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq;
|
|
789
|
-
if (t < 0) {
|
|
790
|
-
t = 0;
|
|
791
|
-
} else if (t > 1) {
|
|
792
|
-
t = 1;
|
|
793
|
-
}
|
|
794
|
-
const cx = ax + t * dx;
|
|
795
|
-
const cy = ay + t * dy;
|
|
796
|
-
return Math.hypot(px - cx, py - cy);
|
|
797
|
-
}
|
|
798
|
-
function pointInPolygon(point, vertices, fillRule = "nonzero") {
|
|
799
|
-
if (vertices.length < 6) {
|
|
800
|
-
return false;
|
|
801
|
-
}
|
|
802
|
-
if (fillRule === "evenodd") {
|
|
803
|
-
return (crossingNumber(point.x, point.y, vertices) & 1) === 1;
|
|
804
|
-
}
|
|
805
|
-
return windingNumber(point.x, point.y, vertices) !== 0;
|
|
806
|
-
}
|
|
807
|
-
function pointInPolygons(point, polygons, fillRule = "nonzero") {
|
|
808
|
-
const { x, y } = point;
|
|
809
|
-
if (fillRule === "evenodd") {
|
|
810
|
-
let cn = 0;
|
|
811
|
-
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
812
|
-
const ring = polygons[i];
|
|
813
|
-
if (ring.length >= 6) {
|
|
814
|
-
cn += crossingNumber(x, y, ring);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
return (cn & 1) === 1;
|
|
818
|
-
}
|
|
819
|
-
let wn = 0;
|
|
820
|
-
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
821
|
-
const ring = polygons[i];
|
|
822
|
-
if (ring.length >= 6) {
|
|
823
|
-
wn += windingNumber(x, y, ring);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
return wn !== 0;
|
|
827
|
-
}
|
|
828
|
-
function pointToSegmentDistance(point, a, b) {
|
|
829
|
-
return segmentDistance(point.x, point.y, a.x, a.y, b.x, b.y);
|
|
830
|
-
}
|
|
831
|
-
function pointToPolylineDistance(point, vertices, closed = false) {
|
|
832
|
-
const len = vertices.length;
|
|
833
|
-
if (len < 2) {
|
|
834
|
-
return Infinity;
|
|
835
|
-
}
|
|
836
|
-
const { x: px, y: py } = point;
|
|
837
|
-
if (len === 2) {
|
|
838
|
-
return Math.hypot(px - vertices[0], py - vertices[1]);
|
|
839
|
-
}
|
|
840
|
-
let min = Infinity;
|
|
841
|
-
for (let i = 0; i < len - 2; i += 2) {
|
|
842
|
-
const d = segmentDistance(px, py, vertices[i], vertices[i + 1], vertices[i + 2], vertices[i + 3]);
|
|
843
|
-
if (d < min) {
|
|
844
|
-
min = d;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
if (closed && len >= 6) {
|
|
848
|
-
const d = segmentDistance(px, py, vertices[len - 2], vertices[len - 1], vertices[0], vertices[1]);
|
|
849
|
-
if (d < min) {
|
|
850
|
-
min = d;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return min;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
990
|
function quadraticBezierP0(t, p) {
|
|
857
991
|
const k = 1 - t;
|
|
858
992
|
return k * k * p;
|
|
@@ -867,22 +1001,35 @@ function quadraticBezier(t, p0, p1, p2) {
|
|
|
867
1001
|
return quadraticBezierP0(t, p0) + quadraticBezierP1(t, p1) + quadraticBezierP2(t, p2);
|
|
868
1002
|
}
|
|
869
1003
|
|
|
1004
|
+
function resolveLineJoin(join) {
|
|
1005
|
+
switch (join) {
|
|
1006
|
+
case "round":
|
|
1007
|
+
case "bevel":
|
|
1008
|
+
case "miter":
|
|
1009
|
+
return join;
|
|
1010
|
+
default:
|
|
1011
|
+
return "miter";
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function resolveLineStyle(style) {
|
|
1015
|
+
return {
|
|
1016
|
+
width: style?.strokeWidth ?? 1,
|
|
1017
|
+
alignment: 0.5,
|
|
1018
|
+
join: resolveLineJoin(style?.strokeLinejoin),
|
|
1019
|
+
cap: style?.strokeLinecap ?? "butt",
|
|
1020
|
+
miterLimit: style?.strokeMiterlimit ?? 10
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
870
1023
|
const closePointEps = 1e-4;
|
|
871
1024
|
const curveEps = 1e-4;
|
|
872
1025
|
function strokeTriangulate(points, options = {}) {
|
|
873
1026
|
const {
|
|
874
1027
|
vertices = [],
|
|
875
1028
|
indices = [],
|
|
876
|
-
lineStyle = {
|
|
877
|
-
alignment: 0.5,
|
|
878
|
-
cap: "butt",
|
|
879
|
-
join: "miter",
|
|
880
|
-
width: 1,
|
|
881
|
-
miterLimit: 10
|
|
882
|
-
},
|
|
883
1029
|
flipAlignment = false,
|
|
884
1030
|
closed = true
|
|
885
1031
|
} = options;
|
|
1032
|
+
const lineStyle = options.lineStyle ?? resolveLineStyle(options.style);
|
|
886
1033
|
const eps = closePointEps;
|
|
887
1034
|
if (points.length === 0) {
|
|
888
1035
|
return { vertices, indices };
|
|
@@ -2710,6 +2857,22 @@ class Curve {
|
|
|
2710
2857
|
getControlPointRefs() {
|
|
2711
2858
|
return [];
|
|
2712
2859
|
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Reverse the traversal direction in place (start ↔ end, same geometry). The base
|
|
2862
|
+
* implementation reverses the order of the control-point *values*, which is correct for
|
|
2863
|
+
* line / Bézier / spline primitives whose {@link getControlPointRefs} order matches their
|
|
2864
|
+
* parametric order. {@link RoundCurve} (angle-based) and composites (child order) override it.
|
|
2865
|
+
*/
|
|
2866
|
+
reverse() {
|
|
2867
|
+
const refs = this.getControlPointRefs();
|
|
2868
|
+
const n = refs.length;
|
|
2869
|
+
const snapshot = refs.map((p) => p.clone());
|
|
2870
|
+
for (let i = 0; i < n; i++) {
|
|
2871
|
+
refs[i].copyFrom(snapshot[n - 1 - i]);
|
|
2872
|
+
}
|
|
2873
|
+
this.invalidate();
|
|
2874
|
+
return this;
|
|
2875
|
+
}
|
|
2713
2876
|
applyTransform(transform) {
|
|
2714
2877
|
const isFunction = typeof transform === "function";
|
|
2715
2878
|
this.getControlPointRefs().forEach((p) => {
|
|
@@ -2837,6 +3000,22 @@ class Curve {
|
|
|
2837
3000
|
getTangentAt(u, output) {
|
|
2838
3001
|
return this.getTangent(this.getUToTMapping(u), output);
|
|
2839
3002
|
}
|
|
3003
|
+
/**
|
|
3004
|
+
* PathKit-style sample at an absolute arc-length `distance` along the curve: the point, the unit
|
|
3005
|
+
* tangent, and the tangent `angle` in radians. `distance` is clamped to `[0, getLength()]`, so
|
|
3006
|
+
* passing `0`/`getLength()` always yields the endpoints. See {@link PathMeasure} for a wrapper.
|
|
3007
|
+
*/
|
|
3008
|
+
getPosTan(distance) {
|
|
3009
|
+
const length = this.getLength();
|
|
3010
|
+
const u = length > 0 ? Math.min(Math.max(distance / length, 0), 1) : 0;
|
|
3011
|
+
const t = this.getUToTMapping(u);
|
|
3012
|
+
const tangent = this.getTangent(t);
|
|
3013
|
+
return {
|
|
3014
|
+
position: this.getPoint(t),
|
|
3015
|
+
tangent,
|
|
3016
|
+
angle: Math.atan2(tangent.y, tangent.x)
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
2840
3019
|
getNormal(t, output = new Vector2()) {
|
|
2841
3020
|
this.getTangent(t, output);
|
|
2842
3021
|
return output.set(-output.y, output.x).normalize();
|
|
@@ -2932,10 +3111,28 @@ class Curve {
|
|
|
2932
3111
|
options
|
|
2933
3112
|
);
|
|
2934
3113
|
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Whether this curve forms a closed loop (its outline should be stroked without end caps,
|
|
3116
|
+
* stitching the last vertex back to the first). The base test is purely geometric — the first
|
|
3117
|
+
* sampled vertex coincides with the last. Curves that close without a duplicated endpoint
|
|
3118
|
+
* (a full-revolution {@link RoundCurve}, rectangles, polygons) override this.
|
|
3119
|
+
*/
|
|
3120
|
+
isClosed() {
|
|
3121
|
+
const v = this._getCachedAdaptiveVertices();
|
|
3122
|
+
const len = v.length;
|
|
3123
|
+
if (len < 6) {
|
|
3124
|
+
return false;
|
|
3125
|
+
}
|
|
3126
|
+
const eps = 1e-4;
|
|
3127
|
+
return Math.abs(v[0] - v[len - 2]) < eps && Math.abs(v[1] - v[len - 1]) < eps;
|
|
3128
|
+
}
|
|
2935
3129
|
strokeTriangulate(options) {
|
|
2936
3130
|
return strokeTriangulate(
|
|
2937
3131
|
this.getAdaptiveVertices(),
|
|
2938
|
-
|
|
3132
|
+
{
|
|
3133
|
+
...options,
|
|
3134
|
+
closed: options?.closed ?? this.isClosed()
|
|
3135
|
+
}
|
|
2939
3136
|
);
|
|
2940
3137
|
}
|
|
2941
3138
|
toCommands() {
|
|
@@ -3030,6 +3227,22 @@ class RoundCurve extends Curve {
|
|
|
3030
3227
|
isClockwise() {
|
|
3031
3228
|
return this.clockwise;
|
|
3032
3229
|
}
|
|
3230
|
+
/**
|
|
3231
|
+
* A circle/ellipse arc is closed when it sweeps (at least) a full revolution — the sampled
|
|
3232
|
+
* outline does not duplicate the start vertex, so the geometric first==last test in the base
|
|
3233
|
+
* class would wrongly report a full circle as open and leave a seam gap in the stroke.
|
|
3234
|
+
*/
|
|
3235
|
+
isClosed() {
|
|
3236
|
+
return Math.abs(this.endAngle - this.startAngle) >= Math.PI * 2 - 1e-9 || super.isClosed();
|
|
3237
|
+
}
|
|
3238
|
+
reverse() {
|
|
3239
|
+
const { startAngle, endAngle } = this;
|
|
3240
|
+
this.startAngle = endAngle;
|
|
3241
|
+
this.endAngle = startAngle;
|
|
3242
|
+
this.clockwise = !this.clockwise;
|
|
3243
|
+
this.invalidate();
|
|
3244
|
+
return this;
|
|
3245
|
+
}
|
|
3033
3246
|
_getDeltaAngle() {
|
|
3034
3247
|
const PI_2 = Math.PI * 2;
|
|
3035
3248
|
let deltaAngle = this.endAngle - this.startAngle;
|
|
@@ -3286,9 +3499,25 @@ class RoundCurve extends Curve {
|
|
|
3286
3499
|
return output;
|
|
3287
3500
|
}
|
|
3288
3501
|
getAdaptiveVertices(output = []) {
|
|
3289
|
-
|
|
3502
|
+
const PI2 = Math.PI * 2;
|
|
3503
|
+
if (this.startAngle === 0 && this.endAngle === PI2) {
|
|
3290
3504
|
return this._getAdaptiveVerticesByCircle(output);
|
|
3291
3505
|
}
|
|
3506
|
+
if (Math.abs(this.endAngle - this.startAngle) >= PI2 - 1e-9) {
|
|
3507
|
+
const tmp = this._getAdaptiveVerticesByCircle([]);
|
|
3508
|
+
const n = tmp.length / 2;
|
|
3509
|
+
if (this.endAngle > this.startAngle) {
|
|
3510
|
+
for (let i = 0; i < tmp.length; i++) {
|
|
3511
|
+
output.push(tmp[i]);
|
|
3512
|
+
}
|
|
3513
|
+
} else {
|
|
3514
|
+
output.push(tmp[0], tmp[1]);
|
|
3515
|
+
for (let i = n - 1; i >= 1; i--) {
|
|
3516
|
+
output.push(tmp[i * 2], tmp[i * 2 + 1]);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
return output;
|
|
3520
|
+
}
|
|
3292
3521
|
return this._getAdaptiveVerticesByArc(output);
|
|
3293
3522
|
}
|
|
3294
3523
|
copyFrom(source) {
|
|
@@ -3498,6 +3727,15 @@ class LineCurve extends Curve {
|
|
|
3498
3727
|
getControlPointRefs() {
|
|
3499
3728
|
return [this.p1, this.p2];
|
|
3500
3729
|
}
|
|
3730
|
+
// Swap endpoint *references* (not values) so corner Vector2s shared with adjacent
|
|
3731
|
+
// segments stay intact and simply re-associate with the reversed segment.
|
|
3732
|
+
reverse() {
|
|
3733
|
+
const { p1, p2 } = this;
|
|
3734
|
+
this.p1 = p2;
|
|
3735
|
+
this.p2 = p1;
|
|
3736
|
+
this.invalidate();
|
|
3737
|
+
return this;
|
|
3738
|
+
}
|
|
3501
3739
|
getAdaptiveVertices(output = []) {
|
|
3502
3740
|
output.push(
|
|
3503
3741
|
this.p1.x,
|
|
@@ -3661,6 +3899,16 @@ class CompositeCurve extends Curve {
|
|
|
3661
3899
|
});
|
|
3662
3900
|
return output;
|
|
3663
3901
|
}
|
|
3902
|
+
/**
|
|
3903
|
+
* A composite is closed when its single child is closed (e.g. a lone full-circle arc), or when
|
|
3904
|
+
* its assembled outline returns to its start (rectangles, polygons, multi-segment loops).
|
|
3905
|
+
*/
|
|
3906
|
+
isClosed() {
|
|
3907
|
+
if (this.curves.length === 1) {
|
|
3908
|
+
return this.curves[0].isClosed();
|
|
3909
|
+
}
|
|
3910
|
+
return super.isClosed();
|
|
3911
|
+
}
|
|
3664
3912
|
strokeTriangulate(options) {
|
|
3665
3913
|
if (this.curves.length === 1) {
|
|
3666
3914
|
return this.curves[0].strokeTriangulate(options);
|
|
@@ -3668,6 +3916,13 @@ class CompositeCurve extends Curve {
|
|
|
3668
3916
|
return super.strokeTriangulate(options);
|
|
3669
3917
|
}
|
|
3670
3918
|
}
|
|
3919
|
+
/** Reverse the sub-curve order and reverse each sub-curve, so the whole outline runs backwards. */
|
|
3920
|
+
reverse() {
|
|
3921
|
+
this.curves.reverse();
|
|
3922
|
+
this.curves.forEach((curve) => curve.reverse());
|
|
3923
|
+
this.invalidate();
|
|
3924
|
+
return this;
|
|
3925
|
+
}
|
|
3671
3926
|
getFillVertices(options) {
|
|
3672
3927
|
if (this.curves.length === 1) {
|
|
3673
3928
|
return this.curves[0].getFillVertices(options);
|
|
@@ -3763,6 +4018,16 @@ class CubicBezierCurve extends Curve {
|
|
|
3763
4018
|
getControlPointRefs() {
|
|
3764
4019
|
return [this.p1, this.cp1, this.cp2, this.p2];
|
|
3765
4020
|
}
|
|
4021
|
+
// Swap endpoint and control-point references; keeps shared corner Vector2s intact.
|
|
4022
|
+
reverse() {
|
|
4023
|
+
const { p1, cp1, cp2, p2 } = this;
|
|
4024
|
+
this.p1 = p2;
|
|
4025
|
+
this.cp1 = cp2;
|
|
4026
|
+
this.cp2 = cp1;
|
|
4027
|
+
this.p2 = p1;
|
|
4028
|
+
this.invalidate();
|
|
4029
|
+
return this;
|
|
4030
|
+
}
|
|
3766
4031
|
_solveQuadratic(a, b, c) {
|
|
3767
4032
|
if (Math.abs(a) < 1e-12) {
|
|
3768
4033
|
if (Math.abs(b) < 1e-12)
|
|
@@ -3923,6 +4188,14 @@ class QuadraticBezierCurve extends Curve {
|
|
|
3923
4188
|
getControlPointRefs() {
|
|
3924
4189
|
return [this.p1, this.cp, this.p2];
|
|
3925
4190
|
}
|
|
4191
|
+
// Swap endpoint references (cp is symmetric); keeps shared corner Vector2s intact.
|
|
4192
|
+
reverse() {
|
|
4193
|
+
const { p1, p2 } = this;
|
|
4194
|
+
this.p1 = p2;
|
|
4195
|
+
this.p2 = p1;
|
|
4196
|
+
this.invalidate();
|
|
4197
|
+
return this;
|
|
4198
|
+
}
|
|
3926
4199
|
getAdaptiveVertices(output = []) {
|
|
3927
4200
|
return getAdaptiveQuadraticBezierCurvePoints(
|
|
3928
4201
|
this.p1.x,
|
|
@@ -4124,6 +4397,11 @@ class SplineCurve extends Curve {
|
|
|
4124
4397
|
getControlPointRefs() {
|
|
4125
4398
|
return this.points;
|
|
4126
4399
|
}
|
|
4400
|
+
reverse() {
|
|
4401
|
+
this.points.reverse();
|
|
4402
|
+
this.invalidate();
|
|
4403
|
+
return this;
|
|
4404
|
+
}
|
|
4127
4405
|
copyFrom(source) {
|
|
4128
4406
|
super.copyFrom(source);
|
|
4129
4407
|
this.points = [];
|
|
@@ -4160,6 +4438,22 @@ class CurvePath extends CompositeCurve {
|
|
|
4160
4438
|
this.addCommands(svgPathDataToCommands(data));
|
|
4161
4439
|
return this;
|
|
4162
4440
|
}
|
|
4441
|
+
/**
|
|
4442
|
+
* A sub-path is closed if it was explicitly closed (`autoClose`, i.e. a `Z`/`closePath`), or if
|
|
4443
|
+
* it forms a geometric loop / wraps a single closed primitive (handled by the base class).
|
|
4444
|
+
*/
|
|
4445
|
+
isClosed() {
|
|
4446
|
+
return this.autoClose || super.isClosed();
|
|
4447
|
+
}
|
|
4448
|
+
/** Reverse direction, then refresh the {@link startPoint}/{@link currentPoint} cursors. */
|
|
4449
|
+
reverse() {
|
|
4450
|
+
super.reverse();
|
|
4451
|
+
if (this.curves.length) {
|
|
4452
|
+
this.startPoint = this.getPoint(0);
|
|
4453
|
+
this.currentPoint = this.getPoint(1);
|
|
4454
|
+
}
|
|
4455
|
+
return this;
|
|
4456
|
+
}
|
|
4163
4457
|
_closeVertices(output) {
|
|
4164
4458
|
if (this.autoClose && output.length >= 4 && (output[0] !== output[output.length - 2] && output[1] !== output[output.length - 1])) {
|
|
4165
4459
|
output.push(output[0], output[1]);
|
|
@@ -4596,6 +4890,51 @@ class Path2D extends CompositeCurve {
|
|
|
4596
4890
|
const fillRule = options.fillRule ?? this.style.fillRule ?? "nonzero";
|
|
4597
4891
|
return pointInPolygons(point, this._getRings(), fillRule);
|
|
4598
4892
|
}
|
|
4893
|
+
/** Build a `Path2D` from flat rings (`[x0,y0,…]` per sub-path); closed-and-filled as sub-paths. */
|
|
4894
|
+
static fromRings(rings, style = {}) {
|
|
4895
|
+
const path = new Path2D(void 0, style);
|
|
4896
|
+
for (const ring of rings) {
|
|
4897
|
+
if (ring.length < 6) {
|
|
4898
|
+
continue;
|
|
4899
|
+
}
|
|
4900
|
+
let end = ring.length;
|
|
4901
|
+
if (ring[0] === ring[end - 2] && ring[1] === ring[end - 1]) {
|
|
4902
|
+
end -= 2;
|
|
4903
|
+
}
|
|
4904
|
+
path.moveTo(ring[0], ring[1]);
|
|
4905
|
+
for (let i = 2; i < end; i += 2) {
|
|
4906
|
+
path.lineTo(ring[i], ring[i + 1]);
|
|
4907
|
+
}
|
|
4908
|
+
path.closePath();
|
|
4909
|
+
}
|
|
4910
|
+
return path;
|
|
4911
|
+
}
|
|
4912
|
+
/**
|
|
4913
|
+
* Boolean (path) operation against another path, returning a NEW `Path2D` whose outline is the
|
|
4914
|
+
* polygonal result. Curves are sampled before clipping, so the result is a polygonal
|
|
4915
|
+
* approximation (see {@link polygonBoolean}). The result inherits this path's `style` unless
|
|
4916
|
+
* overridden via `style`. Holes are emitted as oppositely-wound sub-paths (nonzero fill).
|
|
4917
|
+
*/
|
|
4918
|
+
booleanOp(op, other, style) {
|
|
4919
|
+
const rings = polygonBoolean(op, this._getRings(), other._getRings());
|
|
4920
|
+
return Path2D.fromRings(rings, { ...this.style, ...style });
|
|
4921
|
+
}
|
|
4922
|
+
/** `this ∪ other` — the combined filled area. */
|
|
4923
|
+
union(other, style) {
|
|
4924
|
+
return this.booleanOp("union", other, style);
|
|
4925
|
+
}
|
|
4926
|
+
/** `this ∩ other` — only the overlapping area. */
|
|
4927
|
+
intersection(other, style) {
|
|
4928
|
+
return this.booleanOp("intersection", other, style);
|
|
4929
|
+
}
|
|
4930
|
+
/** `this − other` — this path with `other` cut away. */
|
|
4931
|
+
difference(other, style) {
|
|
4932
|
+
return this.booleanOp("difference", other, style);
|
|
4933
|
+
}
|
|
4934
|
+
/** `this ⊕ other` — areas covered by exactly one of the two paths. */
|
|
4935
|
+
xor(other, style) {
|
|
4936
|
+
return this.booleanOp("xor", other, style);
|
|
4937
|
+
}
|
|
4599
4938
|
/**
|
|
4600
4939
|
* Test whether a point lies on this path's stroke. A hit on any sub-path counts.
|
|
4601
4940
|
*
|
|
@@ -4678,14 +5017,30 @@ class Path2D extends CompositeCurve {
|
|
|
4678
5017
|
});
|
|
4679
5018
|
}
|
|
4680
5019
|
} else {
|
|
4681
|
-
this.curves.
|
|
4682
|
-
|
|
5020
|
+
const paths = this.curves.map((curve) => curve.getFillVertices(_options));
|
|
5021
|
+
const groups = evenoddFillRule(paths);
|
|
5022
|
+
const groupsLen = groups.length;
|
|
5023
|
+
for (let i = 0; i < groupsLen; i++) {
|
|
5024
|
+
const pointArray = paths[i];
|
|
5025
|
+
if ((groups[i].depth & 1) === 1 || !pointArray.length) {
|
|
5026
|
+
continue;
|
|
5027
|
+
}
|
|
5028
|
+
const _pointArray = pointArray.slice();
|
|
5029
|
+
const holes = [];
|
|
5030
|
+
for (let j = 0; j < groupsLen; j++) {
|
|
5031
|
+
if (groups[j].parentIndex === i && (groups[j].depth & 1) === 1) {
|
|
5032
|
+
holes.push(_pointArray.length / 2);
|
|
5033
|
+
_pointArray.push(...paths[j]);
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
fillTriangulate(_pointArray, {
|
|
4683
5037
|
...options,
|
|
4684
5038
|
indices,
|
|
4685
5039
|
vertices,
|
|
5040
|
+
holes,
|
|
4686
5041
|
style: { ...this.style }
|
|
4687
5042
|
});
|
|
4688
|
-
}
|
|
5043
|
+
}
|
|
4689
5044
|
}
|
|
4690
5045
|
return { indices, vertices };
|
|
4691
5046
|
}
|
|
@@ -4695,7 +5050,7 @@ class Path2D extends CompositeCurve {
|
|
|
4695
5050
|
}
|
|
4696
5051
|
drawTo(ctx, style = {}) {
|
|
4697
5052
|
style = { ...this.style, ...style };
|
|
4698
|
-
const { fill = "#000", stroke = "none" } = style;
|
|
5053
|
+
const { fill = "#000", stroke = "none", fillRule = "nonzero" } = style;
|
|
4699
5054
|
ctx.beginPath();
|
|
4700
5055
|
ctx.save();
|
|
4701
5056
|
setCanvasContext(ctx, style);
|
|
@@ -4703,7 +5058,7 @@ class Path2D extends CompositeCurve {
|
|
|
4703
5058
|
path.drawTo(ctx);
|
|
4704
5059
|
});
|
|
4705
5060
|
if (fill !== "none") {
|
|
4706
|
-
ctx.fill();
|
|
5061
|
+
ctx.fill(fillRule);
|
|
4707
5062
|
}
|
|
4708
5063
|
if (stroke !== "none") {
|
|
4709
5064
|
ctx.stroke();
|
|
@@ -4909,6 +5264,44 @@ ${content}
|
|
|
4909
5264
|
}
|
|
4910
5265
|
}
|
|
4911
5266
|
|
|
5267
|
+
class PathMeasure {
|
|
5268
|
+
constructor(curve) {
|
|
5269
|
+
this.curve = curve;
|
|
5270
|
+
}
|
|
5271
|
+
/** Total arc length of the path. */
|
|
5272
|
+
getLength() {
|
|
5273
|
+
return this.curve.getLength();
|
|
5274
|
+
}
|
|
5275
|
+
/** Whether the path forms a closed loop (see {@link Curve.isClosed}). */
|
|
5276
|
+
isClosed() {
|
|
5277
|
+
return this.curve.isClosed();
|
|
5278
|
+
}
|
|
5279
|
+
/** Point + unit tangent + tangent angle at an absolute arc-length `distance` (clamped). */
|
|
5280
|
+
getPosTan(distance) {
|
|
5281
|
+
return this.curve.getPosTan(distance);
|
|
5282
|
+
}
|
|
5283
|
+
/** Point at an absolute arc-length `distance` (clamped to `[0, getLength()]`). */
|
|
5284
|
+
getPosition(distance) {
|
|
5285
|
+
return this.curve.getPosTan(distance).position;
|
|
5286
|
+
}
|
|
5287
|
+
/** Point + tangent at a normalized progress `t ∈ [0, 1]` along the path. */
|
|
5288
|
+
getPosTanAtProgress(t) {
|
|
5289
|
+
return this.curve.getPosTan(this.getLength() * t);
|
|
5290
|
+
}
|
|
5291
|
+
/**
|
|
5292
|
+
* Evenly sample the path into `count + 1` {@link PosTan} entries (arc-length spaced), e.g. to
|
|
5293
|
+
* lay glyphs along a path or drive an `animate(progress)`-style traversal.
|
|
5294
|
+
*/
|
|
5295
|
+
sample(count = 100) {
|
|
5296
|
+
const length = this.getLength();
|
|
5297
|
+
const out = [];
|
|
5298
|
+
for (let i = 0; i <= count; i++) {
|
|
5299
|
+
out.push(this.curve.getPosTan(length * i / count));
|
|
5300
|
+
}
|
|
5301
|
+
return out;
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
|
|
4912
5305
|
class FFDControlGrid {
|
|
4913
5306
|
constructor(rows, cols, width = 1, height = 1) {
|
|
4914
5307
|
this.rows = rows;
|
|
@@ -4978,6 +5371,7 @@ exports.PI = PI;
|
|
|
4978
5371
|
exports.PI_2 = PI_2;
|
|
4979
5372
|
exports.Path2D = Path2D;
|
|
4980
5373
|
exports.Path2DSet = Path2DSet;
|
|
5374
|
+
exports.PathMeasure = PathMeasure;
|
|
4981
5375
|
exports.PolygonCurve = PolygonCurve;
|
|
4982
5376
|
exports.QuadraticBezierCurve = QuadraticBezierCurve;
|
|
4983
5377
|
exports.RectangleCurve = RectangleCurve;
|
|
@@ -4989,6 +5383,7 @@ exports.applyFFD = applyFFD;
|
|
|
4989
5383
|
exports.catmullRom = catmullRom;
|
|
4990
5384
|
exports.cubicBezier = cubicBezier;
|
|
4991
5385
|
exports.drawPoint = drawPoint;
|
|
5386
|
+
exports.evenoddFillRule = evenoddFillRule;
|
|
4992
5387
|
exports.fillTriangulate = fillTriangulate;
|
|
4993
5388
|
exports.getAdaptiveCubicBezierCurvePoints = getAdaptiveCubicBezierCurvePoints;
|
|
4994
5389
|
exports.getAdaptiveQuadraticBezierCurvePoints = getAdaptiveQuadraticBezierCurvePoints;
|
|
@@ -5004,7 +5399,9 @@ exports.pointInPolygon = pointInPolygon;
|
|
|
5004
5399
|
exports.pointInPolygons = pointInPolygons;
|
|
5005
5400
|
exports.pointToPolylineDistance = pointToPolylineDistance;
|
|
5006
5401
|
exports.pointToSegmentDistance = pointToSegmentDistance;
|
|
5402
|
+
exports.polygonBoolean = polygonBoolean;
|
|
5007
5403
|
exports.quadraticBezier = quadraticBezier;
|
|
5404
|
+
exports.resolveLineStyle = resolveLineStyle;
|
|
5008
5405
|
exports.setCanvasContext = setCanvasContext;
|
|
5009
5406
|
exports.strokeTriangulate = strokeTriangulate;
|
|
5010
5407
|
exports.svgPathCommandsAddToPath2D = svgPathCommandsAddToPath2D;
|