modern-path2d 1.8.0 → 1.8.2

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 CHANGED
@@ -492,6 +492,195 @@ function cubicBezier(t, p0, p1, p2, p3) {
492
492
  return cubicBezierP0(t, p0) + cubicBezierP1(t, p1) + cubicBezierP2(t, p2) + cubicBezierP3(t, p3);
493
493
  }
494
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
+
495
684
  function fillTriangulate(pointArray, options = {}) {
496
685
  let {
497
686
  vertices = [],
@@ -654,7 +843,7 @@ function getDirectedArea(vertices) {
654
843
  function cross(ax, ay, bx, by, cx, cy) {
655
844
  return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
656
845
  }
657
- function windingNumber$1(px, py, polygon) {
846
+ function windingNumber(px, py, polygon) {
658
847
  const polygonLen = polygon.length;
659
848
  let wn = 0;
660
849
  for (let i = 0, j = polygonLen - 2; i < polygonLen; j = i, i += 2) {
@@ -775,7 +964,7 @@ function nonzeroFillRule(paths) {
775
964
  const wnList = [];
776
965
  for (let p = 0, pLen = testPoints.length; p < pLen; p++) {
777
966
  const [x, y] = testPoints[p];
778
- const winding = windingNumber$1(x, y, paths[j]);
967
+ const winding = windingNumber(x, y, paths[j]);
779
968
  wnMap[winding] = (wnMap[winding] ?? 0) + 1;
780
969
  wnList.push(winding);
781
970
  }
@@ -798,120 +987,6 @@ function nonzeroFillRule(paths) {
798
987
  return results;
799
988
  }
800
989
 
801
- function isLeft(ax, ay, bx, by, px, py) {
802
- return (bx - ax) * (py - ay) - (px - ax) * (by - ay);
803
- }
804
- function windingNumber(px, py, vertices) {
805
- const len = vertices.length;
806
- let wn = 0;
807
- for (let i = 0; i < len; i += 2) {
808
- const ax = vertices[i];
809
- const ay = vertices[i + 1];
810
- const k = (i + 2) % len;
811
- const bx = vertices[k];
812
- const by = vertices[k + 1];
813
- if (ay <= py) {
814
- if (by > py && isLeft(ax, ay, bx, by, px, py) > 0) {
815
- wn++;
816
- }
817
- } else {
818
- if (by <= py && isLeft(ax, ay, bx, by, px, py) < 0) {
819
- wn--;
820
- }
821
- }
822
- }
823
- return wn;
824
- }
825
- function crossingNumber(px, py, vertices) {
826
- const len = vertices.length;
827
- let cn = 0;
828
- for (let i = 0; i < len; i += 2) {
829
- const ax = vertices[i];
830
- const ay = vertices[i + 1];
831
- const k = (i + 2) % len;
832
- const bx = vertices[k];
833
- const by = vertices[k + 1];
834
- if (ay <= py && by > py || ay > py && by <= py) {
835
- const t = (py - ay) / (by - ay);
836
- if (px < ax + t * (bx - ax)) {
837
- cn++;
838
- }
839
- }
840
- }
841
- return cn;
842
- }
843
- function segmentDistance(px, py, ax, ay, bx, by) {
844
- const dx = bx - ax;
845
- const dy = by - ay;
846
- const lenSq = dx * dx + dy * dy;
847
- let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq;
848
- if (t < 0) {
849
- t = 0;
850
- } else if (t > 1) {
851
- t = 1;
852
- }
853
- const cx = ax + t * dx;
854
- const cy = ay + t * dy;
855
- return Math.hypot(px - cx, py - cy);
856
- }
857
- function pointInPolygon(point, vertices, fillRule = "nonzero") {
858
- if (vertices.length < 6) {
859
- return false;
860
- }
861
- if (fillRule === "evenodd") {
862
- return (crossingNumber(point.x, point.y, vertices) & 1) === 1;
863
- }
864
- return windingNumber(point.x, point.y, vertices) !== 0;
865
- }
866
- function pointInPolygons(point, polygons, fillRule = "nonzero") {
867
- const { x, y } = point;
868
- if (fillRule === "evenodd") {
869
- let cn = 0;
870
- for (let i = 0, len = polygons.length; i < len; i++) {
871
- const ring = polygons[i];
872
- if (ring.length >= 6) {
873
- cn += crossingNumber(x, y, ring);
874
- }
875
- }
876
- return (cn & 1) === 1;
877
- }
878
- let wn = 0;
879
- for (let i = 0, len = polygons.length; i < len; i++) {
880
- const ring = polygons[i];
881
- if (ring.length >= 6) {
882
- wn += windingNumber(x, y, ring);
883
- }
884
- }
885
- return wn !== 0;
886
- }
887
- function pointToSegmentDistance(point, a, b) {
888
- return segmentDistance(point.x, point.y, a.x, a.y, b.x, b.y);
889
- }
890
- function pointToPolylineDistance(point, vertices, closed = false) {
891
- const len = vertices.length;
892
- if (len < 2) {
893
- return Infinity;
894
- }
895
- const { x: px, y: py } = point;
896
- if (len === 2) {
897
- return Math.hypot(px - vertices[0], py - vertices[1]);
898
- }
899
- let min = Infinity;
900
- for (let i = 0; i < len - 2; i += 2) {
901
- const d = segmentDistance(px, py, vertices[i], vertices[i + 1], vertices[i + 2], vertices[i + 3]);
902
- if (d < min) {
903
- min = d;
904
- }
905
- }
906
- if (closed && len >= 6) {
907
- const d = segmentDistance(px, py, vertices[len - 2], vertices[len - 1], vertices[0], vertices[1]);
908
- if (d < min) {
909
- min = d;
910
- }
911
- }
912
- return min;
913
- }
914
-
915
990
  function quadraticBezierP0(t, p) {
916
991
  const k = 1 - t;
917
992
  return k * k * p;
@@ -959,6 +1034,10 @@ function strokeTriangulate(points, options = {}) {
959
1034
  if (points.length === 0) {
960
1035
  return { vertices, indices };
961
1036
  }
1037
+ points = dedupeConsecutivePoints(points, eps);
1038
+ if (points.length < 4) {
1039
+ return { vertices, indices };
1040
+ }
962
1041
  const style = lineStyle;
963
1042
  let alignment = style.alignment;
964
1043
  if (lineStyle.alignment !== 0.5) {
@@ -1248,6 +1327,17 @@ function strokeTriangulate(points, options = {}) {
1248
1327
  indices
1249
1328
  };
1250
1329
  }
1330
+ function dedupeConsecutivePoints(points, eps) {
1331
+ const out = [points[0], points[1]];
1332
+ for (let i = 2; i < points.length; i += 2) {
1333
+ const x = points[i];
1334
+ const y = points[i + 1];
1335
+ if (Math.abs(x - out[out.length - 2]) >= eps || Math.abs(y - out[out.length - 1]) >= eps) {
1336
+ out.push(x, y);
1337
+ }
1338
+ }
1339
+ return out;
1340
+ }
1251
1341
  function getOrientationOfPoints(points) {
1252
1342
  const m = points.length;
1253
1343
  if (m < 6) {
@@ -4942,14 +5032,30 @@ class Path2D extends CompositeCurve {
4942
5032
  });
4943
5033
  }
4944
5034
  } else {
4945
- this.curves.forEach((curve) => {
4946
- curve.fillTriangulate({
5035
+ const paths = this.curves.map((curve) => curve.getFillVertices(_options));
5036
+ const groups = evenoddFillRule(paths);
5037
+ const groupsLen = groups.length;
5038
+ for (let i = 0; i < groupsLen; i++) {
5039
+ const pointArray = paths[i];
5040
+ if ((groups[i].depth & 1) === 1 || !pointArray.length) {
5041
+ continue;
5042
+ }
5043
+ const _pointArray = pointArray.slice();
5044
+ const holes = [];
5045
+ for (let j = 0; j < groupsLen; j++) {
5046
+ if (groups[j].parentIndex === i && (groups[j].depth & 1) === 1) {
5047
+ holes.push(_pointArray.length / 2);
5048
+ _pointArray.push(...paths[j]);
5049
+ }
5050
+ }
5051
+ fillTriangulate(_pointArray, {
4947
5052
  ...options,
4948
5053
  indices,
4949
5054
  vertices,
5055
+ holes,
4950
5056
  style: { ...this.style }
4951
5057
  });
4952
- });
5058
+ }
4953
5059
  }
4954
5060
  return { indices, vertices };
4955
5061
  }
@@ -5292,6 +5398,7 @@ exports.applyFFD = applyFFD;
5292
5398
  exports.catmullRom = catmullRom;
5293
5399
  exports.cubicBezier = cubicBezier;
5294
5400
  exports.drawPoint = drawPoint;
5401
+ exports.evenoddFillRule = evenoddFillRule;
5295
5402
  exports.fillTriangulate = fillTriangulate;
5296
5403
  exports.getAdaptiveCubicBezierCurvePoints = getAdaptiveCubicBezierCurvePoints;
5297
5404
  exports.getAdaptiveQuadraticBezierCurvePoints = getAdaptiveQuadraticBezierCurvePoints;
package/dist/index.d.cts CHANGED
@@ -288,6 +288,25 @@ declare function parseCssArg(name: string, value: string, context?: ParseCssFunc
288
288
 
289
289
  declare function cubicBezier(t: number, p0: number, p1: number, p2: number, p3: number): number;
290
290
 
291
+ interface EvenoddFillRuleResult {
292
+ index: number;
293
+ /** Containment depth: 0/2/4… are filled shells, 1/3/5… are holes. */
294
+ depth: number;
295
+ /** Immediate enclosing ring (deepest container), or -1 for a top-level ring. */
296
+ parentIndex: number;
297
+ }
298
+ /**
299
+ * Even-odd nesting of a ring soup: classify each ring by how many other rings contain it.
300
+ * Even depth → a filled shell; odd depth → a hole. Each ring also gets its immediate parent
301
+ * (the deepest container), so a shell can collect exactly the holes one level inside it — and
302
+ * an island inside a hole becomes its own shell again (nested donuts work).
303
+ *
304
+ * Mirrors {@link nonzeroFillRule}'s output shape, but uses containment parity (even-odd) instead
305
+ * of winding. Used by `Path2D.fillTriangulate` for `fillRule: 'evenodd'` so WebGL/triangulated
306
+ * fills get real holes instead of solid overlapping rings.
307
+ */
308
+ declare function evenoddFillRule(paths: number[][]): EvenoddFillRuleResult[];
309
+
291
310
  interface FillTriangulateOptions {
292
311
  holes?: number[];
293
312
  vertices?: number[];
@@ -995,5 +1014,5 @@ declare function svgToDom(svg: string | SVGElement): SVGElement;
995
1014
 
996
1015
  declare function svgToPath2DSet(svg: string | SVGElement): Path2DSet;
997
1016
 
998
- 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, 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 };
999
- export type { BooleanOp, CssFunction, CssFunctionArg, DrawPointOptions, FillRule, FillTriangulateOptions, FillTriangulatedResult, FlatRing, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, PosTan, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };
1017
+ 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 };
1018
+ export type { BooleanOp, CssFunction, CssFunctionArg, DrawPointOptions, EvenoddFillRuleResult, FillRule, FillTriangulateOptions, FillTriangulatedResult, FlatRing, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, PosTan, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };
package/dist/index.d.mts CHANGED
@@ -288,6 +288,25 @@ declare function parseCssArg(name: string, value: string, context?: ParseCssFunc
288
288
 
289
289
  declare function cubicBezier(t: number, p0: number, p1: number, p2: number, p3: number): number;
290
290
 
291
+ interface EvenoddFillRuleResult {
292
+ index: number;
293
+ /** Containment depth: 0/2/4… are filled shells, 1/3/5… are holes. */
294
+ depth: number;
295
+ /** Immediate enclosing ring (deepest container), or -1 for a top-level ring. */
296
+ parentIndex: number;
297
+ }
298
+ /**
299
+ * Even-odd nesting of a ring soup: classify each ring by how many other rings contain it.
300
+ * Even depth → a filled shell; odd depth → a hole. Each ring also gets its immediate parent
301
+ * (the deepest container), so a shell can collect exactly the holes one level inside it — and
302
+ * an island inside a hole becomes its own shell again (nested donuts work).
303
+ *
304
+ * Mirrors {@link nonzeroFillRule}'s output shape, but uses containment parity (even-odd) instead
305
+ * of winding. Used by `Path2D.fillTriangulate` for `fillRule: 'evenodd'` so WebGL/triangulated
306
+ * fills get real holes instead of solid overlapping rings.
307
+ */
308
+ declare function evenoddFillRule(paths: number[][]): EvenoddFillRuleResult[];
309
+
291
310
  interface FillTriangulateOptions {
292
311
  holes?: number[];
293
312
  vertices?: number[];
@@ -995,5 +1014,5 @@ declare function svgToDom(svg: string | SVGElement): SVGElement;
995
1014
 
996
1015
  declare function svgToPath2DSet(svg: string | SVGElement): Path2DSet;
997
1016
 
998
- 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, 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 };
999
- export type { BooleanOp, CssFunction, CssFunctionArg, DrawPointOptions, FillRule, FillTriangulateOptions, FillTriangulatedResult, FlatRing, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, PosTan, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };
1017
+ 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 };
1018
+ export type { BooleanOp, CssFunction, CssFunctionArg, DrawPointOptions, EvenoddFillRuleResult, FillRule, FillTriangulateOptions, FillTriangulatedResult, FlatRing, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, PosTan, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };
package/dist/index.d.ts CHANGED
@@ -288,6 +288,25 @@ declare function parseCssArg(name: string, value: string, context?: ParseCssFunc
288
288
 
289
289
  declare function cubicBezier(t: number, p0: number, p1: number, p2: number, p3: number): number;
290
290
 
291
+ interface EvenoddFillRuleResult {
292
+ index: number;
293
+ /** Containment depth: 0/2/4… are filled shells, 1/3/5… are holes. */
294
+ depth: number;
295
+ /** Immediate enclosing ring (deepest container), or -1 for a top-level ring. */
296
+ parentIndex: number;
297
+ }
298
+ /**
299
+ * Even-odd nesting of a ring soup: classify each ring by how many other rings contain it.
300
+ * Even depth → a filled shell; odd depth → a hole. Each ring also gets its immediate parent
301
+ * (the deepest container), so a shell can collect exactly the holes one level inside it — and
302
+ * an island inside a hole becomes its own shell again (nested donuts work).
303
+ *
304
+ * Mirrors {@link nonzeroFillRule}'s output shape, but uses containment parity (even-odd) instead
305
+ * of winding. Used by `Path2D.fillTriangulate` for `fillRule: 'evenodd'` so WebGL/triangulated
306
+ * fills get real holes instead of solid overlapping rings.
307
+ */
308
+ declare function evenoddFillRule(paths: number[][]): EvenoddFillRuleResult[];
309
+
291
310
  interface FillTriangulateOptions {
292
311
  holes?: number[];
293
312
  vertices?: number[];
@@ -995,5 +1014,5 @@ declare function svgToDom(svg: string | SVGElement): SVGElement;
995
1014
 
996
1015
  declare function svgToPath2DSet(svg: string | SVGElement): Path2DSet;
997
1016
 
998
- 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, 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 };
999
- export type { BooleanOp, CssFunction, CssFunctionArg, DrawPointOptions, FillRule, FillTriangulateOptions, FillTriangulatedResult, FlatRing, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, PosTan, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };
1017
+ 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 };
1018
+ export type { BooleanOp, CssFunction, CssFunctionArg, DrawPointOptions, EvenoddFillRuleResult, FillRule, FillTriangulateOptions, FillTriangulatedResult, FlatRing, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, PosTan, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };