modern-path2d 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/index.cjs +478 -57
- package/dist/index.d.cts +185 -4
- package/dist/index.d.mts +185 -4
- package/dist/index.d.ts +185 -4
- package/dist/index.js +2 -2
- package/dist/index.mjs +475 -58
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -126,7 +126,10 @@ class Vector2 {
|
|
|
126
126
|
return this.set(this._x * x, this._y * y);
|
|
127
127
|
}
|
|
128
128
|
divide(x = 0, y = x) {
|
|
129
|
-
return this.set(
|
|
129
|
+
return this.set(
|
|
130
|
+
x === 0 ? this._x : this._x / x,
|
|
131
|
+
y === 0 ? this._y : this._y / y
|
|
132
|
+
);
|
|
130
133
|
}
|
|
131
134
|
cross(p) {
|
|
132
135
|
return this._x * p.y - this._y * p.x;
|
|
@@ -319,17 +322,11 @@ function getIntersectionPoint(p1, p2, q1, q2) {
|
|
|
319
322
|
const q1p1 = q1.clone().sub(p1);
|
|
320
323
|
const crossRS = r.cross(s);
|
|
321
324
|
if (crossRS === 0) {
|
|
322
|
-
return
|
|
323
|
-
(p1.x + q1.x) / 2,
|
|
324
|
-
(p1.y + q1.y) / 2
|
|
325
|
-
);
|
|
325
|
+
return null;
|
|
326
326
|
}
|
|
327
327
|
const t = q1p1.cross(s) / crossRS;
|
|
328
328
|
if (Math.abs(t) > 1) {
|
|
329
|
-
return
|
|
330
|
-
(p1.x + q1.x) / 2,
|
|
331
|
-
(p1.y + q1.y) / 2
|
|
332
|
-
);
|
|
329
|
+
return null;
|
|
333
330
|
}
|
|
334
331
|
return new Vector2(
|
|
335
332
|
p1.x + t * r.x,
|
|
@@ -598,7 +595,7 @@ function getDirectedArea(vertices) {
|
|
|
598
595
|
function cross(ax, ay, bx, by, cx, cy) {
|
|
599
596
|
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
600
597
|
}
|
|
601
|
-
function windingNumber(px, py, polygon) {
|
|
598
|
+
function windingNumber$1(px, py, polygon) {
|
|
602
599
|
const polygonLen = polygon.length;
|
|
603
600
|
let wn = 0;
|
|
604
601
|
for (let i = 0, j = polygonLen - 2; i < polygonLen; j = i, i += 2) {
|
|
@@ -621,11 +618,16 @@ function distance(p1, p2) {
|
|
|
621
618
|
const dy = p2[1] - p1[1];
|
|
622
619
|
return Math.sqrt(dx * dx + dy * dy);
|
|
623
620
|
}
|
|
621
|
+
function aabbIntersects(a, b) {
|
|
622
|
+
return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY;
|
|
623
|
+
}
|
|
624
624
|
function nonzeroFillRule(paths) {
|
|
625
625
|
const results = paths.map((_, i) => ({ index: i }));
|
|
626
|
-
const
|
|
626
|
+
const bboxes = [];
|
|
627
|
+
const testPointsGroups = paths.map((path, pathIndex) => {
|
|
627
628
|
const len = path.length;
|
|
628
629
|
if (!len) {
|
|
630
|
+
bboxes[pathIndex] = null;
|
|
629
631
|
return [];
|
|
630
632
|
}
|
|
631
633
|
let xMinYAuto = [Number.MAX_SAFE_INTEGER, 0];
|
|
@@ -648,6 +650,12 @@ function nonzeroFillRule(paths) {
|
|
|
648
650
|
xAutoYMax = [x, y];
|
|
649
651
|
}
|
|
650
652
|
}
|
|
653
|
+
bboxes[pathIndex] = {
|
|
654
|
+
minX: xMinYAuto[0],
|
|
655
|
+
minY: xAutoYMin[1],
|
|
656
|
+
maxX: xMaxYAuto[0],
|
|
657
|
+
maxY: xAutoYMax[1]
|
|
658
|
+
};
|
|
651
659
|
const mid = [
|
|
652
660
|
(xMinYAuto[0] + xMaxYAuto[0]) / 2,
|
|
653
661
|
(xAutoYMin[1] + xAutoYMax[1]) / 2
|
|
@@ -696,14 +704,19 @@ function nonzeroFillRule(paths) {
|
|
|
696
704
|
for (let i = 0, len = paths.length; i < len; i++) {
|
|
697
705
|
const _results = [];
|
|
698
706
|
const testPoints = testPointsGroups[i];
|
|
707
|
+
const boxI = bboxes[i];
|
|
699
708
|
for (let j = 0; j < len; j++) {
|
|
700
709
|
if (i === j)
|
|
701
710
|
continue;
|
|
711
|
+
const boxJ = bboxes[j];
|
|
712
|
+
if (!boxI || !boxJ || !aabbIntersects(boxI, boxJ)) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
702
715
|
const wnMap = {};
|
|
703
716
|
const wnList = [];
|
|
704
717
|
for (let p = 0, pLen = testPoints.length; p < pLen; p++) {
|
|
705
718
|
const [x, y] = testPoints[p];
|
|
706
|
-
const winding = windingNumber(x, y, paths[j]);
|
|
719
|
+
const winding = windingNumber$1(x, y, paths[j]);
|
|
707
720
|
wnMap[winding] = (wnMap[winding] ?? 0) + 1;
|
|
708
721
|
wnList.push(winding);
|
|
709
722
|
}
|
|
@@ -726,6 +739,120 @@ function nonzeroFillRule(paths) {
|
|
|
726
739
|
return results;
|
|
727
740
|
}
|
|
728
741
|
|
|
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
|
+
|
|
729
856
|
function quadraticBezierP0(t, p) {
|
|
730
857
|
const k = 1 - t;
|
|
731
858
|
return k * k * p;
|
|
@@ -2539,6 +2666,41 @@ function svgToPath2DSet(svg) {
|
|
|
2539
2666
|
class Curve {
|
|
2540
2667
|
arcLengthDivision = 200;
|
|
2541
2668
|
_lengths = [];
|
|
2669
|
+
_adaptiveCache;
|
|
2670
|
+
/**
|
|
2671
|
+
* Parent composite, set lazily when a composite caches its children. Lets
|
|
2672
|
+
* {@link invalidate} propagate up so an ancestor's caches refresh too.
|
|
2673
|
+
*/
|
|
2674
|
+
_owner;
|
|
2675
|
+
_invalidating = false;
|
|
2676
|
+
/**
|
|
2677
|
+
* Drop cached arc lengths and the cached sampled outline used by hit testing, then
|
|
2678
|
+
* bubble up to {@link _owner}. Called automatically by {@link applyTransform} and the
|
|
2679
|
+
* `Path2D` mutators; call it manually after mutating control-point coordinates in place —
|
|
2680
|
+
* the caches cannot observe such mutations.
|
|
2681
|
+
*/
|
|
2682
|
+
invalidate() {
|
|
2683
|
+
if (this._invalidating) {
|
|
2684
|
+
return this;
|
|
2685
|
+
}
|
|
2686
|
+
this._invalidating = true;
|
|
2687
|
+
this._invalidateSelf();
|
|
2688
|
+
this._owner?.invalidate();
|
|
2689
|
+
this._invalidating = false;
|
|
2690
|
+
return this;
|
|
2691
|
+
}
|
|
2692
|
+
/** Clears this curve's own caches. Composites also clear their children (see override). */
|
|
2693
|
+
_invalidateSelf() {
|
|
2694
|
+
this._lengths.length = 0;
|
|
2695
|
+
this._adaptiveCache = void 0;
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Sampled outline cached for repeated hit tests (read-only — do not mutate the result).
|
|
2699
|
+
* Invalidated by {@link invalidate}.
|
|
2700
|
+
*/
|
|
2701
|
+
_getCachedAdaptiveVertices() {
|
|
2702
|
+
return this._adaptiveCache ??= this.getAdaptiveVertices();
|
|
2703
|
+
}
|
|
2542
2704
|
getPointAt(u, output = new Vector2()) {
|
|
2543
2705
|
return this.getPoint(this.getUToTMapping(u), output);
|
|
2544
2706
|
}
|
|
@@ -2557,6 +2719,7 @@ class Curve {
|
|
|
2557
2719
|
transform.apply(p, p);
|
|
2558
2720
|
}
|
|
2559
2721
|
});
|
|
2722
|
+
this.invalidate();
|
|
2560
2723
|
return this;
|
|
2561
2724
|
}
|
|
2562
2725
|
getUnevenVertices(count = 5, output = []) {
|
|
@@ -2701,18 +2864,65 @@ class Curve {
|
|
|
2701
2864
|
return mid;
|
|
2702
2865
|
}
|
|
2703
2866
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
2704
|
-
const
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2867
|
+
const vertices = this.getAdaptiveVertices();
|
|
2868
|
+
let minX = min.x;
|
|
2869
|
+
let minY = min.y;
|
|
2870
|
+
let maxX = max.x;
|
|
2871
|
+
let maxY = max.y;
|
|
2872
|
+
for (let i = 0, len = vertices.length; i < len; i += 2) {
|
|
2873
|
+
const x = vertices[i];
|
|
2874
|
+
const y = vertices[i + 1];
|
|
2875
|
+
if (x < minX)
|
|
2876
|
+
minX = x;
|
|
2877
|
+
if (y < minY)
|
|
2878
|
+
minY = y;
|
|
2879
|
+
if (x > maxX)
|
|
2880
|
+
maxX = x;
|
|
2881
|
+
if (y > maxY)
|
|
2882
|
+
maxY = y;
|
|
2709
2883
|
}
|
|
2884
|
+
min.set(minX, minY);
|
|
2885
|
+
max.set(maxX, maxY);
|
|
2710
2886
|
return { min: min.finite(), max: max.finite() };
|
|
2711
2887
|
}
|
|
2712
2888
|
getBoundingBox() {
|
|
2713
2889
|
const { min, max } = this.getMinMax();
|
|
2714
2890
|
return new BoundingBox(min.x, min.y, max.x - min.x, max.y - min.y);
|
|
2715
2891
|
}
|
|
2892
|
+
/**
|
|
2893
|
+
* Test whether a point lies inside the area enclosed by this curve.
|
|
2894
|
+
*
|
|
2895
|
+
* The curve is sampled via {@link getAdaptiveVertices} into a single implicitly closed
|
|
2896
|
+
* ring. This is purely geometric (it ignores any `fill`/`stroke` style), mirroring
|
|
2897
|
+
* `CanvasRenderingContext2D.isPointInPath`.
|
|
2898
|
+
*
|
|
2899
|
+
* Composites that hold multiple sub-paths (e.g. {@link Path2D}) override this so holes
|
|
2900
|
+
* are honored — a single `Curve` is always one ring.
|
|
2901
|
+
*/
|
|
2902
|
+
isPointInFill(point, options = {}) {
|
|
2903
|
+
return pointInPolygon(point, this._getCachedAdaptiveVertices(), options.fillRule);
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Test whether a point lies on this curve's stroke, i.e. within `strokeWidth / 2 + tolerance`
|
|
2907
|
+
* of the sampled outline. The point must be in the same coordinate space as the curve.
|
|
2908
|
+
*
|
|
2909
|
+
* Options: `strokeWidth` (path units, default `1`), `tolerance` (extra hit slack in path
|
|
2910
|
+
* units, default `0` — useful for thin strokes; no coordinate scaling is assumed, so convert
|
|
2911
|
+
* pixel tolerance to path units upstream if your path is normalized), and `closed` (whether
|
|
2912
|
+
* to include the closing edge from the last vertex back to the first).
|
|
2913
|
+
*/
|
|
2914
|
+
isPointInStroke(point, options = {}) {
|
|
2915
|
+
const { strokeWidth = 1, tolerance = 0, closed = false } = options;
|
|
2916
|
+
const distance = pointToPolylineDistance(point, this._getCachedAdaptiveVertices(), closed);
|
|
2917
|
+
return distance <= strokeWidth / 2 + tolerance;
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Concise PathKit-style fill containment test: `contains(x, y)` is shorthand for
|
|
2921
|
+
* {@link isPointInFill} with a `{ x, y }` point.
|
|
2922
|
+
*/
|
|
2923
|
+
contains(x, y, options = {}) {
|
|
2924
|
+
return this.isPointInFill({ x, y }, options);
|
|
2925
|
+
}
|
|
2716
2926
|
getFillVertices(_options) {
|
|
2717
2927
|
return this.getAdaptiveVertices();
|
|
2718
2928
|
}
|
|
@@ -2847,6 +3057,64 @@ class RoundCurve extends Curve {
|
|
|
2847
3057
|
}
|
|
2848
3058
|
return output.set(_x, _y);
|
|
2849
3059
|
}
|
|
3060
|
+
/**
|
|
3061
|
+
* Point on the ellipse at an absolute angle (mirrors {@link getPoint}'s parameterization,
|
|
3062
|
+
* ignoring `_diff`).
|
|
3063
|
+
*/
|
|
3064
|
+
_pointAtAngle(angle, output) {
|
|
3065
|
+
let x = this.cx + this.rx * Math.cos(angle);
|
|
3066
|
+
let y = this.cy + this.ry * Math.sin(angle);
|
|
3067
|
+
if (this.rotate !== 0) {
|
|
3068
|
+
const cos = Math.cos(this.rotate);
|
|
3069
|
+
const sin = Math.sin(this.rotate);
|
|
3070
|
+
const tx = x - this.cx;
|
|
3071
|
+
const ty = y - this.cy;
|
|
3072
|
+
x = tx * cos - ty * sin + this.cx;
|
|
3073
|
+
y = tx * sin + ty * cos + this.cy;
|
|
3074
|
+
}
|
|
3075
|
+
return output.set(x, y);
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* Analytical bounds of the (elliptical) arc: the start/end points plus the per-axis
|
|
3079
|
+
* extrema angles that fall within the swept interval. Matches {@link getPoint}, so it is
|
|
3080
|
+
* exact for `ArcCurve`/`EllipseCurve`. The `_diff` offset (used only by the legacy
|
|
3081
|
+
* `_getAdaptiveVerticesByCircle` path) is intentionally ignored here.
|
|
3082
|
+
*/
|
|
3083
|
+
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
3084
|
+
const { startAngle, rotate } = this;
|
|
3085
|
+
const delta = this._getDeltaAngle();
|
|
3086
|
+
const cosT = Math.cos(rotate);
|
|
3087
|
+
const sinT = Math.sin(rotate);
|
|
3088
|
+
const p = tempV2;
|
|
3089
|
+
let minX = min.x;
|
|
3090
|
+
let minY = min.y;
|
|
3091
|
+
let maxX = max.x;
|
|
3092
|
+
let maxY = max.y;
|
|
3093
|
+
const consider = (angle) => {
|
|
3094
|
+
this._pointAtAngle(angle, p);
|
|
3095
|
+
if (p.x < minX)
|
|
3096
|
+
minX = p.x;
|
|
3097
|
+
if (p.y < minY)
|
|
3098
|
+
minY = p.y;
|
|
3099
|
+
if (p.x > maxX)
|
|
3100
|
+
maxX = p.x;
|
|
3101
|
+
if (p.y > maxY)
|
|
3102
|
+
maxY = p.y;
|
|
3103
|
+
};
|
|
3104
|
+
consider(startAngle);
|
|
3105
|
+
consider(startAngle + delta);
|
|
3106
|
+
const ax = Math.atan2(-this.ry * sinT, this.rx * cosT);
|
|
3107
|
+
const ay = Math.atan2(this.ry * cosT, this.rx * sinT);
|
|
3108
|
+
const bases = [ax, ax + Math.PI, ay, ay + Math.PI];
|
|
3109
|
+
for (let i = 0; i < 4; i++) {
|
|
3110
|
+
if (angleInSweep(bases[i], startAngle, delta)) {
|
|
3111
|
+
consider(bases[i]);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
min.set(minX, minY);
|
|
3115
|
+
max.set(maxX, maxY);
|
|
3116
|
+
return { min: min.finite(), max: max.finite() };
|
|
3117
|
+
}
|
|
2850
3118
|
toCommands() {
|
|
2851
3119
|
const { cx, cy, rx, ry, startAngle, endAngle, clockwise, rotate } = this;
|
|
2852
3120
|
const startX = cx + rx * Math.cos(startAngle) * Math.cos(rotate) - ry * Math.sin(startAngle) * Math.sin(rotate);
|
|
@@ -2897,6 +3165,7 @@ class RoundCurve extends Curve {
|
|
|
2897
3165
|
} else {
|
|
2898
3166
|
transfEllipseNoSkew(this, transform);
|
|
2899
3167
|
}
|
|
3168
|
+
this.invalidate();
|
|
2900
3169
|
return this;
|
|
2901
3170
|
}
|
|
2902
3171
|
getControlPointRefs() {
|
|
@@ -3037,6 +3306,23 @@ class RoundCurve extends Curve {
|
|
|
3037
3306
|
return this;
|
|
3038
3307
|
}
|
|
3039
3308
|
}
|
|
3309
|
+
function angleInSweep(a, start, delta) {
|
|
3310
|
+
const PI_2 = Math.PI * 2;
|
|
3311
|
+
const eps = 1e-9;
|
|
3312
|
+
if (Math.abs(delta) >= PI_2 - eps) {
|
|
3313
|
+
return true;
|
|
3314
|
+
}
|
|
3315
|
+
let off = (a - start) % PI_2;
|
|
3316
|
+
if (delta >= 0) {
|
|
3317
|
+
if (off < -eps)
|
|
3318
|
+
off += PI_2;
|
|
3319
|
+
return off >= -eps && off <= delta + eps;
|
|
3320
|
+
}
|
|
3321
|
+
if (off > eps) {
|
|
3322
|
+
off -= PI_2;
|
|
3323
|
+
}
|
|
3324
|
+
return off <= eps && off >= delta - eps;
|
|
3325
|
+
}
|
|
3040
3326
|
function transfEllipseGeneric(curve, m) {
|
|
3041
3327
|
const a = curve.rx;
|
|
3042
3328
|
const b = curve.ry;
|
|
@@ -3275,6 +3561,22 @@ class CompositeCurve extends Curve {
|
|
|
3275
3561
|
super();
|
|
3276
3562
|
this.curves = curves;
|
|
3277
3563
|
}
|
|
3564
|
+
_adaptiveCacheLen = -1;
|
|
3565
|
+
_invalidateSelf() {
|
|
3566
|
+
super._invalidateSelf();
|
|
3567
|
+
this._adaptiveCacheLen = -1;
|
|
3568
|
+
this.curves.forEach((curve) => curve.invalidate());
|
|
3569
|
+
}
|
|
3570
|
+
_getCachedAdaptiveVertices() {
|
|
3571
|
+
if (!this._adaptiveCache || this._adaptiveCacheLen !== this.curves.length) {
|
|
3572
|
+
this.curves.forEach((curve) => {
|
|
3573
|
+
curve._owner = this;
|
|
3574
|
+
});
|
|
3575
|
+
this._adaptiveCache = this.getAdaptiveVertices();
|
|
3576
|
+
this._adaptiveCacheLen = this.curves.length;
|
|
3577
|
+
}
|
|
3578
|
+
return this._adaptiveCache;
|
|
3579
|
+
}
|
|
3278
3580
|
getFlatCurves() {
|
|
3279
3581
|
return this.curves.flatMap((curve) => {
|
|
3280
3582
|
if (curve instanceof CompositeCurve) {
|
|
@@ -3320,6 +3622,7 @@ class CompositeCurve extends Curve {
|
|
|
3320
3622
|
updateLengths() {
|
|
3321
3623
|
const lengths = [];
|
|
3322
3624
|
for (let i = 0, sum = 0, len = this.curves.length; i < len; i++) {
|
|
3625
|
+
this.curves[i]._owner = this;
|
|
3323
3626
|
sum += this.curves[i].getLength();
|
|
3324
3627
|
lengths.push(sum);
|
|
3325
3628
|
}
|
|
@@ -3388,7 +3691,10 @@ class CompositeCurve extends Curve {
|
|
|
3388
3691
|
}
|
|
3389
3692
|
}
|
|
3390
3693
|
applyTransform(transform) {
|
|
3694
|
+
this._invalidating = true;
|
|
3391
3695
|
this.curves.forEach((curve) => curve.applyTransform(transform));
|
|
3696
|
+
this._invalidating = false;
|
|
3697
|
+
this.invalidate();
|
|
3392
3698
|
return this;
|
|
3393
3699
|
}
|
|
3394
3700
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
@@ -3727,7 +4033,7 @@ class RectangleCurve extends PolygonCurve {
|
|
|
3727
4033
|
}
|
|
3728
4034
|
}
|
|
3729
4035
|
|
|
3730
|
-
class RoundRectangleCurve extends
|
|
4036
|
+
class RoundRectangleCurve extends CompositeCurve {
|
|
3731
4037
|
constructor(x = 0, y = 0, width = 1, height = 1, radius = 1) {
|
|
3732
4038
|
super();
|
|
3733
4039
|
this.x = x;
|
|
@@ -3738,25 +4044,53 @@ class RoundRectangleCurve extends RoundCurve {
|
|
|
3738
4044
|
this.update();
|
|
3739
4045
|
}
|
|
3740
4046
|
update() {
|
|
3741
|
-
const { x, y, width, height
|
|
3742
|
-
const
|
|
3743
|
-
const
|
|
3744
|
-
const
|
|
3745
|
-
const
|
|
3746
|
-
const
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
4047
|
+
const { x, y, width, height } = this;
|
|
4048
|
+
const r = Math.max(0, Math.min(this.radius, Math.abs(width) / 2, Math.abs(height) / 2));
|
|
4049
|
+
const x0 = x;
|
|
4050
|
+
const x1 = x + r;
|
|
4051
|
+
const x2 = x + width - r;
|
|
4052
|
+
const x3 = x + width;
|
|
4053
|
+
const y0 = y;
|
|
4054
|
+
const y1 = y + r;
|
|
4055
|
+
const y2 = y + height - r;
|
|
4056
|
+
const y3 = y + height;
|
|
4057
|
+
if (r <= 0) {
|
|
4058
|
+
this.curves = [
|
|
4059
|
+
LineCurve.from(x0, y0, x3, y0),
|
|
4060
|
+
LineCurve.from(x3, y0, x3, y3),
|
|
4061
|
+
LineCurve.from(x3, y3, x0, y3),
|
|
4062
|
+
LineCurve.from(x0, y3, x0, y0)
|
|
4063
|
+
];
|
|
4064
|
+
} else {
|
|
4065
|
+
const HALF_PI = Math.PI / 2;
|
|
4066
|
+
this.curves = [
|
|
4067
|
+
LineCurve.from(x1, y0, x2, y0),
|
|
4068
|
+
// top edge
|
|
4069
|
+
new ArcCurve(x2, y1, r, -HALF_PI, 0, true),
|
|
4070
|
+
// top-right corner
|
|
4071
|
+
LineCurve.from(x3, y1, x3, y2),
|
|
4072
|
+
// right edge
|
|
4073
|
+
new ArcCurve(x2, y2, r, 0, HALF_PI, true),
|
|
4074
|
+
// bottom-right corner
|
|
4075
|
+
LineCurve.from(x2, y3, x1, y3),
|
|
4076
|
+
// bottom edge
|
|
4077
|
+
new ArcCurve(x1, y2, r, HALF_PI, Math.PI, true),
|
|
4078
|
+
// bottom-left corner
|
|
4079
|
+
LineCurve.from(x0, y2, x0, y1),
|
|
4080
|
+
// left edge
|
|
4081
|
+
new ArcCurve(x1, y1, r, Math.PI, Math.PI * 1.5, true)
|
|
4082
|
+
// top-left corner
|
|
4083
|
+
];
|
|
4084
|
+
}
|
|
4085
|
+
this.invalidate();
|
|
3751
4086
|
return this;
|
|
3752
4087
|
}
|
|
3753
4088
|
drawTo(ctx) {
|
|
3754
|
-
|
|
3755
|
-
ctx.roundRect(x, y, width, height, radius);
|
|
4089
|
+
ctx.roundRect(this.x, this.y, this.width, this.height, this.radius);
|
|
3756
4090
|
return this;
|
|
3757
4091
|
}
|
|
3758
4092
|
copyFrom(source) {
|
|
3759
|
-
|
|
4093
|
+
this.arcLengthDivision = source.arcLengthDivision;
|
|
3760
4094
|
this.x = source.x;
|
|
3761
4095
|
this.y = source.y;
|
|
3762
4096
|
this.width = source.width;
|
|
@@ -3852,6 +4186,17 @@ class CurvePath extends CompositeCurve {
|
|
|
3852
4186
|
super.getFillVertices(options)
|
|
3853
4187
|
);
|
|
3854
4188
|
}
|
|
4189
|
+
/**
|
|
4190
|
+
* Same as {@link Curve.isPointInStroke}, but `closed` defaults to this sub-path's actual
|
|
4191
|
+
* closed-ness: explicitly `autoClose`, or geometrically closed (first vertex === last).
|
|
4192
|
+
*/
|
|
4193
|
+
isPointInStroke(point, options = {}) {
|
|
4194
|
+
const { strokeWidth = 1, tolerance = 0 } = options;
|
|
4195
|
+
const vertices = this._getCachedAdaptiveVertices();
|
|
4196
|
+
const len = vertices.length;
|
|
4197
|
+
const closed = options.closed ?? (this.autoClose || len >= 6 && vertices[0] === vertices[len - 2] && vertices[1] === vertices[len - 1]);
|
|
4198
|
+
return pointToPolylineDistance(point, vertices, closed) <= strokeWidth / 2 + tolerance;
|
|
4199
|
+
}
|
|
3855
4200
|
_setCurrentPoint(point) {
|
|
3856
4201
|
this.currentPoint = new Vector2(point.x, point.y);
|
|
3857
4202
|
if (!this.startPoint) {
|
|
@@ -4025,6 +4370,8 @@ class CurvePath extends CompositeCurve {
|
|
|
4025
4370
|
|
|
4026
4371
|
class Path2D extends CompositeCurve {
|
|
4027
4372
|
_meta;
|
|
4373
|
+
_ringsCache;
|
|
4374
|
+
_ringsCacheLen = -1;
|
|
4028
4375
|
currentCurve = new CurvePath();
|
|
4029
4376
|
style;
|
|
4030
4377
|
get startPoint() {
|
|
@@ -4143,18 +4490,21 @@ class Path2D extends CompositeCurve {
|
|
|
4143
4490
|
this.getControlPointRefs().forEach((point) => {
|
|
4144
4491
|
point.scale(sx, sy, target);
|
|
4145
4492
|
});
|
|
4493
|
+
this.invalidate();
|
|
4146
4494
|
return this;
|
|
4147
4495
|
}
|
|
4148
4496
|
skew(ax, ay = 0, target = { x: 0, y: 0 }) {
|
|
4149
4497
|
this.getControlPointRefs().forEach((point) => {
|
|
4150
4498
|
point.skew(ax, ay, target);
|
|
4151
4499
|
});
|
|
4500
|
+
this.invalidate();
|
|
4152
4501
|
return this;
|
|
4153
4502
|
}
|
|
4154
4503
|
rotate(rad, target = { x: 0, y: 0 }) {
|
|
4155
4504
|
this.getControlPointRefs().forEach((point) => {
|
|
4156
4505
|
point.rotate(rad, target);
|
|
4157
4506
|
});
|
|
4507
|
+
this.invalidate();
|
|
4158
4508
|
return this;
|
|
4159
4509
|
}
|
|
4160
4510
|
bold(b) {
|
|
@@ -4213,38 +4563,67 @@ class Path2D extends CompositeCurve {
|
|
|
4213
4563
|
}
|
|
4214
4564
|
});
|
|
4215
4565
|
});
|
|
4566
|
+
this.invalidate();
|
|
4216
4567
|
return this;
|
|
4217
4568
|
}
|
|
4569
|
+
/**
|
|
4570
|
+
* Test whether a point lies inside the filled area of this path.
|
|
4571
|
+
*
|
|
4572
|
+
* Each sub-path ({@link CurvePath}) is sampled into its own ring and all rings are
|
|
4573
|
+
* evaluated together via {@link pointInPolygons}, so holes (donut / hollow shapes) are
|
|
4574
|
+
* honored. This is purely geometric and ignores `style.fill` — for the `fill: 'none'`
|
|
4575
|
+
* fallback, gate the call upstream (see {@link Path2DSet.hitTest}).
|
|
4576
|
+
*
|
|
4577
|
+
* Defaults `fillRule` to `style.fillRule`, then `'nonzero'` (matching SVG/Canvas).
|
|
4578
|
+
*/
|
|
4579
|
+
_invalidateSelf() {
|
|
4580
|
+
super._invalidateSelf();
|
|
4581
|
+
this._ringsCache = void 0;
|
|
4582
|
+
this._ringsCacheLen = -1;
|
|
4583
|
+
}
|
|
4584
|
+
/** Per-sub-path sampled rings, cached for repeated hit tests. */
|
|
4585
|
+
_getRings() {
|
|
4586
|
+
if (!this._ringsCache || this._ringsCacheLen !== this.curves.length) {
|
|
4587
|
+
this._ringsCache = this.curves.map((curve) => {
|
|
4588
|
+
curve._owner = this;
|
|
4589
|
+
return curve.getAdaptiveVertices();
|
|
4590
|
+
});
|
|
4591
|
+
this._ringsCacheLen = this.curves.length;
|
|
4592
|
+
}
|
|
4593
|
+
return this._ringsCache;
|
|
4594
|
+
}
|
|
4595
|
+
isPointInFill(point, options = {}) {
|
|
4596
|
+
const fillRule = options.fillRule ?? this.style.fillRule ?? "nonzero";
|
|
4597
|
+
return pointInPolygons(point, this._getRings(), fillRule);
|
|
4598
|
+
}
|
|
4599
|
+
/**
|
|
4600
|
+
* Test whether a point lies on this path's stroke. A hit on any sub-path counts.
|
|
4601
|
+
*
|
|
4602
|
+
* Defaults `strokeWidth` to this path's own {@link strokeWidth} (which is `0` when
|
|
4603
|
+
* `style.stroke` is `'none'`). Each sub-path infers its own closed-ness unless `closed`
|
|
4604
|
+
* is given explicitly.
|
|
4605
|
+
*/
|
|
4606
|
+
isPointInStroke(point, options = {}) {
|
|
4607
|
+
const strokeWidth = options.strokeWidth ?? this.strokeWidth;
|
|
4608
|
+
const { tolerance = 0, closed } = options;
|
|
4609
|
+
return this.curves.some((curve) => curve.isPointInStroke(point, {
|
|
4610
|
+
strokeWidth,
|
|
4611
|
+
tolerance,
|
|
4612
|
+
closed
|
|
4613
|
+
}));
|
|
4614
|
+
}
|
|
4218
4615
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN, withStyle = true) {
|
|
4219
|
-
const strokeWidth = this.strokeWidth;
|
|
4220
4616
|
this.curves.forEach((curve) => {
|
|
4221
4617
|
curve.getMinMax(min, max);
|
|
4222
|
-
if (withStyle) {
|
|
4223
|
-
if (strokeWidth > 1) {
|
|
4224
|
-
const halfStrokeWidth = strokeWidth / 2;
|
|
4225
|
-
const isClockwise = curve.isClockwise();
|
|
4226
|
-
const points = [];
|
|
4227
|
-
for (let t = 0; t <= 1; t += 1 / curve.arcLengthDivision) {
|
|
4228
|
-
const point = curve.getPoint(t);
|
|
4229
|
-
const normal = curve.getNormal(t);
|
|
4230
|
-
const dist1 = normal.clone().scale(isClockwise ? halfStrokeWidth : -halfStrokeWidth);
|
|
4231
|
-
const dist2 = normal.clone().scale(isClockwise ? -halfStrokeWidth : halfStrokeWidth);
|
|
4232
|
-
points.push(
|
|
4233
|
-
point.clone().add(dist1),
|
|
4234
|
-
point.clone().add(dist2),
|
|
4235
|
-
point.clone().add({ x: halfStrokeWidth, y: 0 }),
|
|
4236
|
-
point.clone().add({ x: -halfStrokeWidth, y: 0 }),
|
|
4237
|
-
point.clone().add({ x: 0, y: halfStrokeWidth }),
|
|
4238
|
-
point.clone().add({ x: 0, y: -halfStrokeWidth }),
|
|
4239
|
-
point.clone().add({ x: halfStrokeWidth, y: halfStrokeWidth }),
|
|
4240
|
-
point.clone().add({ x: -halfStrokeWidth, y: -halfStrokeWidth })
|
|
4241
|
-
);
|
|
4242
|
-
}
|
|
4243
|
-
min.clampMin(...points);
|
|
4244
|
-
max.clampMax(...points);
|
|
4245
|
-
}
|
|
4246
|
-
}
|
|
4247
4618
|
});
|
|
4619
|
+
if (withStyle) {
|
|
4620
|
+
const strokeWidth = this.strokeWidth;
|
|
4621
|
+
if (strokeWidth > 1 && Number.isFinite(min.x)) {
|
|
4622
|
+
const half = strokeWidth / 2;
|
|
4623
|
+
min.set(min.x - half, min.y - half);
|
|
4624
|
+
max.set(max.x + half, max.y + half);
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4248
4627
|
return { min: min.finite(), max: max.finite() };
|
|
4249
4628
|
}
|
|
4250
4629
|
strokeTriangulate(options) {
|
|
@@ -4392,6 +4771,44 @@ class Path2DSet {
|
|
|
4392
4771
|
this.paths = paths;
|
|
4393
4772
|
this.viewBox = viewBox;
|
|
4394
4773
|
}
|
|
4774
|
+
/**
|
|
4775
|
+
* Test whether a point lies inside the filled area of any path in this set.
|
|
4776
|
+
* Purely geometric (ignores `fill: 'none'`); use {@link hitTest} for style-aware hits.
|
|
4777
|
+
*/
|
|
4778
|
+
isPointInFill(point, options = {}) {
|
|
4779
|
+
return this.paths.some((path) => path.isPointInFill(point, options));
|
|
4780
|
+
}
|
|
4781
|
+
/**
|
|
4782
|
+
* Concise PathKit-style fill containment test across the whole set; shorthand for
|
|
4783
|
+
* {@link isPointInFill} with a `{ x, y }` point.
|
|
4784
|
+
*/
|
|
4785
|
+
contains(x, y, options = {}) {
|
|
4786
|
+
return this.isPointInFill({ x, y }, options);
|
|
4787
|
+
}
|
|
4788
|
+
/**
|
|
4789
|
+
* Find the topmost path hit by a point, or `undefined` if none.
|
|
4790
|
+
*
|
|
4791
|
+
* Paths are tested top-to-bottom (last drawn first). For each path a fill hit is checked
|
|
4792
|
+
* first (skipped when `style.fill` is `'none'`), then — if `stroke` is enabled — a stroke
|
|
4793
|
+
* hit (skipped when `style.stroke` is `'none'`). This honors the "fill: none falls back to
|
|
4794
|
+
* stroke" rule; the coordinate space of `point` must match the paths (no scaling assumed).
|
|
4795
|
+
*
|
|
4796
|
+
* Options: `stroke` (also test strokes, default `true`), `tolerance` (extra stroke hit slack
|
|
4797
|
+
* in path units, default `0`), and `fillRule` (overrides each path's own fill rule).
|
|
4798
|
+
*/
|
|
4799
|
+
hitTest(point, options = {}) {
|
|
4800
|
+
const { stroke = true, tolerance, fillRule } = options;
|
|
4801
|
+
for (let i = this.paths.length - 1; i >= 0; i--) {
|
|
4802
|
+
const path = this.paths[i];
|
|
4803
|
+
if ((path.style.fill ?? "#000") !== "none" && path.isPointInFill(point, { fillRule })) {
|
|
4804
|
+
return path;
|
|
4805
|
+
}
|
|
4806
|
+
if (stroke && (path.style.stroke ?? "none") !== "none" && path.isPointInStroke(point, { tolerance })) {
|
|
4807
|
+
return path;
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
return void 0;
|
|
4811
|
+
}
|
|
4395
4812
|
getBoundingBox(withStyle = true) {
|
|
4396
4813
|
if (!this.paths.length) {
|
|
4397
4814
|
return void 0;
|
|
@@ -4583,6 +5000,10 @@ exports.parseCssArg = parseCssArg;
|
|
|
4583
5000
|
exports.parseCssArgs = parseCssArgs;
|
|
4584
5001
|
exports.parseCssFunctions = parseCssFunctions;
|
|
4585
5002
|
exports.parsePathDataArgs = parsePathDataArgs;
|
|
5003
|
+
exports.pointInPolygon = pointInPolygon;
|
|
5004
|
+
exports.pointInPolygons = pointInPolygons;
|
|
5005
|
+
exports.pointToPolylineDistance = pointToPolylineDistance;
|
|
5006
|
+
exports.pointToSegmentDistance = pointToSegmentDistance;
|
|
4586
5007
|
exports.quadraticBezier = quadraticBezier;
|
|
4587
5008
|
exports.setCanvasContext = setCanvasContext;
|
|
4588
5009
|
exports.strokeTriangulate = strokeTriangulate;
|