modern-path2d 1.5.6 → 1.6.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 CHANGED
@@ -189,11 +189,9 @@ class Vector2 {
189
189
  return Math.sqrt(this.lengthSquared());
190
190
  }
191
191
  scale(sx, sy = sx, origin = { x: 0, y: 0 }) {
192
- const x = sx < 0 ? origin.x - this._x + origin.x : this._x;
193
- const y = sy < 0 ? origin.y - this._y + origin.y : this._y;
194
192
  return this.set(
195
- x * Math.abs(sx),
196
- y * Math.abs(sy)
193
+ origin.x + (this._x - origin.x) * sx,
194
+ origin.y + (this._y - origin.y) * sy
197
195
  );
198
196
  }
199
197
  skew(ax, ay = 0, origin = { x: 0, y: 0 }) {
@@ -590,7 +588,7 @@ function getDirectedArea(vertices) {
590
588
  for (let i = 0; i < n; i += 2) {
591
589
  const x0 = vertices[i];
592
590
  const y0 = vertices[i + 1];
593
- const x1 = vertices[(i + 2) % (n - 1)];
591
+ const x1 = vertices[(i + 2) % n];
594
592
  const y1 = vertices[(i + 3) % n];
595
593
  area += x0 * y1 - x1 * y0;
596
594
  }
@@ -600,7 +598,7 @@ function getDirectedArea(vertices) {
600
598
  function cross(ax, ay, bx, by, cx, cy) {
601
599
  return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
602
600
  }
603
- function windingNumber(px, py, polygon) {
601
+ function windingNumber$1(px, py, polygon) {
604
602
  const polygonLen = polygon.length;
605
603
  let wn = 0;
606
604
  for (let i = 0, j = polygonLen - 2; i < polygonLen; j = i, i += 2) {
@@ -705,7 +703,7 @@ function nonzeroFillRule(paths) {
705
703
  const wnList = [];
706
704
  for (let p = 0, pLen = testPoints.length; p < pLen; p++) {
707
705
  const [x, y] = testPoints[p];
708
- const winding = windingNumber(x, y, paths[j]);
706
+ const winding = windingNumber$1(x, y, paths[j]);
709
707
  wnMap[winding] = (wnMap[winding] ?? 0) + 1;
710
708
  wnList.push(winding);
711
709
  }
@@ -728,6 +726,120 @@ function nonzeroFillRule(paths) {
728
726
  return results;
729
727
  }
730
728
 
729
+ function isLeft(ax, ay, bx, by, px, py) {
730
+ return (bx - ax) * (py - ay) - (px - ax) * (by - ay);
731
+ }
732
+ function windingNumber(px, py, vertices) {
733
+ const len = vertices.length;
734
+ let wn = 0;
735
+ for (let i = 0; i < len; i += 2) {
736
+ const ax = vertices[i];
737
+ const ay = vertices[i + 1];
738
+ const k = (i + 2) % len;
739
+ const bx = vertices[k];
740
+ const by = vertices[k + 1];
741
+ if (ay <= py) {
742
+ if (by > py && isLeft(ax, ay, bx, by, px, py) > 0) {
743
+ wn++;
744
+ }
745
+ } else {
746
+ if (by <= py && isLeft(ax, ay, bx, by, px, py) < 0) {
747
+ wn--;
748
+ }
749
+ }
750
+ }
751
+ return wn;
752
+ }
753
+ function crossingNumber(px, py, vertices) {
754
+ const len = vertices.length;
755
+ let cn = 0;
756
+ for (let i = 0; i < len; i += 2) {
757
+ const ax = vertices[i];
758
+ const ay = vertices[i + 1];
759
+ const k = (i + 2) % len;
760
+ const bx = vertices[k];
761
+ const by = vertices[k + 1];
762
+ if (ay <= py && by > py || ay > py && by <= py) {
763
+ const t = (py - ay) / (by - ay);
764
+ if (px < ax + t * (bx - ax)) {
765
+ cn++;
766
+ }
767
+ }
768
+ }
769
+ return cn;
770
+ }
771
+ function segmentDistance(px, py, ax, ay, bx, by) {
772
+ const dx = bx - ax;
773
+ const dy = by - ay;
774
+ const lenSq = dx * dx + dy * dy;
775
+ let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq;
776
+ if (t < 0) {
777
+ t = 0;
778
+ } else if (t > 1) {
779
+ t = 1;
780
+ }
781
+ const cx = ax + t * dx;
782
+ const cy = ay + t * dy;
783
+ return Math.hypot(px - cx, py - cy);
784
+ }
785
+ function pointInPolygon(point, vertices, fillRule = "nonzero") {
786
+ if (vertices.length < 6) {
787
+ return false;
788
+ }
789
+ if (fillRule === "evenodd") {
790
+ return (crossingNumber(point.x, point.y, vertices) & 1) === 1;
791
+ }
792
+ return windingNumber(point.x, point.y, vertices) !== 0;
793
+ }
794
+ function pointInPolygons(point, polygons, fillRule = "nonzero") {
795
+ const { x, y } = point;
796
+ if (fillRule === "evenodd") {
797
+ let cn = 0;
798
+ for (let i = 0, len = polygons.length; i < len; i++) {
799
+ const ring = polygons[i];
800
+ if (ring.length >= 6) {
801
+ cn += crossingNumber(x, y, ring);
802
+ }
803
+ }
804
+ return (cn & 1) === 1;
805
+ }
806
+ let wn = 0;
807
+ for (let i = 0, len = polygons.length; i < len; i++) {
808
+ const ring = polygons[i];
809
+ if (ring.length >= 6) {
810
+ wn += windingNumber(x, y, ring);
811
+ }
812
+ }
813
+ return wn !== 0;
814
+ }
815
+ function pointToSegmentDistance(point, a, b) {
816
+ return segmentDistance(point.x, point.y, a.x, a.y, b.x, b.y);
817
+ }
818
+ function pointToPolylineDistance(point, vertices, closed = false) {
819
+ const len = vertices.length;
820
+ if (len < 2) {
821
+ return Infinity;
822
+ }
823
+ const { x: px, y: py } = point;
824
+ if (len === 2) {
825
+ return Math.hypot(px - vertices[0], py - vertices[1]);
826
+ }
827
+ let min = Infinity;
828
+ for (let i = 0; i < len - 2; i += 2) {
829
+ const d = segmentDistance(px, py, vertices[i], vertices[i + 1], vertices[i + 2], vertices[i + 3]);
830
+ if (d < min) {
831
+ min = d;
832
+ }
833
+ }
834
+ if (closed && len >= 6) {
835
+ const d = segmentDistance(px, py, vertices[len - 2], vertices[len - 1], vertices[0], vertices[1]);
836
+ if (d < min) {
837
+ min = d;
838
+ }
839
+ }
840
+ return min;
841
+ }
842
+
731
843
  function quadraticBezierP0(t, p) {
732
844
  const k = 1 - t;
733
845
  return k * k * p;
@@ -1741,8 +1853,14 @@ function getReflection(a, b) {
1741
1853
  function svgPathCommandsAddToPath2D(commands, path) {
1742
1854
  const current = new Vector2();
1743
1855
  const control = new Vector2();
1856
+ let prevType = "";
1744
1857
  for (let i = 0, l = commands.length; i < l; i++) {
1745
1858
  const cmd = commands[i];
1859
+ if ((cmd.type === "s" || cmd.type === "S") && !"CcSs".includes(prevType)) {
1860
+ control.copyFrom(current);
1861
+ } else if ((cmd.type === "t" || cmd.type === "T") && !"QqTt".includes(prevType)) {
1862
+ control.copyFrom(current);
1863
+ }
1746
1864
  if (cmd.type === "m" || cmd.type === "M") {
1747
1865
  if (cmd.type === "m") {
1748
1866
  current.add(cmd);
@@ -1901,6 +2019,7 @@ function svgPathCommandsAddToPath2D(commands, path) {
1901
2019
  } else {
1902
2020
  console.warn("Unsupported commands", cmd);
1903
2021
  }
2022
+ prevType = cmd.type;
1904
2023
  }
1905
2024
  }
1906
2025
 
@@ -2325,10 +2444,14 @@ function parsePolylineNode(node) {
2325
2444
  function parseRectNode(node) {
2326
2445
  const x = parseFloatWithUnits(node.getAttribute("x") || 0);
2327
2446
  const y = parseFloatWithUnits(node.getAttribute("y") || 0);
2328
- const rx = parseFloatWithUnits(node.getAttribute("rx") || node.getAttribute("ry") || 0);
2329
- const ry = parseFloatWithUnits(node.getAttribute("ry") || node.getAttribute("rx") || 0);
2447
+ const rxAttr = node.getAttribute("rx");
2448
+ const ryAttr = node.getAttribute("ry");
2449
+ let rx = parseFloatWithUnits(rxAttr ?? ryAttr ?? 0);
2450
+ let ry = parseFloatWithUnits(ryAttr ?? rxAttr ?? 0);
2330
2451
  const w = parseFloatWithUnits(node.getAttribute("width"));
2331
2452
  const h = parseFloatWithUnits(node.getAttribute("height"));
2453
+ rx = Math.max(0, Math.min(rx, w / 2));
2454
+ ry = Math.max(0, Math.min(ry, h / 2));
2332
2455
  const bci = 1 - 0.551915024494;
2333
2456
  const path = new Path2D();
2334
2457
  path.moveTo(x + rx, y);
@@ -2704,6 +2827,40 @@ class Curve {
2704
2827
  const { min, max } = this.getMinMax();
2705
2828
  return new BoundingBox(min.x, min.y, max.x - min.x, max.y - min.y);
2706
2829
  }
2830
+ /**
2831
+ * Test whether a point lies inside the area enclosed by this curve.
2832
+ *
2833
+ * The curve is sampled via {@link getAdaptiveVertices} into a single implicitly closed
2834
+ * ring. This is purely geometric (it ignores any `fill`/`stroke` style), mirroring
2835
+ * `CanvasRenderingContext2D.isPointInPath`.
2836
+ *
2837
+ * Composites that hold multiple sub-paths (e.g. {@link Path2D}) override this so holes
2838
+ * are honored — a single `Curve` is always one ring.
2839
+ */
2840
+ isPointInFill(point, options = {}) {
2841
+ return pointInPolygon(point, this.getAdaptiveVertices(), options.fillRule);
2842
+ }
2843
+ /**
2844
+ * Test whether a point lies on this curve's stroke, i.e. within `strokeWidth / 2 + tolerance`
2845
+ * of the sampled outline. The point must be in the same coordinate space as the curve.
2846
+ *
2847
+ * Options: `strokeWidth` (path units, default `1`), `tolerance` (extra hit slack in path
2848
+ * units, default `0` — useful for thin strokes; no coordinate scaling is assumed, so convert
2849
+ * pixel tolerance to path units upstream if your path is normalized), and `closed` (whether
2850
+ * to include the closing edge from the last vertex back to the first).
2851
+ */
2852
+ isPointInStroke(point, options = {}) {
2853
+ const { strokeWidth = 1, tolerance = 0, closed = false } = options;
2854
+ const distance = pointToPolylineDistance(point, this.getAdaptiveVertices(), closed);
2855
+ return distance <= strokeWidth / 2 + tolerance;
2856
+ }
2857
+ /**
2858
+ * Concise PathKit-style fill containment test: `contains(x, y)` is shorthand for
2859
+ * {@link isPointInFill} with a `{ x, y }` point.
2860
+ */
2861
+ contains(x, y, options = {}) {
2862
+ return this.isPointInFill({ x, y }, options);
2863
+ }
2707
2864
  getFillVertices(_options) {
2708
2865
  return this.getAdaptiveVertices();
2709
2866
  }
@@ -3114,6 +3271,8 @@ function eigenDecomposition(A, B, C) {
3114
3271
  rt2 = A * t * C - B * t * B;
3115
3272
  } else if (sm < 0) {
3116
3273
  rt2 = 0.5 * (sm - rt);
3274
+ t = 1 / rt2;
3275
+ rt1 = A * t * C - B * t * B;
3117
3276
  } else {
3118
3277
  rt1 = 0.5 * rt;
3119
3278
  rt2 = -0.5 * rt;
@@ -3279,20 +3438,26 @@ class CompositeCurve extends Curve {
3279
3438
  getPoint(t, output = new Vector2()) {
3280
3439
  const d = t * this.getLength();
3281
3440
  const lengths = this.getLengths();
3282
- let i = 0;
3283
- while (i < lengths.length) {
3284
- if (lengths[i] >= d) {
3285
- const diff = lengths[i] - d;
3286
- const curve = this.curves[i];
3287
- const length = curve.getLength();
3288
- return curve.getPointAt(
3289
- length === 0 ? 0 : 1 - diff / length,
3290
- output
3291
- );
3441
+ const n = lengths.length;
3442
+ if (n === 0)
3443
+ return output;
3444
+ let lo = 0;
3445
+ let hi = n - 1;
3446
+ while (lo < hi) {
3447
+ const mid = lo + hi >>> 1;
3448
+ if (lengths[mid] < d) {
3449
+ lo = mid + 1;
3450
+ } else {
3451
+ hi = mid;
3292
3452
  }
3293
- i++;
3294
3453
  }
3295
- return output;
3454
+ const diff = lengths[lo] - d;
3455
+ const curve = this.curves[lo];
3456
+ const length = curve.getLength();
3457
+ return curve.getPointAt(
3458
+ length === 0 ? 0 : 1 - diff / length,
3459
+ output
3460
+ );
3296
3461
  }
3297
3462
  getLengths() {
3298
3463
  if (this._lengths.length !== this.curves.length) {
@@ -3441,6 +3606,12 @@ class CubicBezierCurve extends Curve {
3441
3606
  return [this.p1, this.cp1, this.cp2, this.p2];
3442
3607
  }
3443
3608
  _solveQuadratic(a, b, c) {
3609
+ if (Math.abs(a) < 1e-12) {
3610
+ if (Math.abs(b) < 1e-12)
3611
+ return [];
3612
+ const t = -c / b;
3613
+ return t >= 0 && t <= 1 ? [t] : [];
3614
+ }
3444
3615
  const discriminant = b * b - 4 * a * c;
3445
3616
  if (discriminant < 0)
3446
3617
  return [];
@@ -3452,30 +3623,23 @@ class CubicBezierCurve extends Curve {
3452
3623
  getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
3453
3624
  const { p1, cp1, cp2, p2 } = this;
3454
3625
  const dxRoots = this._solveQuadratic(
3455
- 3 * (cp1.x - p1.x),
3456
- 6 * (cp2.x - cp1.x),
3457
- 3 * (p2.x - cp2.x)
3626
+ 3 * (-p1.x + 3 * cp1.x - 3 * cp2.x + p2.x),
3627
+ 6 * (p1.x - 2 * cp1.x + cp2.x),
3628
+ 3 * (cp1.x - p1.x)
3458
3629
  );
3459
3630
  const dyRoots = this._solveQuadratic(
3460
- 3 * (cp1.y - p1.y),
3461
- 6 * (cp2.y - cp1.y),
3462
- 3 * (p2.y - cp2.y)
3631
+ 3 * (-p1.y + 3 * cp1.y - 3 * cp2.y + p2.y),
3632
+ 6 * (p1.y - 2 * cp1.y + cp2.y),
3633
+ 3 * (cp1.y - p1.y)
3463
3634
  );
3464
3635
  const tValues = [0, 1, ...dxRoots, ...dyRoots];
3465
- const samplePoints = (tValues2, precision) => {
3466
- for (const t of tValues2) {
3467
- for (let i = 0; i <= precision; i++) {
3468
- const delta = i / precision - 0.5;
3469
- const refinedT = Math.min(1, Math.max(0, t + delta));
3470
- const point = this.getPoint(refinedT);
3471
- min.x = Math.min(min.x, point.x);
3472
- min.y = Math.min(min.y, point.y);
3473
- max.x = Math.max(max.x, point.x);
3474
- max.y = Math.max(max.y, point.y);
3475
- }
3476
- }
3477
- };
3478
- samplePoints(tValues, 10);
3636
+ for (const t of tValues) {
3637
+ const point = this.getPoint(t);
3638
+ min.x = Math.min(min.x, point.x);
3639
+ min.y = Math.min(min.y, point.y);
3640
+ max.x = Math.max(max.x, point.x);
3641
+ max.y = Math.max(max.y, point.y);
3642
+ }
3479
3643
  return { min: min.finite(), max: max.finite() };
3480
3644
  }
3481
3645
  toCommands() {
@@ -3528,11 +3692,11 @@ class EllipseCurve extends RoundCurve {
3528
3692
  }
3529
3693
  }
3530
3694
 
3531
- class PloygonCurve extends CompositeCurve {
3695
+ class PolygonCurve extends CompositeCurve {
3532
3696
  //
3533
3697
  }
3534
3698
 
3535
- class EquilateralPloygonCurve extends PloygonCurve {
3699
+ class EquilateralPolygonCurve extends PolygonCurve {
3536
3700
  constructor(cx = 0, cy = 0, radius = 1, sideCount = 3) {
3537
3701
  super();
3538
3702
  this.cx = cx;
@@ -3615,14 +3779,25 @@ class QuadraticBezierCurve extends Curve {
3615
3779
  }
3616
3780
  getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
3617
3781
  const { p1, cp, p2 } = this;
3618
- const x1 = 0.5 * (p1.x + cp.x);
3619
- const y1 = 0.5 * (p1.y + cp.y);
3620
- const x2 = 0.5 * (p1.x + p2.x);
3621
- const y2 = 0.5 * (p1.y + p2.y);
3622
- min.x = Math.min(min.x, p1.x, p2.x, x1, x2);
3623
- min.y = Math.min(min.y, p1.y, p2.y, y1, y2);
3624
- max.x = Math.max(max.x, p1.x, p2.x, x1, x2);
3625
- max.y = Math.max(max.y, p1.y, p2.y, y1, y2);
3782
+ const extrema = (a, b, c) => {
3783
+ const denom = a - 2 * b + c;
3784
+ if (Math.abs(denom) < 1e-12)
3785
+ return null;
3786
+ const t = (a - b) / denom;
3787
+ return t > 0 && t < 1 ? t : null;
3788
+ };
3789
+ const tx = extrema(p1.x, cp.x, p2.x);
3790
+ const ty = extrema(p1.y, cp.y, p2.y);
3791
+ const xs = [p1.x, p2.x];
3792
+ const ys = [p1.y, p2.y];
3793
+ if (tx !== null)
3794
+ xs.push(quadraticBezier(tx, p1.x, cp.x, p2.x));
3795
+ if (ty !== null)
3796
+ ys.push(quadraticBezier(ty, p1.y, cp.y, p2.y));
3797
+ min.x = Math.min(min.x, ...xs);
3798
+ min.y = Math.min(min.y, ...ys);
3799
+ max.x = Math.max(max.x, ...xs);
3800
+ max.y = Math.max(max.y, ...ys);
3626
3801
  return { min: min.finite(), max: max.finite() };
3627
3802
  }
3628
3803
  toCommands() {
@@ -3647,7 +3822,7 @@ class QuadraticBezierCurve extends Curve {
3647
3822
  }
3648
3823
  }
3649
3824
 
3650
- class RectangleCurve extends PloygonCurve {
3825
+ class RectangleCurve extends PolygonCurve {
3651
3826
  constructor(x = 0, y = 0, width = 0, height = 0) {
3652
3827
  super();
3653
3828
  this.x = x;
@@ -3825,6 +4000,17 @@ class CurvePath extends CompositeCurve {
3825
4000
  super.getFillVertices(options)
3826
4001
  );
3827
4002
  }
4003
+ /**
4004
+ * Same as {@link Curve.isPointInStroke}, but `closed` defaults to this sub-path's actual
4005
+ * closed-ness: explicitly `autoClose`, or geometrically closed (first vertex === last).
4006
+ */
4007
+ isPointInStroke(point, options = {}) {
4008
+ const { strokeWidth = 1, tolerance = 0 } = options;
4009
+ const vertices = this.getAdaptiveVertices();
4010
+ const len = vertices.length;
4011
+ const closed = options.closed ?? (this.autoClose || len >= 6 && vertices[0] === vertices[len - 2] && vertices[1] === vertices[len - 1]);
4012
+ return pointToPolylineDistance(point, vertices, closed) <= strokeWidth / 2 + tolerance;
4013
+ }
3828
4014
  _setCurrentPoint(point) {
3829
4015
  this.currentPoint = new Vector2(point.x, point.y);
3830
4016
  if (!this.startPoint) {
@@ -4055,13 +4241,11 @@ class Path2D extends CompositeCurve {
4055
4241
  return this;
4056
4242
  }
4057
4243
  moveTo(x, y) {
4058
- if (!this.currentCurve.currentPoint?.equals({ x, y })) {
4059
- if (this.currentCurve.curves.length) {
4060
- this.currentCurve = new CurvePath();
4061
- this.curves.push(this.currentCurve);
4062
- }
4063
- this.currentCurve.moveTo(x, y);
4244
+ if (this.currentCurve.curves.length) {
4245
+ this.currentCurve = new CurvePath();
4246
+ this.curves.push(this.currentCurve);
4064
4247
  }
4248
+ this.currentCurve.moveTo(x, y);
4065
4249
  return this;
4066
4250
  }
4067
4251
  lineTo(x, y) {
@@ -4190,6 +4374,40 @@ class Path2D extends CompositeCurve {
4190
4374
  });
4191
4375
  return this;
4192
4376
  }
4377
+ /**
4378
+ * Test whether a point lies inside the filled area of this path.
4379
+ *
4380
+ * Each sub-path ({@link CurvePath}) is sampled into its own ring and all rings are
4381
+ * evaluated together via {@link pointInPolygons}, so holes (donut / hollow shapes) are
4382
+ * honored. This is purely geometric and ignores `style.fill` — for the `fill: 'none'`
4383
+ * fallback, gate the call upstream (see {@link Path2DSet.hitTest}).
4384
+ *
4385
+ * Defaults `fillRule` to `style.fillRule`, then `'nonzero'` (matching SVG/Canvas).
4386
+ */
4387
+ isPointInFill(point, options = {}) {
4388
+ const fillRule = options.fillRule ?? this.style.fillRule ?? "nonzero";
4389
+ return pointInPolygons(
4390
+ point,
4391
+ this.curves.map((curve) => curve.getAdaptiveVertices()),
4392
+ fillRule
4393
+ );
4394
+ }
4395
+ /**
4396
+ * Test whether a point lies on this path's stroke. A hit on any sub-path counts.
4397
+ *
4398
+ * Defaults `strokeWidth` to this path's own {@link strokeWidth} (which is `0` when
4399
+ * `style.stroke` is `'none'`). Each sub-path infers its own closed-ness unless `closed`
4400
+ * is given explicitly.
4401
+ */
4402
+ isPointInStroke(point, options = {}) {
4403
+ const strokeWidth = options.strokeWidth ?? this.strokeWidth;
4404
+ const { tolerance = 0, closed } = options;
4405
+ return this.curves.some((curve) => curve.isPointInStroke(point, {
4406
+ strokeWidth,
4407
+ tolerance,
4408
+ closed
4409
+ }));
4410
+ }
4193
4411
  getMinMax(min = Vector2.MAX, max = Vector2.MIN, withStyle = true) {
4194
4412
  const strokeWidth = this.strokeWidth;
4195
4413
  this.curves.forEach((curve) => {
@@ -4367,6 +4585,44 @@ class Path2DSet {
4367
4585
  this.paths = paths;
4368
4586
  this.viewBox = viewBox;
4369
4587
  }
4588
+ /**
4589
+ * Test whether a point lies inside the filled area of any path in this set.
4590
+ * Purely geometric (ignores `fill: 'none'`); use {@link hitTest} for style-aware hits.
4591
+ */
4592
+ isPointInFill(point, options = {}) {
4593
+ return this.paths.some((path) => path.isPointInFill(point, options));
4594
+ }
4595
+ /**
4596
+ * Concise PathKit-style fill containment test across the whole set; shorthand for
4597
+ * {@link isPointInFill} with a `{ x, y }` point.
4598
+ */
4599
+ contains(x, y, options = {}) {
4600
+ return this.isPointInFill({ x, y }, options);
4601
+ }
4602
+ /**
4603
+ * Find the topmost path hit by a point, or `undefined` if none.
4604
+ *
4605
+ * Paths are tested top-to-bottom (last drawn first). For each path a fill hit is checked
4606
+ * first (skipped when `style.fill` is `'none'`), then — if `stroke` is enabled — a stroke
4607
+ * hit (skipped when `style.stroke` is `'none'`). This honors the "fill: none falls back to
4608
+ * stroke" rule; the coordinate space of `point` must match the paths (no scaling assumed).
4609
+ *
4610
+ * Options: `stroke` (also test strokes, default `true`), `tolerance` (extra stroke hit slack
4611
+ * in path units, default `0`), and `fillRule` (overrides each path's own fill rule).
4612
+ */
4613
+ hitTest(point, options = {}) {
4614
+ const { stroke = true, tolerance, fillRule } = options;
4615
+ for (let i = this.paths.length - 1; i >= 0; i--) {
4616
+ const path = this.paths[i];
4617
+ if ((path.style.fill ?? "#000") !== "none" && path.isPointInFill(point, { fillRule })) {
4618
+ return path;
4619
+ }
4620
+ if (stroke && (path.style.stroke ?? "none") !== "none" && path.isPointInStroke(point, { tolerance })) {
4621
+ return path;
4622
+ }
4623
+ }
4624
+ return void 0;
4625
+ }
4370
4626
  getBoundingBox(withStyle = true) {
4371
4627
  if (!this.paths.length) {
4372
4628
  return void 0;
@@ -4529,14 +4785,14 @@ exports.CubicBezierCurve = CubicBezierCurve;
4529
4785
  exports.Curve = Curve;
4530
4786
  exports.CurvePath = CurvePath;
4531
4787
  exports.EllipseCurve = EllipseCurve;
4532
- exports.EquilateralPloygonCurve = EquilateralPloygonCurve;
4788
+ exports.EquilateralPolygonCurve = EquilateralPolygonCurve;
4533
4789
  exports.FFDControlGrid = FFDControlGrid;
4534
4790
  exports.LineCurve = LineCurve;
4535
4791
  exports.PI = PI;
4536
4792
  exports.PI_2 = PI_2;
4537
4793
  exports.Path2D = Path2D;
4538
4794
  exports.Path2DSet = Path2DSet;
4539
- exports.PloygonCurve = PloygonCurve;
4795
+ exports.PolygonCurve = PolygonCurve;
4540
4796
  exports.QuadraticBezierCurve = QuadraticBezierCurve;
4541
4797
  exports.RectangleCurve = RectangleCurve;
4542
4798
  exports.RoundRectangleCurve = RoundRectangleCurve;
@@ -4558,6 +4814,10 @@ exports.parseCssArg = parseCssArg;
4558
4814
  exports.parseCssArgs = parseCssArgs;
4559
4815
  exports.parseCssFunctions = parseCssFunctions;
4560
4816
  exports.parsePathDataArgs = parsePathDataArgs;
4817
+ exports.pointInPolygon = pointInPolygon;
4818
+ exports.pointInPolygons = pointInPolygons;
4819
+ exports.pointToPolylineDistance = pointToPolylineDistance;
4820
+ exports.pointToSegmentDistance = pointToSegmentDistance;
4561
4821
  exports.quadraticBezier = quadraticBezier;
4562
4822
  exports.setCanvasContext = setCanvasContext;
4563
4823
  exports.strokeTriangulate = strokeTriangulate;