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.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import earcut from 'earcut';
|
|
2
|
+
import polygonClipping from 'polygon-clipping';
|
|
2
3
|
|
|
3
4
|
function drawPoint(ctx, x, y, options = {}) {
|
|
4
5
|
const { radius = 1 } = options;
|
|
@@ -297,6 +298,63 @@ class BoundingBox {
|
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
function flatRingToPairs(r) {
|
|
302
|
+
const ring = [];
|
|
303
|
+
for (let i = 0; i < r.length; i += 2) {
|
|
304
|
+
ring.push([r[i], r[i + 1]]);
|
|
305
|
+
}
|
|
306
|
+
const first = ring[0];
|
|
307
|
+
const last = ring[ring.length - 1];
|
|
308
|
+
if (first && last && (first[0] !== last[0] || first[1] !== last[1])) {
|
|
309
|
+
ring.push([first[0], first[1]]);
|
|
310
|
+
}
|
|
311
|
+
return ring;
|
|
312
|
+
}
|
|
313
|
+
function ringsToGeom(rings) {
|
|
314
|
+
const valid = rings.filter((r) => r.length >= 6).map(flatRingToPairs);
|
|
315
|
+
if (!valid.length) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
let geom = [[valid[0]]];
|
|
319
|
+
for (let i = 1; i < valid.length; i++) {
|
|
320
|
+
geom = polygonClipping.xor(geom, [[valid[i]]]);
|
|
321
|
+
}
|
|
322
|
+
return geom;
|
|
323
|
+
}
|
|
324
|
+
function geomToRings(geom) {
|
|
325
|
+
const out = [];
|
|
326
|
+
for (const poly of geom) {
|
|
327
|
+
for (const ring of poly) {
|
|
328
|
+
const flat = [];
|
|
329
|
+
for (const [x, y] of ring) {
|
|
330
|
+
flat.push(x, y);
|
|
331
|
+
}
|
|
332
|
+
out.push(flat);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
function polygonBoolean(op, ringsA, ringsB) {
|
|
338
|
+
const a = ringsToGeom(ringsA);
|
|
339
|
+
const b = ringsToGeom(ringsB);
|
|
340
|
+
let res;
|
|
341
|
+
switch (op) {
|
|
342
|
+
case "union":
|
|
343
|
+
res = polygonClipping.union(a, b);
|
|
344
|
+
break;
|
|
345
|
+
case "intersection":
|
|
346
|
+
res = polygonClipping.intersection(a, b);
|
|
347
|
+
break;
|
|
348
|
+
case "difference":
|
|
349
|
+
res = polygonClipping.difference(a, b);
|
|
350
|
+
break;
|
|
351
|
+
case "xor":
|
|
352
|
+
res = polygonClipping.xor(a, b);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
return geomToRings(res);
|
|
356
|
+
}
|
|
357
|
+
|
|
300
358
|
function catmullRom(t, p0, p1, p2, p3) {
|
|
301
359
|
const v0 = (p2 - p0) * 0.5;
|
|
302
360
|
const v1 = (p3 - p1) * 0.5;
|
|
@@ -427,6 +485,195 @@ function cubicBezier(t, p0, p1, p2, p3) {
|
|
|
427
485
|
return cubicBezierP0(t, p0) + cubicBezierP1(t, p1) + cubicBezierP2(t, p2) + cubicBezierP3(t, p3);
|
|
428
486
|
}
|
|
429
487
|
|
|
488
|
+
function isLeft(ax, ay, bx, by, px, py) {
|
|
489
|
+
return (bx - ax) * (py - ay) - (px - ax) * (by - ay);
|
|
490
|
+
}
|
|
491
|
+
function windingNumber$1(px, py, vertices) {
|
|
492
|
+
const len = vertices.length;
|
|
493
|
+
let wn = 0;
|
|
494
|
+
for (let i = 0; i < len; i += 2) {
|
|
495
|
+
const ax = vertices[i];
|
|
496
|
+
const ay = vertices[i + 1];
|
|
497
|
+
const k = (i + 2) % len;
|
|
498
|
+
const bx = vertices[k];
|
|
499
|
+
const by = vertices[k + 1];
|
|
500
|
+
if (ay <= py) {
|
|
501
|
+
if (by > py && isLeft(ax, ay, bx, by, px, py) > 0) {
|
|
502
|
+
wn++;
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
if (by <= py && isLeft(ax, ay, bx, by, px, py) < 0) {
|
|
506
|
+
wn--;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return wn;
|
|
511
|
+
}
|
|
512
|
+
function crossingNumber(px, py, vertices) {
|
|
513
|
+
const len = vertices.length;
|
|
514
|
+
let cn = 0;
|
|
515
|
+
for (let i = 0; i < len; i += 2) {
|
|
516
|
+
const ax = vertices[i];
|
|
517
|
+
const ay = vertices[i + 1];
|
|
518
|
+
const k = (i + 2) % len;
|
|
519
|
+
const bx = vertices[k];
|
|
520
|
+
const by = vertices[k + 1];
|
|
521
|
+
if (ay <= py && by > py || ay > py && by <= py) {
|
|
522
|
+
const t = (py - ay) / (by - ay);
|
|
523
|
+
if (px < ax + t * (bx - ax)) {
|
|
524
|
+
cn++;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return cn;
|
|
529
|
+
}
|
|
530
|
+
function segmentDistance(px, py, ax, ay, bx, by) {
|
|
531
|
+
const dx = bx - ax;
|
|
532
|
+
const dy = by - ay;
|
|
533
|
+
const lenSq = dx * dx + dy * dy;
|
|
534
|
+
let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq;
|
|
535
|
+
if (t < 0) {
|
|
536
|
+
t = 0;
|
|
537
|
+
} else if (t > 1) {
|
|
538
|
+
t = 1;
|
|
539
|
+
}
|
|
540
|
+
const cx = ax + t * dx;
|
|
541
|
+
const cy = ay + t * dy;
|
|
542
|
+
return Math.hypot(px - cx, py - cy);
|
|
543
|
+
}
|
|
544
|
+
function pointInPolygon(point, vertices, fillRule = "nonzero") {
|
|
545
|
+
if (vertices.length < 6) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
if (fillRule === "evenodd") {
|
|
549
|
+
return (crossingNumber(point.x, point.y, vertices) & 1) === 1;
|
|
550
|
+
}
|
|
551
|
+
return windingNumber$1(point.x, point.y, vertices) !== 0;
|
|
552
|
+
}
|
|
553
|
+
function pointInPolygons(point, polygons, fillRule = "nonzero") {
|
|
554
|
+
const { x, y } = point;
|
|
555
|
+
if (fillRule === "evenodd") {
|
|
556
|
+
let cn = 0;
|
|
557
|
+
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
558
|
+
const ring = polygons[i];
|
|
559
|
+
if (ring.length >= 6) {
|
|
560
|
+
cn += crossingNumber(x, y, ring);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return (cn & 1) === 1;
|
|
564
|
+
}
|
|
565
|
+
let wn = 0;
|
|
566
|
+
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
567
|
+
const ring = polygons[i];
|
|
568
|
+
if (ring.length >= 6) {
|
|
569
|
+
wn += windingNumber$1(x, y, ring);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return wn !== 0;
|
|
573
|
+
}
|
|
574
|
+
function pointToSegmentDistance(point, a, b) {
|
|
575
|
+
return segmentDistance(point.x, point.y, a.x, a.y, b.x, b.y);
|
|
576
|
+
}
|
|
577
|
+
function pointToPolylineDistance(point, vertices, closed = false) {
|
|
578
|
+
const len = vertices.length;
|
|
579
|
+
if (len < 2) {
|
|
580
|
+
return Infinity;
|
|
581
|
+
}
|
|
582
|
+
const { x: px, y: py } = point;
|
|
583
|
+
if (len === 2) {
|
|
584
|
+
return Math.hypot(px - vertices[0], py - vertices[1]);
|
|
585
|
+
}
|
|
586
|
+
let min = Infinity;
|
|
587
|
+
for (let i = 0; i < len - 2; i += 2) {
|
|
588
|
+
const d = segmentDistance(px, py, vertices[i], vertices[i + 1], vertices[i + 2], vertices[i + 3]);
|
|
589
|
+
if (d < min) {
|
|
590
|
+
min = d;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (closed && len >= 6) {
|
|
594
|
+
const d = segmentDistance(px, py, vertices[len - 2], vertices[len - 1], vertices[0], vertices[1]);
|
|
595
|
+
if (d < min) {
|
|
596
|
+
min = d;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return min;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function boundsOf(ring) {
|
|
603
|
+
if (ring.length < 6) {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
let minX = Infinity;
|
|
607
|
+
let minY = Infinity;
|
|
608
|
+
let maxX = -Infinity;
|
|
609
|
+
let maxY = -Infinity;
|
|
610
|
+
for (let i = 0; i < ring.length; i += 2) {
|
|
611
|
+
const x = ring[i];
|
|
612
|
+
const y = ring[i + 1];
|
|
613
|
+
if (x < minX)
|
|
614
|
+
minX = x;
|
|
615
|
+
if (y < minY)
|
|
616
|
+
minY = y;
|
|
617
|
+
if (x > maxX)
|
|
618
|
+
maxX = x;
|
|
619
|
+
if (y > maxY)
|
|
620
|
+
maxY = y;
|
|
621
|
+
}
|
|
622
|
+
return { minX, minY, maxX, maxY };
|
|
623
|
+
}
|
|
624
|
+
function bboxInside(inner, outer) {
|
|
625
|
+
return inner.minX >= outer.minX && inner.maxX <= outer.maxX && inner.minY >= outer.minY && inner.maxY <= outer.maxY;
|
|
626
|
+
}
|
|
627
|
+
function ringInsideRing(inner, outer) {
|
|
628
|
+
const n = inner.length / 2;
|
|
629
|
+
const step = Math.max(1, Math.floor(n / 9));
|
|
630
|
+
let tested = 0;
|
|
631
|
+
let inside = 0;
|
|
632
|
+
for (let i = 0; i < n; i += step) {
|
|
633
|
+
tested++;
|
|
634
|
+
if (pointInPolygon({ x: inner[i * 2], y: inner[i * 2 + 1] }, outer, "evenodd")) {
|
|
635
|
+
inside++;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return tested > 0 && inside * 2 > tested;
|
|
639
|
+
}
|
|
640
|
+
function evenoddFillRule(paths) {
|
|
641
|
+
const len = paths.length;
|
|
642
|
+
const bboxes = paths.map(boundsOf);
|
|
643
|
+
const depth = Array.from({ length: len }).fill(0);
|
|
644
|
+
const containers = paths.map(() => []);
|
|
645
|
+
for (let i = 0; i < len; i++) {
|
|
646
|
+
const bi = bboxes[i];
|
|
647
|
+
if (!bi) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
for (let j = 0; j < len; j++) {
|
|
651
|
+
if (i === j) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const bj = bboxes[j];
|
|
655
|
+
if (!bj || !bboxInside(bi, bj)) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (ringInsideRing(paths[i], paths[j])) {
|
|
659
|
+
depth[i]++;
|
|
660
|
+
containers[i].push(j);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return paths.map((_, i) => {
|
|
665
|
+
let parentIndex = -1;
|
|
666
|
+
let bestDepth = -1;
|
|
667
|
+
for (const j of containers[i]) {
|
|
668
|
+
if (depth[j] > bestDepth) {
|
|
669
|
+
bestDepth = depth[j];
|
|
670
|
+
parentIndex = j;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return { index: i, depth: depth[i], parentIndex };
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
430
677
|
function fillTriangulate(pointArray, options = {}) {
|
|
431
678
|
let {
|
|
432
679
|
vertices = [],
|
|
@@ -589,7 +836,7 @@ function getDirectedArea(vertices) {
|
|
|
589
836
|
function cross(ax, ay, bx, by, cx, cy) {
|
|
590
837
|
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
591
838
|
}
|
|
592
|
-
function windingNumber
|
|
839
|
+
function windingNumber(px, py, polygon) {
|
|
593
840
|
const polygonLen = polygon.length;
|
|
594
841
|
let wn = 0;
|
|
595
842
|
for (let i = 0, j = polygonLen - 2; i < polygonLen; j = i, i += 2) {
|
|
@@ -710,7 +957,7 @@ function nonzeroFillRule(paths) {
|
|
|
710
957
|
const wnList = [];
|
|
711
958
|
for (let p = 0, pLen = testPoints.length; p < pLen; p++) {
|
|
712
959
|
const [x, y] = testPoints[p];
|
|
713
|
-
const winding = windingNumber
|
|
960
|
+
const winding = windingNumber(x, y, paths[j]);
|
|
714
961
|
wnMap[winding] = (wnMap[winding] ?? 0) + 1;
|
|
715
962
|
wnList.push(winding);
|
|
716
963
|
}
|
|
@@ -733,120 +980,6 @@ function nonzeroFillRule(paths) {
|
|
|
733
980
|
return results;
|
|
734
981
|
}
|
|
735
982
|
|
|
736
|
-
function isLeft(ax, ay, bx, by, px, py) {
|
|
737
|
-
return (bx - ax) * (py - ay) - (px - ax) * (by - ay);
|
|
738
|
-
}
|
|
739
|
-
function windingNumber(px, py, vertices) {
|
|
740
|
-
const len = vertices.length;
|
|
741
|
-
let wn = 0;
|
|
742
|
-
for (let i = 0; i < len; i += 2) {
|
|
743
|
-
const ax = vertices[i];
|
|
744
|
-
const ay = vertices[i + 1];
|
|
745
|
-
const k = (i + 2) % len;
|
|
746
|
-
const bx = vertices[k];
|
|
747
|
-
const by = vertices[k + 1];
|
|
748
|
-
if (ay <= py) {
|
|
749
|
-
if (by > py && isLeft(ax, ay, bx, by, px, py) > 0) {
|
|
750
|
-
wn++;
|
|
751
|
-
}
|
|
752
|
-
} else {
|
|
753
|
-
if (by <= py && isLeft(ax, ay, bx, by, px, py) < 0) {
|
|
754
|
-
wn--;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
return wn;
|
|
759
|
-
}
|
|
760
|
-
function crossingNumber(px, py, vertices) {
|
|
761
|
-
const len = vertices.length;
|
|
762
|
-
let cn = 0;
|
|
763
|
-
for (let i = 0; i < len; i += 2) {
|
|
764
|
-
const ax = vertices[i];
|
|
765
|
-
const ay = vertices[i + 1];
|
|
766
|
-
const k = (i + 2) % len;
|
|
767
|
-
const bx = vertices[k];
|
|
768
|
-
const by = vertices[k + 1];
|
|
769
|
-
if (ay <= py && by > py || ay > py && by <= py) {
|
|
770
|
-
const t = (py - ay) / (by - ay);
|
|
771
|
-
if (px < ax + t * (bx - ax)) {
|
|
772
|
-
cn++;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
return cn;
|
|
777
|
-
}
|
|
778
|
-
function segmentDistance(px, py, ax, ay, bx, by) {
|
|
779
|
-
const dx = bx - ax;
|
|
780
|
-
const dy = by - ay;
|
|
781
|
-
const lenSq = dx * dx + dy * dy;
|
|
782
|
-
let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq;
|
|
783
|
-
if (t < 0) {
|
|
784
|
-
t = 0;
|
|
785
|
-
} else if (t > 1) {
|
|
786
|
-
t = 1;
|
|
787
|
-
}
|
|
788
|
-
const cx = ax + t * dx;
|
|
789
|
-
const cy = ay + t * dy;
|
|
790
|
-
return Math.hypot(px - cx, py - cy);
|
|
791
|
-
}
|
|
792
|
-
function pointInPolygon(point, vertices, fillRule = "nonzero") {
|
|
793
|
-
if (vertices.length < 6) {
|
|
794
|
-
return false;
|
|
795
|
-
}
|
|
796
|
-
if (fillRule === "evenodd") {
|
|
797
|
-
return (crossingNumber(point.x, point.y, vertices) & 1) === 1;
|
|
798
|
-
}
|
|
799
|
-
return windingNumber(point.x, point.y, vertices) !== 0;
|
|
800
|
-
}
|
|
801
|
-
function pointInPolygons(point, polygons, fillRule = "nonzero") {
|
|
802
|
-
const { x, y } = point;
|
|
803
|
-
if (fillRule === "evenodd") {
|
|
804
|
-
let cn = 0;
|
|
805
|
-
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
806
|
-
const ring = polygons[i];
|
|
807
|
-
if (ring.length >= 6) {
|
|
808
|
-
cn += crossingNumber(x, y, ring);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
return (cn & 1) === 1;
|
|
812
|
-
}
|
|
813
|
-
let wn = 0;
|
|
814
|
-
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
815
|
-
const ring = polygons[i];
|
|
816
|
-
if (ring.length >= 6) {
|
|
817
|
-
wn += windingNumber(x, y, ring);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
return wn !== 0;
|
|
821
|
-
}
|
|
822
|
-
function pointToSegmentDistance(point, a, b) {
|
|
823
|
-
return segmentDistance(point.x, point.y, a.x, a.y, b.x, b.y);
|
|
824
|
-
}
|
|
825
|
-
function pointToPolylineDistance(point, vertices, closed = false) {
|
|
826
|
-
const len = vertices.length;
|
|
827
|
-
if (len < 2) {
|
|
828
|
-
return Infinity;
|
|
829
|
-
}
|
|
830
|
-
const { x: px, y: py } = point;
|
|
831
|
-
if (len === 2) {
|
|
832
|
-
return Math.hypot(px - vertices[0], py - vertices[1]);
|
|
833
|
-
}
|
|
834
|
-
let min = Infinity;
|
|
835
|
-
for (let i = 0; i < len - 2; i += 2) {
|
|
836
|
-
const d = segmentDistance(px, py, vertices[i], vertices[i + 1], vertices[i + 2], vertices[i + 3]);
|
|
837
|
-
if (d < min) {
|
|
838
|
-
min = d;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
if (closed && len >= 6) {
|
|
842
|
-
const d = segmentDistance(px, py, vertices[len - 2], vertices[len - 1], vertices[0], vertices[1]);
|
|
843
|
-
if (d < min) {
|
|
844
|
-
min = d;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
return min;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
983
|
function quadraticBezierP0(t, p) {
|
|
851
984
|
const k = 1 - t;
|
|
852
985
|
return k * k * p;
|
|
@@ -861,22 +994,35 @@ function quadraticBezier(t, p0, p1, p2) {
|
|
|
861
994
|
return quadraticBezierP0(t, p0) + quadraticBezierP1(t, p1) + quadraticBezierP2(t, p2);
|
|
862
995
|
}
|
|
863
996
|
|
|
997
|
+
function resolveLineJoin(join) {
|
|
998
|
+
switch (join) {
|
|
999
|
+
case "round":
|
|
1000
|
+
case "bevel":
|
|
1001
|
+
case "miter":
|
|
1002
|
+
return join;
|
|
1003
|
+
default:
|
|
1004
|
+
return "miter";
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function resolveLineStyle(style) {
|
|
1008
|
+
return {
|
|
1009
|
+
width: style?.strokeWidth ?? 1,
|
|
1010
|
+
alignment: 0.5,
|
|
1011
|
+
join: resolveLineJoin(style?.strokeLinejoin),
|
|
1012
|
+
cap: style?.strokeLinecap ?? "butt",
|
|
1013
|
+
miterLimit: style?.strokeMiterlimit ?? 10
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
864
1016
|
const closePointEps = 1e-4;
|
|
865
1017
|
const curveEps = 1e-4;
|
|
866
1018
|
function strokeTriangulate(points, options = {}) {
|
|
867
1019
|
const {
|
|
868
1020
|
vertices = [],
|
|
869
1021
|
indices = [],
|
|
870
|
-
lineStyle = {
|
|
871
|
-
alignment: 0.5,
|
|
872
|
-
cap: "butt",
|
|
873
|
-
join: "miter",
|
|
874
|
-
width: 1,
|
|
875
|
-
miterLimit: 10
|
|
876
|
-
},
|
|
877
1022
|
flipAlignment = false,
|
|
878
1023
|
closed = true
|
|
879
1024
|
} = options;
|
|
1025
|
+
const lineStyle = options.lineStyle ?? resolveLineStyle(options.style);
|
|
880
1026
|
const eps = closePointEps;
|
|
881
1027
|
if (points.length === 0) {
|
|
882
1028
|
return { vertices, indices };
|
|
@@ -2704,6 +2850,22 @@ class Curve {
|
|
|
2704
2850
|
getControlPointRefs() {
|
|
2705
2851
|
return [];
|
|
2706
2852
|
}
|
|
2853
|
+
/**
|
|
2854
|
+
* Reverse the traversal direction in place (start ↔ end, same geometry). The base
|
|
2855
|
+
* implementation reverses the order of the control-point *values*, which is correct for
|
|
2856
|
+
* line / Bézier / spline primitives whose {@link getControlPointRefs} order matches their
|
|
2857
|
+
* parametric order. {@link RoundCurve} (angle-based) and composites (child order) override it.
|
|
2858
|
+
*/
|
|
2859
|
+
reverse() {
|
|
2860
|
+
const refs = this.getControlPointRefs();
|
|
2861
|
+
const n = refs.length;
|
|
2862
|
+
const snapshot = refs.map((p) => p.clone());
|
|
2863
|
+
for (let i = 0; i < n; i++) {
|
|
2864
|
+
refs[i].copyFrom(snapshot[n - 1 - i]);
|
|
2865
|
+
}
|
|
2866
|
+
this.invalidate();
|
|
2867
|
+
return this;
|
|
2868
|
+
}
|
|
2707
2869
|
applyTransform(transform) {
|
|
2708
2870
|
const isFunction = typeof transform === "function";
|
|
2709
2871
|
this.getControlPointRefs().forEach((p) => {
|
|
@@ -2831,6 +2993,22 @@ class Curve {
|
|
|
2831
2993
|
getTangentAt(u, output) {
|
|
2832
2994
|
return this.getTangent(this.getUToTMapping(u), output);
|
|
2833
2995
|
}
|
|
2996
|
+
/**
|
|
2997
|
+
* PathKit-style sample at an absolute arc-length `distance` along the curve: the point, the unit
|
|
2998
|
+
* tangent, and the tangent `angle` in radians. `distance` is clamped to `[0, getLength()]`, so
|
|
2999
|
+
* passing `0`/`getLength()` always yields the endpoints. See {@link PathMeasure} for a wrapper.
|
|
3000
|
+
*/
|
|
3001
|
+
getPosTan(distance) {
|
|
3002
|
+
const length = this.getLength();
|
|
3003
|
+
const u = length > 0 ? Math.min(Math.max(distance / length, 0), 1) : 0;
|
|
3004
|
+
const t = this.getUToTMapping(u);
|
|
3005
|
+
const tangent = this.getTangent(t);
|
|
3006
|
+
return {
|
|
3007
|
+
position: this.getPoint(t),
|
|
3008
|
+
tangent,
|
|
3009
|
+
angle: Math.atan2(tangent.y, tangent.x)
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
2834
3012
|
getNormal(t, output = new Vector2()) {
|
|
2835
3013
|
this.getTangent(t, output);
|
|
2836
3014
|
return output.set(-output.y, output.x).normalize();
|
|
@@ -2926,10 +3104,28 @@ class Curve {
|
|
|
2926
3104
|
options
|
|
2927
3105
|
);
|
|
2928
3106
|
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Whether this curve forms a closed loop (its outline should be stroked without end caps,
|
|
3109
|
+
* stitching the last vertex back to the first). The base test is purely geometric — the first
|
|
3110
|
+
* sampled vertex coincides with the last. Curves that close without a duplicated endpoint
|
|
3111
|
+
* (a full-revolution {@link RoundCurve}, rectangles, polygons) override this.
|
|
3112
|
+
*/
|
|
3113
|
+
isClosed() {
|
|
3114
|
+
const v = this._getCachedAdaptiveVertices();
|
|
3115
|
+
const len = v.length;
|
|
3116
|
+
if (len < 6) {
|
|
3117
|
+
return false;
|
|
3118
|
+
}
|
|
3119
|
+
const eps = 1e-4;
|
|
3120
|
+
return Math.abs(v[0] - v[len - 2]) < eps && Math.abs(v[1] - v[len - 1]) < eps;
|
|
3121
|
+
}
|
|
2929
3122
|
strokeTriangulate(options) {
|
|
2930
3123
|
return strokeTriangulate(
|
|
2931
3124
|
this.getAdaptiveVertices(),
|
|
2932
|
-
|
|
3125
|
+
{
|
|
3126
|
+
...options,
|
|
3127
|
+
closed: options?.closed ?? this.isClosed()
|
|
3128
|
+
}
|
|
2933
3129
|
);
|
|
2934
3130
|
}
|
|
2935
3131
|
toCommands() {
|
|
@@ -3024,6 +3220,22 @@ class RoundCurve extends Curve {
|
|
|
3024
3220
|
isClockwise() {
|
|
3025
3221
|
return this.clockwise;
|
|
3026
3222
|
}
|
|
3223
|
+
/**
|
|
3224
|
+
* A circle/ellipse arc is closed when it sweeps (at least) a full revolution — the sampled
|
|
3225
|
+
* outline does not duplicate the start vertex, so the geometric first==last test in the base
|
|
3226
|
+
* class would wrongly report a full circle as open and leave a seam gap in the stroke.
|
|
3227
|
+
*/
|
|
3228
|
+
isClosed() {
|
|
3229
|
+
return Math.abs(this.endAngle - this.startAngle) >= Math.PI * 2 - 1e-9 || super.isClosed();
|
|
3230
|
+
}
|
|
3231
|
+
reverse() {
|
|
3232
|
+
const { startAngle, endAngle } = this;
|
|
3233
|
+
this.startAngle = endAngle;
|
|
3234
|
+
this.endAngle = startAngle;
|
|
3235
|
+
this.clockwise = !this.clockwise;
|
|
3236
|
+
this.invalidate();
|
|
3237
|
+
return this;
|
|
3238
|
+
}
|
|
3027
3239
|
_getDeltaAngle() {
|
|
3028
3240
|
const PI_2 = Math.PI * 2;
|
|
3029
3241
|
let deltaAngle = this.endAngle - this.startAngle;
|
|
@@ -3280,9 +3492,25 @@ class RoundCurve extends Curve {
|
|
|
3280
3492
|
return output;
|
|
3281
3493
|
}
|
|
3282
3494
|
getAdaptiveVertices(output = []) {
|
|
3283
|
-
|
|
3495
|
+
const PI2 = Math.PI * 2;
|
|
3496
|
+
if (this.startAngle === 0 && this.endAngle === PI2) {
|
|
3284
3497
|
return this._getAdaptiveVerticesByCircle(output);
|
|
3285
3498
|
}
|
|
3499
|
+
if (Math.abs(this.endAngle - this.startAngle) >= PI2 - 1e-9) {
|
|
3500
|
+
const tmp = this._getAdaptiveVerticesByCircle([]);
|
|
3501
|
+
const n = tmp.length / 2;
|
|
3502
|
+
if (this.endAngle > this.startAngle) {
|
|
3503
|
+
for (let i = 0; i < tmp.length; i++) {
|
|
3504
|
+
output.push(tmp[i]);
|
|
3505
|
+
}
|
|
3506
|
+
} else {
|
|
3507
|
+
output.push(tmp[0], tmp[1]);
|
|
3508
|
+
for (let i = n - 1; i >= 1; i--) {
|
|
3509
|
+
output.push(tmp[i * 2], tmp[i * 2 + 1]);
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
return output;
|
|
3513
|
+
}
|
|
3286
3514
|
return this._getAdaptiveVerticesByArc(output);
|
|
3287
3515
|
}
|
|
3288
3516
|
copyFrom(source) {
|
|
@@ -3492,6 +3720,15 @@ class LineCurve extends Curve {
|
|
|
3492
3720
|
getControlPointRefs() {
|
|
3493
3721
|
return [this.p1, this.p2];
|
|
3494
3722
|
}
|
|
3723
|
+
// Swap endpoint *references* (not values) so corner Vector2s shared with adjacent
|
|
3724
|
+
// segments stay intact and simply re-associate with the reversed segment.
|
|
3725
|
+
reverse() {
|
|
3726
|
+
const { p1, p2 } = this;
|
|
3727
|
+
this.p1 = p2;
|
|
3728
|
+
this.p2 = p1;
|
|
3729
|
+
this.invalidate();
|
|
3730
|
+
return this;
|
|
3731
|
+
}
|
|
3495
3732
|
getAdaptiveVertices(output = []) {
|
|
3496
3733
|
output.push(
|
|
3497
3734
|
this.p1.x,
|
|
@@ -3655,6 +3892,16 @@ class CompositeCurve extends Curve {
|
|
|
3655
3892
|
});
|
|
3656
3893
|
return output;
|
|
3657
3894
|
}
|
|
3895
|
+
/**
|
|
3896
|
+
* A composite is closed when its single child is closed (e.g. a lone full-circle arc), or when
|
|
3897
|
+
* its assembled outline returns to its start (rectangles, polygons, multi-segment loops).
|
|
3898
|
+
*/
|
|
3899
|
+
isClosed() {
|
|
3900
|
+
if (this.curves.length === 1) {
|
|
3901
|
+
return this.curves[0].isClosed();
|
|
3902
|
+
}
|
|
3903
|
+
return super.isClosed();
|
|
3904
|
+
}
|
|
3658
3905
|
strokeTriangulate(options) {
|
|
3659
3906
|
if (this.curves.length === 1) {
|
|
3660
3907
|
return this.curves[0].strokeTriangulate(options);
|
|
@@ -3662,6 +3909,13 @@ class CompositeCurve extends Curve {
|
|
|
3662
3909
|
return super.strokeTriangulate(options);
|
|
3663
3910
|
}
|
|
3664
3911
|
}
|
|
3912
|
+
/** Reverse the sub-curve order and reverse each sub-curve, so the whole outline runs backwards. */
|
|
3913
|
+
reverse() {
|
|
3914
|
+
this.curves.reverse();
|
|
3915
|
+
this.curves.forEach((curve) => curve.reverse());
|
|
3916
|
+
this.invalidate();
|
|
3917
|
+
return this;
|
|
3918
|
+
}
|
|
3665
3919
|
getFillVertices(options) {
|
|
3666
3920
|
if (this.curves.length === 1) {
|
|
3667
3921
|
return this.curves[0].getFillVertices(options);
|
|
@@ -3757,6 +4011,16 @@ class CubicBezierCurve extends Curve {
|
|
|
3757
4011
|
getControlPointRefs() {
|
|
3758
4012
|
return [this.p1, this.cp1, this.cp2, this.p2];
|
|
3759
4013
|
}
|
|
4014
|
+
// Swap endpoint and control-point references; keeps shared corner Vector2s intact.
|
|
4015
|
+
reverse() {
|
|
4016
|
+
const { p1, cp1, cp2, p2 } = this;
|
|
4017
|
+
this.p1 = p2;
|
|
4018
|
+
this.cp1 = cp2;
|
|
4019
|
+
this.cp2 = cp1;
|
|
4020
|
+
this.p2 = p1;
|
|
4021
|
+
this.invalidate();
|
|
4022
|
+
return this;
|
|
4023
|
+
}
|
|
3760
4024
|
_solveQuadratic(a, b, c) {
|
|
3761
4025
|
if (Math.abs(a) < 1e-12) {
|
|
3762
4026
|
if (Math.abs(b) < 1e-12)
|
|
@@ -3917,6 +4181,14 @@ class QuadraticBezierCurve extends Curve {
|
|
|
3917
4181
|
getControlPointRefs() {
|
|
3918
4182
|
return [this.p1, this.cp, this.p2];
|
|
3919
4183
|
}
|
|
4184
|
+
// Swap endpoint references (cp is symmetric); keeps shared corner Vector2s intact.
|
|
4185
|
+
reverse() {
|
|
4186
|
+
const { p1, p2 } = this;
|
|
4187
|
+
this.p1 = p2;
|
|
4188
|
+
this.p2 = p1;
|
|
4189
|
+
this.invalidate();
|
|
4190
|
+
return this;
|
|
4191
|
+
}
|
|
3920
4192
|
getAdaptiveVertices(output = []) {
|
|
3921
4193
|
return getAdaptiveQuadraticBezierCurvePoints(
|
|
3922
4194
|
this.p1.x,
|
|
@@ -4118,6 +4390,11 @@ class SplineCurve extends Curve {
|
|
|
4118
4390
|
getControlPointRefs() {
|
|
4119
4391
|
return this.points;
|
|
4120
4392
|
}
|
|
4393
|
+
reverse() {
|
|
4394
|
+
this.points.reverse();
|
|
4395
|
+
this.invalidate();
|
|
4396
|
+
return this;
|
|
4397
|
+
}
|
|
4121
4398
|
copyFrom(source) {
|
|
4122
4399
|
super.copyFrom(source);
|
|
4123
4400
|
this.points = [];
|
|
@@ -4154,6 +4431,22 @@ class CurvePath extends CompositeCurve {
|
|
|
4154
4431
|
this.addCommands(svgPathDataToCommands(data));
|
|
4155
4432
|
return this;
|
|
4156
4433
|
}
|
|
4434
|
+
/**
|
|
4435
|
+
* A sub-path is closed if it was explicitly closed (`autoClose`, i.e. a `Z`/`closePath`), or if
|
|
4436
|
+
* it forms a geometric loop / wraps a single closed primitive (handled by the base class).
|
|
4437
|
+
*/
|
|
4438
|
+
isClosed() {
|
|
4439
|
+
return this.autoClose || super.isClosed();
|
|
4440
|
+
}
|
|
4441
|
+
/** Reverse direction, then refresh the {@link startPoint}/{@link currentPoint} cursors. */
|
|
4442
|
+
reverse() {
|
|
4443
|
+
super.reverse();
|
|
4444
|
+
if (this.curves.length) {
|
|
4445
|
+
this.startPoint = this.getPoint(0);
|
|
4446
|
+
this.currentPoint = this.getPoint(1);
|
|
4447
|
+
}
|
|
4448
|
+
return this;
|
|
4449
|
+
}
|
|
4157
4450
|
_closeVertices(output) {
|
|
4158
4451
|
if (this.autoClose && output.length >= 4 && (output[0] !== output[output.length - 2] && output[1] !== output[output.length - 1])) {
|
|
4159
4452
|
output.push(output[0], output[1]);
|
|
@@ -4590,6 +4883,51 @@ class Path2D extends CompositeCurve {
|
|
|
4590
4883
|
const fillRule = options.fillRule ?? this.style.fillRule ?? "nonzero";
|
|
4591
4884
|
return pointInPolygons(point, this._getRings(), fillRule);
|
|
4592
4885
|
}
|
|
4886
|
+
/** Build a `Path2D` from flat rings (`[x0,y0,…]` per sub-path); closed-and-filled as sub-paths. */
|
|
4887
|
+
static fromRings(rings, style = {}) {
|
|
4888
|
+
const path = new Path2D(void 0, style);
|
|
4889
|
+
for (const ring of rings) {
|
|
4890
|
+
if (ring.length < 6) {
|
|
4891
|
+
continue;
|
|
4892
|
+
}
|
|
4893
|
+
let end = ring.length;
|
|
4894
|
+
if (ring[0] === ring[end - 2] && ring[1] === ring[end - 1]) {
|
|
4895
|
+
end -= 2;
|
|
4896
|
+
}
|
|
4897
|
+
path.moveTo(ring[0], ring[1]);
|
|
4898
|
+
for (let i = 2; i < end; i += 2) {
|
|
4899
|
+
path.lineTo(ring[i], ring[i + 1]);
|
|
4900
|
+
}
|
|
4901
|
+
path.closePath();
|
|
4902
|
+
}
|
|
4903
|
+
return path;
|
|
4904
|
+
}
|
|
4905
|
+
/**
|
|
4906
|
+
* Boolean (path) operation against another path, returning a NEW `Path2D` whose outline is the
|
|
4907
|
+
* polygonal result. Curves are sampled before clipping, so the result is a polygonal
|
|
4908
|
+
* approximation (see {@link polygonBoolean}). The result inherits this path's `style` unless
|
|
4909
|
+
* overridden via `style`. Holes are emitted as oppositely-wound sub-paths (nonzero fill).
|
|
4910
|
+
*/
|
|
4911
|
+
booleanOp(op, other, style) {
|
|
4912
|
+
const rings = polygonBoolean(op, this._getRings(), other._getRings());
|
|
4913
|
+
return Path2D.fromRings(rings, { ...this.style, ...style });
|
|
4914
|
+
}
|
|
4915
|
+
/** `this ∪ other` — the combined filled area. */
|
|
4916
|
+
union(other, style) {
|
|
4917
|
+
return this.booleanOp("union", other, style);
|
|
4918
|
+
}
|
|
4919
|
+
/** `this ∩ other` — only the overlapping area. */
|
|
4920
|
+
intersection(other, style) {
|
|
4921
|
+
return this.booleanOp("intersection", other, style);
|
|
4922
|
+
}
|
|
4923
|
+
/** `this − other` — this path with `other` cut away. */
|
|
4924
|
+
difference(other, style) {
|
|
4925
|
+
return this.booleanOp("difference", other, style);
|
|
4926
|
+
}
|
|
4927
|
+
/** `this ⊕ other` — areas covered by exactly one of the two paths. */
|
|
4928
|
+
xor(other, style) {
|
|
4929
|
+
return this.booleanOp("xor", other, style);
|
|
4930
|
+
}
|
|
4593
4931
|
/**
|
|
4594
4932
|
* Test whether a point lies on this path's stroke. A hit on any sub-path counts.
|
|
4595
4933
|
*
|
|
@@ -4672,14 +5010,30 @@ class Path2D extends CompositeCurve {
|
|
|
4672
5010
|
});
|
|
4673
5011
|
}
|
|
4674
5012
|
} else {
|
|
4675
|
-
this.curves.
|
|
4676
|
-
|
|
5013
|
+
const paths = this.curves.map((curve) => curve.getFillVertices(_options));
|
|
5014
|
+
const groups = evenoddFillRule(paths);
|
|
5015
|
+
const groupsLen = groups.length;
|
|
5016
|
+
for (let i = 0; i < groupsLen; i++) {
|
|
5017
|
+
const pointArray = paths[i];
|
|
5018
|
+
if ((groups[i].depth & 1) === 1 || !pointArray.length) {
|
|
5019
|
+
continue;
|
|
5020
|
+
}
|
|
5021
|
+
const _pointArray = pointArray.slice();
|
|
5022
|
+
const holes = [];
|
|
5023
|
+
for (let j = 0; j < groupsLen; j++) {
|
|
5024
|
+
if (groups[j].parentIndex === i && (groups[j].depth & 1) === 1) {
|
|
5025
|
+
holes.push(_pointArray.length / 2);
|
|
5026
|
+
_pointArray.push(...paths[j]);
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
fillTriangulate(_pointArray, {
|
|
4677
5030
|
...options,
|
|
4678
5031
|
indices,
|
|
4679
5032
|
vertices,
|
|
5033
|
+
holes,
|
|
4680
5034
|
style: { ...this.style }
|
|
4681
5035
|
});
|
|
4682
|
-
}
|
|
5036
|
+
}
|
|
4683
5037
|
}
|
|
4684
5038
|
return { indices, vertices };
|
|
4685
5039
|
}
|
|
@@ -4689,7 +5043,7 @@ class Path2D extends CompositeCurve {
|
|
|
4689
5043
|
}
|
|
4690
5044
|
drawTo(ctx, style = {}) {
|
|
4691
5045
|
style = { ...this.style, ...style };
|
|
4692
|
-
const { fill = "#000", stroke = "none" } = style;
|
|
5046
|
+
const { fill = "#000", stroke = "none", fillRule = "nonzero" } = style;
|
|
4693
5047
|
ctx.beginPath();
|
|
4694
5048
|
ctx.save();
|
|
4695
5049
|
setCanvasContext(ctx, style);
|
|
@@ -4697,7 +5051,7 @@ class Path2D extends CompositeCurve {
|
|
|
4697
5051
|
path.drawTo(ctx);
|
|
4698
5052
|
});
|
|
4699
5053
|
if (fill !== "none") {
|
|
4700
|
-
ctx.fill();
|
|
5054
|
+
ctx.fill(fillRule);
|
|
4701
5055
|
}
|
|
4702
5056
|
if (stroke !== "none") {
|
|
4703
5057
|
ctx.stroke();
|
|
@@ -4903,6 +5257,44 @@ ${content}
|
|
|
4903
5257
|
}
|
|
4904
5258
|
}
|
|
4905
5259
|
|
|
5260
|
+
class PathMeasure {
|
|
5261
|
+
constructor(curve) {
|
|
5262
|
+
this.curve = curve;
|
|
5263
|
+
}
|
|
5264
|
+
/** Total arc length of the path. */
|
|
5265
|
+
getLength() {
|
|
5266
|
+
return this.curve.getLength();
|
|
5267
|
+
}
|
|
5268
|
+
/** Whether the path forms a closed loop (see {@link Curve.isClosed}). */
|
|
5269
|
+
isClosed() {
|
|
5270
|
+
return this.curve.isClosed();
|
|
5271
|
+
}
|
|
5272
|
+
/** Point + unit tangent + tangent angle at an absolute arc-length `distance` (clamped). */
|
|
5273
|
+
getPosTan(distance) {
|
|
5274
|
+
return this.curve.getPosTan(distance);
|
|
5275
|
+
}
|
|
5276
|
+
/** Point at an absolute arc-length `distance` (clamped to `[0, getLength()]`). */
|
|
5277
|
+
getPosition(distance) {
|
|
5278
|
+
return this.curve.getPosTan(distance).position;
|
|
5279
|
+
}
|
|
5280
|
+
/** Point + tangent at a normalized progress `t ∈ [0, 1]` along the path. */
|
|
5281
|
+
getPosTanAtProgress(t) {
|
|
5282
|
+
return this.curve.getPosTan(this.getLength() * t);
|
|
5283
|
+
}
|
|
5284
|
+
/**
|
|
5285
|
+
* Evenly sample the path into `count + 1` {@link PosTan} entries (arc-length spaced), e.g. to
|
|
5286
|
+
* lay glyphs along a path or drive an `animate(progress)`-style traversal.
|
|
5287
|
+
*/
|
|
5288
|
+
sample(count = 100) {
|
|
5289
|
+
const length = this.getLength();
|
|
5290
|
+
const out = [];
|
|
5291
|
+
for (let i = 0; i <= count; i++) {
|
|
5292
|
+
out.push(this.curve.getPosTan(length * i / count));
|
|
5293
|
+
}
|
|
5294
|
+
return out;
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
|
|
4906
5298
|
class FFDControlGrid {
|
|
4907
5299
|
constructor(rows, cols, width = 1, height = 1) {
|
|
4908
5300
|
this.rows = rows;
|
|
@@ -4958,4 +5350,4 @@ function applyFFD(point, grid, width = grid.width, height = grid.height) {
|
|
|
4958
5350
|
point.set(x, y);
|
|
4959
5351
|
}
|
|
4960
5352
|
|
|
4961
|
-
export { ArcCurve, BoundingBox, CompositeCurve, CubicBezierCurve, Curve, CurvePath, EllipseCurve, EquilateralPolygonCurve, FFDControlGrid, LineCurve, PI, PI_2, Path2D, Path2DSet, PolygonCurve, QuadraticBezierCurve, RectangleCurve, RoundRectangleCurve, SplineCurve, Transform2D, Vector2, applyFFD, catmullRom, cubicBezier, drawPoint, fillTriangulate, getAdaptiveCubicBezierCurvePoints, getAdaptiveQuadraticBezierCurvePoints, getDirectedArea, getIntersectionPoint, nonzeroFillRule, parseArcCommand, parseCssArg, parseCssArgs, parseCssFunctions, parsePathDataArgs, pointInPolygon, pointInPolygons, pointToPolylineDistance, pointToSegmentDistance, quadraticBezier, setCanvasContext, strokeTriangulate, svgPathCommandsAddToPath2D, svgPathCommandsToData, svgPathDataToCommands, svgToDom, svgToPath2DSet, toKebabCase };
|
|
5353
|
+
export { ArcCurve, BoundingBox, CompositeCurve, CubicBezierCurve, Curve, CurvePath, EllipseCurve, EquilateralPolygonCurve, FFDControlGrid, LineCurve, PI, PI_2, Path2D, Path2DSet, PathMeasure, PolygonCurve, QuadraticBezierCurve, RectangleCurve, RoundRectangleCurve, SplineCurve, Transform2D, Vector2, applyFFD, catmullRom, cubicBezier, drawPoint, evenoddFillRule, fillTriangulate, getAdaptiveCubicBezierCurvePoints, getAdaptiveQuadraticBezierCurvePoints, getDirectedArea, getIntersectionPoint, nonzeroFillRule, parseArcCommand, parseCssArg, parseCssArgs, parseCssFunctions, parsePathDataArgs, pointInPolygon, pointInPolygons, pointToPolylineDistance, pointToSegmentDistance, polygonBoolean, quadraticBezier, resolveLineStyle, setCanvasContext, strokeTriangulate, svgPathCommandsAddToPath2D, svgPathCommandsToData, svgPathDataToCommands, svgToDom, svgToPath2DSet, toKebabCase };
|