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.mjs
CHANGED
|
@@ -120,7 +120,10 @@ class Vector2 {
|
|
|
120
120
|
return this.set(this._x * x, this._y * y);
|
|
121
121
|
}
|
|
122
122
|
divide(x = 0, y = x) {
|
|
123
|
-
return this.set(
|
|
123
|
+
return this.set(
|
|
124
|
+
x === 0 ? this._x : this._x / x,
|
|
125
|
+
y === 0 ? this._y : this._y / y
|
|
126
|
+
);
|
|
124
127
|
}
|
|
125
128
|
cross(p) {
|
|
126
129
|
return this._x * p.y - this._y * p.x;
|
|
@@ -313,17 +316,11 @@ function getIntersectionPoint(p1, p2, q1, q2) {
|
|
|
313
316
|
const q1p1 = q1.clone().sub(p1);
|
|
314
317
|
const crossRS = r.cross(s);
|
|
315
318
|
if (crossRS === 0) {
|
|
316
|
-
return
|
|
317
|
-
(p1.x + q1.x) / 2,
|
|
318
|
-
(p1.y + q1.y) / 2
|
|
319
|
-
);
|
|
319
|
+
return null;
|
|
320
320
|
}
|
|
321
321
|
const t = q1p1.cross(s) / crossRS;
|
|
322
322
|
if (Math.abs(t) > 1) {
|
|
323
|
-
return
|
|
324
|
-
(p1.x + q1.x) / 2,
|
|
325
|
-
(p1.y + q1.y) / 2
|
|
326
|
-
);
|
|
323
|
+
return null;
|
|
327
324
|
}
|
|
328
325
|
return new Vector2(
|
|
329
326
|
p1.x + t * r.x,
|
|
@@ -592,7 +589,7 @@ function getDirectedArea(vertices) {
|
|
|
592
589
|
function cross(ax, ay, bx, by, cx, cy) {
|
|
593
590
|
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
594
591
|
}
|
|
595
|
-
function windingNumber(px, py, polygon) {
|
|
592
|
+
function windingNumber$1(px, py, polygon) {
|
|
596
593
|
const polygonLen = polygon.length;
|
|
597
594
|
let wn = 0;
|
|
598
595
|
for (let i = 0, j = polygonLen - 2; i < polygonLen; j = i, i += 2) {
|
|
@@ -615,11 +612,16 @@ function distance(p1, p2) {
|
|
|
615
612
|
const dy = p2[1] - p1[1];
|
|
616
613
|
return Math.sqrt(dx * dx + dy * dy);
|
|
617
614
|
}
|
|
615
|
+
function aabbIntersects(a, b) {
|
|
616
|
+
return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY;
|
|
617
|
+
}
|
|
618
618
|
function nonzeroFillRule(paths) {
|
|
619
619
|
const results = paths.map((_, i) => ({ index: i }));
|
|
620
|
-
const
|
|
620
|
+
const bboxes = [];
|
|
621
|
+
const testPointsGroups = paths.map((path, pathIndex) => {
|
|
621
622
|
const len = path.length;
|
|
622
623
|
if (!len) {
|
|
624
|
+
bboxes[pathIndex] = null;
|
|
623
625
|
return [];
|
|
624
626
|
}
|
|
625
627
|
let xMinYAuto = [Number.MAX_SAFE_INTEGER, 0];
|
|
@@ -642,6 +644,12 @@ function nonzeroFillRule(paths) {
|
|
|
642
644
|
xAutoYMax = [x, y];
|
|
643
645
|
}
|
|
644
646
|
}
|
|
647
|
+
bboxes[pathIndex] = {
|
|
648
|
+
minX: xMinYAuto[0],
|
|
649
|
+
minY: xAutoYMin[1],
|
|
650
|
+
maxX: xMaxYAuto[0],
|
|
651
|
+
maxY: xAutoYMax[1]
|
|
652
|
+
};
|
|
645
653
|
const mid = [
|
|
646
654
|
(xMinYAuto[0] + xMaxYAuto[0]) / 2,
|
|
647
655
|
(xAutoYMin[1] + xAutoYMax[1]) / 2
|
|
@@ -690,14 +698,19 @@ function nonzeroFillRule(paths) {
|
|
|
690
698
|
for (let i = 0, len = paths.length; i < len; i++) {
|
|
691
699
|
const _results = [];
|
|
692
700
|
const testPoints = testPointsGroups[i];
|
|
701
|
+
const boxI = bboxes[i];
|
|
693
702
|
for (let j = 0; j < len; j++) {
|
|
694
703
|
if (i === j)
|
|
695
704
|
continue;
|
|
705
|
+
const boxJ = bboxes[j];
|
|
706
|
+
if (!boxI || !boxJ || !aabbIntersects(boxI, boxJ)) {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
696
709
|
const wnMap = {};
|
|
697
710
|
const wnList = [];
|
|
698
711
|
for (let p = 0, pLen = testPoints.length; p < pLen; p++) {
|
|
699
712
|
const [x, y] = testPoints[p];
|
|
700
|
-
const winding = windingNumber(x, y, paths[j]);
|
|
713
|
+
const winding = windingNumber$1(x, y, paths[j]);
|
|
701
714
|
wnMap[winding] = (wnMap[winding] ?? 0) + 1;
|
|
702
715
|
wnList.push(winding);
|
|
703
716
|
}
|
|
@@ -720,6 +733,120 @@ function nonzeroFillRule(paths) {
|
|
|
720
733
|
return results;
|
|
721
734
|
}
|
|
722
735
|
|
|
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
|
+
|
|
723
850
|
function quadraticBezierP0(t, p) {
|
|
724
851
|
const k = 1 - t;
|
|
725
852
|
return k * k * p;
|
|
@@ -2533,6 +2660,41 @@ function svgToPath2DSet(svg) {
|
|
|
2533
2660
|
class Curve {
|
|
2534
2661
|
arcLengthDivision = 200;
|
|
2535
2662
|
_lengths = [];
|
|
2663
|
+
_adaptiveCache;
|
|
2664
|
+
/**
|
|
2665
|
+
* Parent composite, set lazily when a composite caches its children. Lets
|
|
2666
|
+
* {@link invalidate} propagate up so an ancestor's caches refresh too.
|
|
2667
|
+
*/
|
|
2668
|
+
_owner;
|
|
2669
|
+
_invalidating = false;
|
|
2670
|
+
/**
|
|
2671
|
+
* Drop cached arc lengths and the cached sampled outline used by hit testing, then
|
|
2672
|
+
* bubble up to {@link _owner}. Called automatically by {@link applyTransform} and the
|
|
2673
|
+
* `Path2D` mutators; call it manually after mutating control-point coordinates in place —
|
|
2674
|
+
* the caches cannot observe such mutations.
|
|
2675
|
+
*/
|
|
2676
|
+
invalidate() {
|
|
2677
|
+
if (this._invalidating) {
|
|
2678
|
+
return this;
|
|
2679
|
+
}
|
|
2680
|
+
this._invalidating = true;
|
|
2681
|
+
this._invalidateSelf();
|
|
2682
|
+
this._owner?.invalidate();
|
|
2683
|
+
this._invalidating = false;
|
|
2684
|
+
return this;
|
|
2685
|
+
}
|
|
2686
|
+
/** Clears this curve's own caches. Composites also clear their children (see override). */
|
|
2687
|
+
_invalidateSelf() {
|
|
2688
|
+
this._lengths.length = 0;
|
|
2689
|
+
this._adaptiveCache = void 0;
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Sampled outline cached for repeated hit tests (read-only — do not mutate the result).
|
|
2693
|
+
* Invalidated by {@link invalidate}.
|
|
2694
|
+
*/
|
|
2695
|
+
_getCachedAdaptiveVertices() {
|
|
2696
|
+
return this._adaptiveCache ??= this.getAdaptiveVertices();
|
|
2697
|
+
}
|
|
2536
2698
|
getPointAt(u, output = new Vector2()) {
|
|
2537
2699
|
return this.getPoint(this.getUToTMapping(u), output);
|
|
2538
2700
|
}
|
|
@@ -2551,6 +2713,7 @@ class Curve {
|
|
|
2551
2713
|
transform.apply(p, p);
|
|
2552
2714
|
}
|
|
2553
2715
|
});
|
|
2716
|
+
this.invalidate();
|
|
2554
2717
|
return this;
|
|
2555
2718
|
}
|
|
2556
2719
|
getUnevenVertices(count = 5, output = []) {
|
|
@@ -2695,18 +2858,65 @@ class Curve {
|
|
|
2695
2858
|
return mid;
|
|
2696
2859
|
}
|
|
2697
2860
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2861
|
+
const vertices = this.getAdaptiveVertices();
|
|
2862
|
+
let minX = min.x;
|
|
2863
|
+
let minY = min.y;
|
|
2864
|
+
let maxX = max.x;
|
|
2865
|
+
let maxY = max.y;
|
|
2866
|
+
for (let i = 0, len = vertices.length; i < len; i += 2) {
|
|
2867
|
+
const x = vertices[i];
|
|
2868
|
+
const y = vertices[i + 1];
|
|
2869
|
+
if (x < minX)
|
|
2870
|
+
minX = x;
|
|
2871
|
+
if (y < minY)
|
|
2872
|
+
minY = y;
|
|
2873
|
+
if (x > maxX)
|
|
2874
|
+
maxX = x;
|
|
2875
|
+
if (y > maxY)
|
|
2876
|
+
maxY = y;
|
|
2703
2877
|
}
|
|
2878
|
+
min.set(minX, minY);
|
|
2879
|
+
max.set(maxX, maxY);
|
|
2704
2880
|
return { min: min.finite(), max: max.finite() };
|
|
2705
2881
|
}
|
|
2706
2882
|
getBoundingBox() {
|
|
2707
2883
|
const { min, max } = this.getMinMax();
|
|
2708
2884
|
return new BoundingBox(min.x, min.y, max.x - min.x, max.y - min.y);
|
|
2709
2885
|
}
|
|
2886
|
+
/**
|
|
2887
|
+
* Test whether a point lies inside the area enclosed by this curve.
|
|
2888
|
+
*
|
|
2889
|
+
* The curve is sampled via {@link getAdaptiveVertices} into a single implicitly closed
|
|
2890
|
+
* ring. This is purely geometric (it ignores any `fill`/`stroke` style), mirroring
|
|
2891
|
+
* `CanvasRenderingContext2D.isPointInPath`.
|
|
2892
|
+
*
|
|
2893
|
+
* Composites that hold multiple sub-paths (e.g. {@link Path2D}) override this so holes
|
|
2894
|
+
* are honored — a single `Curve` is always one ring.
|
|
2895
|
+
*/
|
|
2896
|
+
isPointInFill(point, options = {}) {
|
|
2897
|
+
return pointInPolygon(point, this._getCachedAdaptiveVertices(), options.fillRule);
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Test whether a point lies on this curve's stroke, i.e. within `strokeWidth / 2 + tolerance`
|
|
2901
|
+
* of the sampled outline. The point must be in the same coordinate space as the curve.
|
|
2902
|
+
*
|
|
2903
|
+
* Options: `strokeWidth` (path units, default `1`), `tolerance` (extra hit slack in path
|
|
2904
|
+
* units, default `0` — useful for thin strokes; no coordinate scaling is assumed, so convert
|
|
2905
|
+
* pixel tolerance to path units upstream if your path is normalized), and `closed` (whether
|
|
2906
|
+
* to include the closing edge from the last vertex back to the first).
|
|
2907
|
+
*/
|
|
2908
|
+
isPointInStroke(point, options = {}) {
|
|
2909
|
+
const { strokeWidth = 1, tolerance = 0, closed = false } = options;
|
|
2910
|
+
const distance = pointToPolylineDistance(point, this._getCachedAdaptiveVertices(), closed);
|
|
2911
|
+
return distance <= strokeWidth / 2 + tolerance;
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* Concise PathKit-style fill containment test: `contains(x, y)` is shorthand for
|
|
2915
|
+
* {@link isPointInFill} with a `{ x, y }` point.
|
|
2916
|
+
*/
|
|
2917
|
+
contains(x, y, options = {}) {
|
|
2918
|
+
return this.isPointInFill({ x, y }, options);
|
|
2919
|
+
}
|
|
2710
2920
|
getFillVertices(_options) {
|
|
2711
2921
|
return this.getAdaptiveVertices();
|
|
2712
2922
|
}
|
|
@@ -2841,6 +3051,64 @@ class RoundCurve extends Curve {
|
|
|
2841
3051
|
}
|
|
2842
3052
|
return output.set(_x, _y);
|
|
2843
3053
|
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Point on the ellipse at an absolute angle (mirrors {@link getPoint}'s parameterization,
|
|
3056
|
+
* ignoring `_diff`).
|
|
3057
|
+
*/
|
|
3058
|
+
_pointAtAngle(angle, output) {
|
|
3059
|
+
let x = this.cx + this.rx * Math.cos(angle);
|
|
3060
|
+
let y = this.cy + this.ry * Math.sin(angle);
|
|
3061
|
+
if (this.rotate !== 0) {
|
|
3062
|
+
const cos = Math.cos(this.rotate);
|
|
3063
|
+
const sin = Math.sin(this.rotate);
|
|
3064
|
+
const tx = x - this.cx;
|
|
3065
|
+
const ty = y - this.cy;
|
|
3066
|
+
x = tx * cos - ty * sin + this.cx;
|
|
3067
|
+
y = tx * sin + ty * cos + this.cy;
|
|
3068
|
+
}
|
|
3069
|
+
return output.set(x, y);
|
|
3070
|
+
}
|
|
3071
|
+
/**
|
|
3072
|
+
* Analytical bounds of the (elliptical) arc: the start/end points plus the per-axis
|
|
3073
|
+
* extrema angles that fall within the swept interval. Matches {@link getPoint}, so it is
|
|
3074
|
+
* exact for `ArcCurve`/`EllipseCurve`. The `_diff` offset (used only by the legacy
|
|
3075
|
+
* `_getAdaptiveVerticesByCircle` path) is intentionally ignored here.
|
|
3076
|
+
*/
|
|
3077
|
+
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
3078
|
+
const { startAngle, rotate } = this;
|
|
3079
|
+
const delta = this._getDeltaAngle();
|
|
3080
|
+
const cosT = Math.cos(rotate);
|
|
3081
|
+
const sinT = Math.sin(rotate);
|
|
3082
|
+
const p = tempV2;
|
|
3083
|
+
let minX = min.x;
|
|
3084
|
+
let minY = min.y;
|
|
3085
|
+
let maxX = max.x;
|
|
3086
|
+
let maxY = max.y;
|
|
3087
|
+
const consider = (angle) => {
|
|
3088
|
+
this._pointAtAngle(angle, p);
|
|
3089
|
+
if (p.x < minX)
|
|
3090
|
+
minX = p.x;
|
|
3091
|
+
if (p.y < minY)
|
|
3092
|
+
minY = p.y;
|
|
3093
|
+
if (p.x > maxX)
|
|
3094
|
+
maxX = p.x;
|
|
3095
|
+
if (p.y > maxY)
|
|
3096
|
+
maxY = p.y;
|
|
3097
|
+
};
|
|
3098
|
+
consider(startAngle);
|
|
3099
|
+
consider(startAngle + delta);
|
|
3100
|
+
const ax = Math.atan2(-this.ry * sinT, this.rx * cosT);
|
|
3101
|
+
const ay = Math.atan2(this.ry * cosT, this.rx * sinT);
|
|
3102
|
+
const bases = [ax, ax + Math.PI, ay, ay + Math.PI];
|
|
3103
|
+
for (let i = 0; i < 4; i++) {
|
|
3104
|
+
if (angleInSweep(bases[i], startAngle, delta)) {
|
|
3105
|
+
consider(bases[i]);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
min.set(minX, minY);
|
|
3109
|
+
max.set(maxX, maxY);
|
|
3110
|
+
return { min: min.finite(), max: max.finite() };
|
|
3111
|
+
}
|
|
2844
3112
|
toCommands() {
|
|
2845
3113
|
const { cx, cy, rx, ry, startAngle, endAngle, clockwise, rotate } = this;
|
|
2846
3114
|
const startX = cx + rx * Math.cos(startAngle) * Math.cos(rotate) - ry * Math.sin(startAngle) * Math.sin(rotate);
|
|
@@ -2891,6 +3159,7 @@ class RoundCurve extends Curve {
|
|
|
2891
3159
|
} else {
|
|
2892
3160
|
transfEllipseNoSkew(this, transform);
|
|
2893
3161
|
}
|
|
3162
|
+
this.invalidate();
|
|
2894
3163
|
return this;
|
|
2895
3164
|
}
|
|
2896
3165
|
getControlPointRefs() {
|
|
@@ -3031,6 +3300,23 @@ class RoundCurve extends Curve {
|
|
|
3031
3300
|
return this;
|
|
3032
3301
|
}
|
|
3033
3302
|
}
|
|
3303
|
+
function angleInSweep(a, start, delta) {
|
|
3304
|
+
const PI_2 = Math.PI * 2;
|
|
3305
|
+
const eps = 1e-9;
|
|
3306
|
+
if (Math.abs(delta) >= PI_2 - eps) {
|
|
3307
|
+
return true;
|
|
3308
|
+
}
|
|
3309
|
+
let off = (a - start) % PI_2;
|
|
3310
|
+
if (delta >= 0) {
|
|
3311
|
+
if (off < -eps)
|
|
3312
|
+
off += PI_2;
|
|
3313
|
+
return off >= -eps && off <= delta + eps;
|
|
3314
|
+
}
|
|
3315
|
+
if (off > eps) {
|
|
3316
|
+
off -= PI_2;
|
|
3317
|
+
}
|
|
3318
|
+
return off <= eps && off >= delta - eps;
|
|
3319
|
+
}
|
|
3034
3320
|
function transfEllipseGeneric(curve, m) {
|
|
3035
3321
|
const a = curve.rx;
|
|
3036
3322
|
const b = curve.ry;
|
|
@@ -3269,6 +3555,22 @@ class CompositeCurve extends Curve {
|
|
|
3269
3555
|
super();
|
|
3270
3556
|
this.curves = curves;
|
|
3271
3557
|
}
|
|
3558
|
+
_adaptiveCacheLen = -1;
|
|
3559
|
+
_invalidateSelf() {
|
|
3560
|
+
super._invalidateSelf();
|
|
3561
|
+
this._adaptiveCacheLen = -1;
|
|
3562
|
+
this.curves.forEach((curve) => curve.invalidate());
|
|
3563
|
+
}
|
|
3564
|
+
_getCachedAdaptiveVertices() {
|
|
3565
|
+
if (!this._adaptiveCache || this._adaptiveCacheLen !== this.curves.length) {
|
|
3566
|
+
this.curves.forEach((curve) => {
|
|
3567
|
+
curve._owner = this;
|
|
3568
|
+
});
|
|
3569
|
+
this._adaptiveCache = this.getAdaptiveVertices();
|
|
3570
|
+
this._adaptiveCacheLen = this.curves.length;
|
|
3571
|
+
}
|
|
3572
|
+
return this._adaptiveCache;
|
|
3573
|
+
}
|
|
3272
3574
|
getFlatCurves() {
|
|
3273
3575
|
return this.curves.flatMap((curve) => {
|
|
3274
3576
|
if (curve instanceof CompositeCurve) {
|
|
@@ -3314,6 +3616,7 @@ class CompositeCurve extends Curve {
|
|
|
3314
3616
|
updateLengths() {
|
|
3315
3617
|
const lengths = [];
|
|
3316
3618
|
for (let i = 0, sum = 0, len = this.curves.length; i < len; i++) {
|
|
3619
|
+
this.curves[i]._owner = this;
|
|
3317
3620
|
sum += this.curves[i].getLength();
|
|
3318
3621
|
lengths.push(sum);
|
|
3319
3622
|
}
|
|
@@ -3382,7 +3685,10 @@ class CompositeCurve extends Curve {
|
|
|
3382
3685
|
}
|
|
3383
3686
|
}
|
|
3384
3687
|
applyTransform(transform) {
|
|
3688
|
+
this._invalidating = true;
|
|
3385
3689
|
this.curves.forEach((curve) => curve.applyTransform(transform));
|
|
3690
|
+
this._invalidating = false;
|
|
3691
|
+
this.invalidate();
|
|
3386
3692
|
return this;
|
|
3387
3693
|
}
|
|
3388
3694
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
@@ -3721,7 +4027,7 @@ class RectangleCurve extends PolygonCurve {
|
|
|
3721
4027
|
}
|
|
3722
4028
|
}
|
|
3723
4029
|
|
|
3724
|
-
class RoundRectangleCurve extends
|
|
4030
|
+
class RoundRectangleCurve extends CompositeCurve {
|
|
3725
4031
|
constructor(x = 0, y = 0, width = 1, height = 1, radius = 1) {
|
|
3726
4032
|
super();
|
|
3727
4033
|
this.x = x;
|
|
@@ -3732,25 +4038,53 @@ class RoundRectangleCurve extends RoundCurve {
|
|
|
3732
4038
|
this.update();
|
|
3733
4039
|
}
|
|
3734
4040
|
update() {
|
|
3735
|
-
const { x, y, width, height
|
|
3736
|
-
const
|
|
3737
|
-
const
|
|
3738
|
-
const
|
|
3739
|
-
const
|
|
3740
|
-
const
|
|
3741
|
-
const
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
4041
|
+
const { x, y, width, height } = this;
|
|
4042
|
+
const r = Math.max(0, Math.min(this.radius, Math.abs(width) / 2, Math.abs(height) / 2));
|
|
4043
|
+
const x0 = x;
|
|
4044
|
+
const x1 = x + r;
|
|
4045
|
+
const x2 = x + width - r;
|
|
4046
|
+
const x3 = x + width;
|
|
4047
|
+
const y0 = y;
|
|
4048
|
+
const y1 = y + r;
|
|
4049
|
+
const y2 = y + height - r;
|
|
4050
|
+
const y3 = y + height;
|
|
4051
|
+
if (r <= 0) {
|
|
4052
|
+
this.curves = [
|
|
4053
|
+
LineCurve.from(x0, y0, x3, y0),
|
|
4054
|
+
LineCurve.from(x3, y0, x3, y3),
|
|
4055
|
+
LineCurve.from(x3, y3, x0, y3),
|
|
4056
|
+
LineCurve.from(x0, y3, x0, y0)
|
|
4057
|
+
];
|
|
4058
|
+
} else {
|
|
4059
|
+
const HALF_PI = Math.PI / 2;
|
|
4060
|
+
this.curves = [
|
|
4061
|
+
LineCurve.from(x1, y0, x2, y0),
|
|
4062
|
+
// top edge
|
|
4063
|
+
new ArcCurve(x2, y1, r, -HALF_PI, 0, true),
|
|
4064
|
+
// top-right corner
|
|
4065
|
+
LineCurve.from(x3, y1, x3, y2),
|
|
4066
|
+
// right edge
|
|
4067
|
+
new ArcCurve(x2, y2, r, 0, HALF_PI, true),
|
|
4068
|
+
// bottom-right corner
|
|
4069
|
+
LineCurve.from(x2, y3, x1, y3),
|
|
4070
|
+
// bottom edge
|
|
4071
|
+
new ArcCurve(x1, y2, r, HALF_PI, Math.PI, true),
|
|
4072
|
+
// bottom-left corner
|
|
4073
|
+
LineCurve.from(x0, y2, x0, y1),
|
|
4074
|
+
// left edge
|
|
4075
|
+
new ArcCurve(x1, y1, r, Math.PI, Math.PI * 1.5, true)
|
|
4076
|
+
// top-left corner
|
|
4077
|
+
];
|
|
4078
|
+
}
|
|
4079
|
+
this.invalidate();
|
|
3745
4080
|
return this;
|
|
3746
4081
|
}
|
|
3747
4082
|
drawTo(ctx) {
|
|
3748
|
-
|
|
3749
|
-
ctx.roundRect(x, y, width, height, radius);
|
|
4083
|
+
ctx.roundRect(this.x, this.y, this.width, this.height, this.radius);
|
|
3750
4084
|
return this;
|
|
3751
4085
|
}
|
|
3752
4086
|
copyFrom(source) {
|
|
3753
|
-
|
|
4087
|
+
this.arcLengthDivision = source.arcLengthDivision;
|
|
3754
4088
|
this.x = source.x;
|
|
3755
4089
|
this.y = source.y;
|
|
3756
4090
|
this.width = source.width;
|
|
@@ -3846,6 +4180,17 @@ class CurvePath extends CompositeCurve {
|
|
|
3846
4180
|
super.getFillVertices(options)
|
|
3847
4181
|
);
|
|
3848
4182
|
}
|
|
4183
|
+
/**
|
|
4184
|
+
* Same as {@link Curve.isPointInStroke}, but `closed` defaults to this sub-path's actual
|
|
4185
|
+
* closed-ness: explicitly `autoClose`, or geometrically closed (first vertex === last).
|
|
4186
|
+
*/
|
|
4187
|
+
isPointInStroke(point, options = {}) {
|
|
4188
|
+
const { strokeWidth = 1, tolerance = 0 } = options;
|
|
4189
|
+
const vertices = this._getCachedAdaptiveVertices();
|
|
4190
|
+
const len = vertices.length;
|
|
4191
|
+
const closed = options.closed ?? (this.autoClose || len >= 6 && vertices[0] === vertices[len - 2] && vertices[1] === vertices[len - 1]);
|
|
4192
|
+
return pointToPolylineDistance(point, vertices, closed) <= strokeWidth / 2 + tolerance;
|
|
4193
|
+
}
|
|
3849
4194
|
_setCurrentPoint(point) {
|
|
3850
4195
|
this.currentPoint = new Vector2(point.x, point.y);
|
|
3851
4196
|
if (!this.startPoint) {
|
|
@@ -4019,6 +4364,8 @@ class CurvePath extends CompositeCurve {
|
|
|
4019
4364
|
|
|
4020
4365
|
class Path2D extends CompositeCurve {
|
|
4021
4366
|
_meta;
|
|
4367
|
+
_ringsCache;
|
|
4368
|
+
_ringsCacheLen = -1;
|
|
4022
4369
|
currentCurve = new CurvePath();
|
|
4023
4370
|
style;
|
|
4024
4371
|
get startPoint() {
|
|
@@ -4137,18 +4484,21 @@ class Path2D extends CompositeCurve {
|
|
|
4137
4484
|
this.getControlPointRefs().forEach((point) => {
|
|
4138
4485
|
point.scale(sx, sy, target);
|
|
4139
4486
|
});
|
|
4487
|
+
this.invalidate();
|
|
4140
4488
|
return this;
|
|
4141
4489
|
}
|
|
4142
4490
|
skew(ax, ay = 0, target = { x: 0, y: 0 }) {
|
|
4143
4491
|
this.getControlPointRefs().forEach((point) => {
|
|
4144
4492
|
point.skew(ax, ay, target);
|
|
4145
4493
|
});
|
|
4494
|
+
this.invalidate();
|
|
4146
4495
|
return this;
|
|
4147
4496
|
}
|
|
4148
4497
|
rotate(rad, target = { x: 0, y: 0 }) {
|
|
4149
4498
|
this.getControlPointRefs().forEach((point) => {
|
|
4150
4499
|
point.rotate(rad, target);
|
|
4151
4500
|
});
|
|
4501
|
+
this.invalidate();
|
|
4152
4502
|
return this;
|
|
4153
4503
|
}
|
|
4154
4504
|
bold(b) {
|
|
@@ -4207,38 +4557,67 @@ class Path2D extends CompositeCurve {
|
|
|
4207
4557
|
}
|
|
4208
4558
|
});
|
|
4209
4559
|
});
|
|
4560
|
+
this.invalidate();
|
|
4210
4561
|
return this;
|
|
4211
4562
|
}
|
|
4563
|
+
/**
|
|
4564
|
+
* Test whether a point lies inside the filled area of this path.
|
|
4565
|
+
*
|
|
4566
|
+
* Each sub-path ({@link CurvePath}) is sampled into its own ring and all rings are
|
|
4567
|
+
* evaluated together via {@link pointInPolygons}, so holes (donut / hollow shapes) are
|
|
4568
|
+
* honored. This is purely geometric and ignores `style.fill` — for the `fill: 'none'`
|
|
4569
|
+
* fallback, gate the call upstream (see {@link Path2DSet.hitTest}).
|
|
4570
|
+
*
|
|
4571
|
+
* Defaults `fillRule` to `style.fillRule`, then `'nonzero'` (matching SVG/Canvas).
|
|
4572
|
+
*/
|
|
4573
|
+
_invalidateSelf() {
|
|
4574
|
+
super._invalidateSelf();
|
|
4575
|
+
this._ringsCache = void 0;
|
|
4576
|
+
this._ringsCacheLen = -1;
|
|
4577
|
+
}
|
|
4578
|
+
/** Per-sub-path sampled rings, cached for repeated hit tests. */
|
|
4579
|
+
_getRings() {
|
|
4580
|
+
if (!this._ringsCache || this._ringsCacheLen !== this.curves.length) {
|
|
4581
|
+
this._ringsCache = this.curves.map((curve) => {
|
|
4582
|
+
curve._owner = this;
|
|
4583
|
+
return curve.getAdaptiveVertices();
|
|
4584
|
+
});
|
|
4585
|
+
this._ringsCacheLen = this.curves.length;
|
|
4586
|
+
}
|
|
4587
|
+
return this._ringsCache;
|
|
4588
|
+
}
|
|
4589
|
+
isPointInFill(point, options = {}) {
|
|
4590
|
+
const fillRule = options.fillRule ?? this.style.fillRule ?? "nonzero";
|
|
4591
|
+
return pointInPolygons(point, this._getRings(), fillRule);
|
|
4592
|
+
}
|
|
4593
|
+
/**
|
|
4594
|
+
* Test whether a point lies on this path's stroke. A hit on any sub-path counts.
|
|
4595
|
+
*
|
|
4596
|
+
* Defaults `strokeWidth` to this path's own {@link strokeWidth} (which is `0` when
|
|
4597
|
+
* `style.stroke` is `'none'`). Each sub-path infers its own closed-ness unless `closed`
|
|
4598
|
+
* is given explicitly.
|
|
4599
|
+
*/
|
|
4600
|
+
isPointInStroke(point, options = {}) {
|
|
4601
|
+
const strokeWidth = options.strokeWidth ?? this.strokeWidth;
|
|
4602
|
+
const { tolerance = 0, closed } = options;
|
|
4603
|
+
return this.curves.some((curve) => curve.isPointInStroke(point, {
|
|
4604
|
+
strokeWidth,
|
|
4605
|
+
tolerance,
|
|
4606
|
+
closed
|
|
4607
|
+
}));
|
|
4608
|
+
}
|
|
4212
4609
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN, withStyle = true) {
|
|
4213
|
-
const strokeWidth = this.strokeWidth;
|
|
4214
4610
|
this.curves.forEach((curve) => {
|
|
4215
4611
|
curve.getMinMax(min, max);
|
|
4216
|
-
if (withStyle) {
|
|
4217
|
-
if (strokeWidth > 1) {
|
|
4218
|
-
const halfStrokeWidth = strokeWidth / 2;
|
|
4219
|
-
const isClockwise = curve.isClockwise();
|
|
4220
|
-
const points = [];
|
|
4221
|
-
for (let t = 0; t <= 1; t += 1 / curve.arcLengthDivision) {
|
|
4222
|
-
const point = curve.getPoint(t);
|
|
4223
|
-
const normal = curve.getNormal(t);
|
|
4224
|
-
const dist1 = normal.clone().scale(isClockwise ? halfStrokeWidth : -halfStrokeWidth);
|
|
4225
|
-
const dist2 = normal.clone().scale(isClockwise ? -halfStrokeWidth : halfStrokeWidth);
|
|
4226
|
-
points.push(
|
|
4227
|
-
point.clone().add(dist1),
|
|
4228
|
-
point.clone().add(dist2),
|
|
4229
|
-
point.clone().add({ x: halfStrokeWidth, y: 0 }),
|
|
4230
|
-
point.clone().add({ x: -halfStrokeWidth, y: 0 }),
|
|
4231
|
-
point.clone().add({ x: 0, y: halfStrokeWidth }),
|
|
4232
|
-
point.clone().add({ x: 0, y: -halfStrokeWidth }),
|
|
4233
|
-
point.clone().add({ x: halfStrokeWidth, y: halfStrokeWidth }),
|
|
4234
|
-
point.clone().add({ x: -halfStrokeWidth, y: -halfStrokeWidth })
|
|
4235
|
-
);
|
|
4236
|
-
}
|
|
4237
|
-
min.clampMin(...points);
|
|
4238
|
-
max.clampMax(...points);
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4241
4612
|
});
|
|
4613
|
+
if (withStyle) {
|
|
4614
|
+
const strokeWidth = this.strokeWidth;
|
|
4615
|
+
if (strokeWidth > 1 && Number.isFinite(min.x)) {
|
|
4616
|
+
const half = strokeWidth / 2;
|
|
4617
|
+
min.set(min.x - half, min.y - half);
|
|
4618
|
+
max.set(max.x + half, max.y + half);
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4242
4621
|
return { min: min.finite(), max: max.finite() };
|
|
4243
4622
|
}
|
|
4244
4623
|
strokeTriangulate(options) {
|
|
@@ -4386,6 +4765,44 @@ class Path2DSet {
|
|
|
4386
4765
|
this.paths = paths;
|
|
4387
4766
|
this.viewBox = viewBox;
|
|
4388
4767
|
}
|
|
4768
|
+
/**
|
|
4769
|
+
* Test whether a point lies inside the filled area of any path in this set.
|
|
4770
|
+
* Purely geometric (ignores `fill: 'none'`); use {@link hitTest} for style-aware hits.
|
|
4771
|
+
*/
|
|
4772
|
+
isPointInFill(point, options = {}) {
|
|
4773
|
+
return this.paths.some((path) => path.isPointInFill(point, options));
|
|
4774
|
+
}
|
|
4775
|
+
/**
|
|
4776
|
+
* Concise PathKit-style fill containment test across the whole set; shorthand for
|
|
4777
|
+
* {@link isPointInFill} with a `{ x, y }` point.
|
|
4778
|
+
*/
|
|
4779
|
+
contains(x, y, options = {}) {
|
|
4780
|
+
return this.isPointInFill({ x, y }, options);
|
|
4781
|
+
}
|
|
4782
|
+
/**
|
|
4783
|
+
* Find the topmost path hit by a point, or `undefined` if none.
|
|
4784
|
+
*
|
|
4785
|
+
* Paths are tested top-to-bottom (last drawn first). For each path a fill hit is checked
|
|
4786
|
+
* first (skipped when `style.fill` is `'none'`), then — if `stroke` is enabled — a stroke
|
|
4787
|
+
* hit (skipped when `style.stroke` is `'none'`). This honors the "fill: none falls back to
|
|
4788
|
+
* stroke" rule; the coordinate space of `point` must match the paths (no scaling assumed).
|
|
4789
|
+
*
|
|
4790
|
+
* Options: `stroke` (also test strokes, default `true`), `tolerance` (extra stroke hit slack
|
|
4791
|
+
* in path units, default `0`), and `fillRule` (overrides each path's own fill rule).
|
|
4792
|
+
*/
|
|
4793
|
+
hitTest(point, options = {}) {
|
|
4794
|
+
const { stroke = true, tolerance, fillRule } = options;
|
|
4795
|
+
for (let i = this.paths.length - 1; i >= 0; i--) {
|
|
4796
|
+
const path = this.paths[i];
|
|
4797
|
+
if ((path.style.fill ?? "#000") !== "none" && path.isPointInFill(point, { fillRule })) {
|
|
4798
|
+
return path;
|
|
4799
|
+
}
|
|
4800
|
+
if (stroke && (path.style.stroke ?? "none") !== "none" && path.isPointInStroke(point, { tolerance })) {
|
|
4801
|
+
return path;
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
return void 0;
|
|
4805
|
+
}
|
|
4389
4806
|
getBoundingBox(withStyle = true) {
|
|
4390
4807
|
if (!this.paths.length) {
|
|
4391
4808
|
return void 0;
|
|
@@ -4541,4 +4958,4 @@ function applyFFD(point, grid, width = grid.width, height = grid.height) {
|
|
|
4541
4958
|
point.set(x, y);
|
|
4542
4959
|
}
|
|
4543
4960
|
|
|
4544
|
-
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, quadraticBezier, setCanvasContext, strokeTriangulate, svgPathCommandsAddToPath2D, svgPathCommandsToData, svgPathDataToCommands, svgToDom, svgToPath2DSet, toKebabCase };
|
|
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 };
|