modern-path2d 1.6.1 → 1.8.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 +565 -74
- package/dist/index.d.cts +178 -4
- package/dist/index.d.mts +178 -4
- package/dist/index.d.ts +178 -4
- package/dist/index.js +3 -2
- package/dist/index.mjs +562 -75
- package/package.json +4 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import earcut from 'earcut';
|
|
2
|
+
import polygonClipping from 'polygon-clipping';
|
|
2
3
|
|
|
3
4
|
function drawPoint(ctx, x, y, options = {}) {
|
|
4
5
|
const { radius = 1 } = options;
|
|
@@ -120,7 +121,10 @@ class Vector2 {
|
|
|
120
121
|
return this.set(this._x * x, this._y * y);
|
|
121
122
|
}
|
|
122
123
|
divide(x = 0, y = x) {
|
|
123
|
-
return this.set(
|
|
124
|
+
return this.set(
|
|
125
|
+
x === 0 ? this._x : this._x / x,
|
|
126
|
+
y === 0 ? this._y : this._y / y
|
|
127
|
+
);
|
|
124
128
|
}
|
|
125
129
|
cross(p) {
|
|
126
130
|
return this._x * p.y - this._y * p.x;
|
|
@@ -294,6 +298,63 @@ class BoundingBox {
|
|
|
294
298
|
}
|
|
295
299
|
}
|
|
296
300
|
|
|
301
|
+
function flatRingToPairs(r) {
|
|
302
|
+
const ring = [];
|
|
303
|
+
for (let i = 0; i < r.length; i += 2) {
|
|
304
|
+
ring.push([r[i], r[i + 1]]);
|
|
305
|
+
}
|
|
306
|
+
const first = ring[0];
|
|
307
|
+
const last = ring[ring.length - 1];
|
|
308
|
+
if (first && last && (first[0] !== last[0] || first[1] !== last[1])) {
|
|
309
|
+
ring.push([first[0], first[1]]);
|
|
310
|
+
}
|
|
311
|
+
return ring;
|
|
312
|
+
}
|
|
313
|
+
function ringsToGeom(rings) {
|
|
314
|
+
const valid = rings.filter((r) => r.length >= 6).map(flatRingToPairs);
|
|
315
|
+
if (!valid.length) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
let geom = [[valid[0]]];
|
|
319
|
+
for (let i = 1; i < valid.length; i++) {
|
|
320
|
+
geom = polygonClipping.xor(geom, [[valid[i]]]);
|
|
321
|
+
}
|
|
322
|
+
return geom;
|
|
323
|
+
}
|
|
324
|
+
function geomToRings(geom) {
|
|
325
|
+
const out = [];
|
|
326
|
+
for (const poly of geom) {
|
|
327
|
+
for (const ring of poly) {
|
|
328
|
+
const flat = [];
|
|
329
|
+
for (const [x, y] of ring) {
|
|
330
|
+
flat.push(x, y);
|
|
331
|
+
}
|
|
332
|
+
out.push(flat);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
function polygonBoolean(op, ringsA, ringsB) {
|
|
338
|
+
const a = ringsToGeom(ringsA);
|
|
339
|
+
const b = ringsToGeom(ringsB);
|
|
340
|
+
let res;
|
|
341
|
+
switch (op) {
|
|
342
|
+
case "union":
|
|
343
|
+
res = polygonClipping.union(a, b);
|
|
344
|
+
break;
|
|
345
|
+
case "intersection":
|
|
346
|
+
res = polygonClipping.intersection(a, b);
|
|
347
|
+
break;
|
|
348
|
+
case "difference":
|
|
349
|
+
res = polygonClipping.difference(a, b);
|
|
350
|
+
break;
|
|
351
|
+
case "xor":
|
|
352
|
+
res = polygonClipping.xor(a, b);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
return geomToRings(res);
|
|
356
|
+
}
|
|
357
|
+
|
|
297
358
|
function catmullRom(t, p0, p1, p2, p3) {
|
|
298
359
|
const v0 = (p2 - p0) * 0.5;
|
|
299
360
|
const v1 = (p3 - p1) * 0.5;
|
|
@@ -313,17 +374,11 @@ function getIntersectionPoint(p1, p2, q1, q2) {
|
|
|
313
374
|
const q1p1 = q1.clone().sub(p1);
|
|
314
375
|
const crossRS = r.cross(s);
|
|
315
376
|
if (crossRS === 0) {
|
|
316
|
-
return
|
|
317
|
-
(p1.x + q1.x) / 2,
|
|
318
|
-
(p1.y + q1.y) / 2
|
|
319
|
-
);
|
|
377
|
+
return null;
|
|
320
378
|
}
|
|
321
379
|
const t = q1p1.cross(s) / crossRS;
|
|
322
380
|
if (Math.abs(t) > 1) {
|
|
323
|
-
return
|
|
324
|
-
(p1.x + q1.x) / 2,
|
|
325
|
-
(p1.y + q1.y) / 2
|
|
326
|
-
);
|
|
381
|
+
return null;
|
|
327
382
|
}
|
|
328
383
|
return new Vector2(
|
|
329
384
|
p1.x + t * r.x,
|
|
@@ -615,11 +670,16 @@ function distance(p1, p2) {
|
|
|
615
670
|
const dy = p2[1] - p1[1];
|
|
616
671
|
return Math.sqrt(dx * dx + dy * dy);
|
|
617
672
|
}
|
|
673
|
+
function aabbIntersects(a, b) {
|
|
674
|
+
return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY;
|
|
675
|
+
}
|
|
618
676
|
function nonzeroFillRule(paths) {
|
|
619
677
|
const results = paths.map((_, i) => ({ index: i }));
|
|
620
|
-
const
|
|
678
|
+
const bboxes = [];
|
|
679
|
+
const testPointsGroups = paths.map((path, pathIndex) => {
|
|
621
680
|
const len = path.length;
|
|
622
681
|
if (!len) {
|
|
682
|
+
bboxes[pathIndex] = null;
|
|
623
683
|
return [];
|
|
624
684
|
}
|
|
625
685
|
let xMinYAuto = [Number.MAX_SAFE_INTEGER, 0];
|
|
@@ -642,6 +702,12 @@ function nonzeroFillRule(paths) {
|
|
|
642
702
|
xAutoYMax = [x, y];
|
|
643
703
|
}
|
|
644
704
|
}
|
|
705
|
+
bboxes[pathIndex] = {
|
|
706
|
+
minX: xMinYAuto[0],
|
|
707
|
+
minY: xAutoYMin[1],
|
|
708
|
+
maxX: xMaxYAuto[0],
|
|
709
|
+
maxY: xAutoYMax[1]
|
|
710
|
+
};
|
|
645
711
|
const mid = [
|
|
646
712
|
(xMinYAuto[0] + xMaxYAuto[0]) / 2,
|
|
647
713
|
(xAutoYMin[1] + xAutoYMax[1]) / 2
|
|
@@ -690,9 +756,14 @@ function nonzeroFillRule(paths) {
|
|
|
690
756
|
for (let i = 0, len = paths.length; i < len; i++) {
|
|
691
757
|
const _results = [];
|
|
692
758
|
const testPoints = testPointsGroups[i];
|
|
759
|
+
const boxI = bboxes[i];
|
|
693
760
|
for (let j = 0; j < len; j++) {
|
|
694
761
|
if (i === j)
|
|
695
762
|
continue;
|
|
763
|
+
const boxJ = bboxes[j];
|
|
764
|
+
if (!boxI || !boxJ || !aabbIntersects(boxI, boxJ)) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
696
767
|
const wnMap = {};
|
|
697
768
|
const wnList = [];
|
|
698
769
|
for (let p = 0, pLen = testPoints.length; p < pLen; p++) {
|
|
@@ -848,22 +919,35 @@ function quadraticBezier(t, p0, p1, p2) {
|
|
|
848
919
|
return quadraticBezierP0(t, p0) + quadraticBezierP1(t, p1) + quadraticBezierP2(t, p2);
|
|
849
920
|
}
|
|
850
921
|
|
|
922
|
+
function resolveLineJoin(join) {
|
|
923
|
+
switch (join) {
|
|
924
|
+
case "round":
|
|
925
|
+
case "bevel":
|
|
926
|
+
case "miter":
|
|
927
|
+
return join;
|
|
928
|
+
default:
|
|
929
|
+
return "miter";
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function resolveLineStyle(style) {
|
|
933
|
+
return {
|
|
934
|
+
width: style?.strokeWidth ?? 1,
|
|
935
|
+
alignment: 0.5,
|
|
936
|
+
join: resolveLineJoin(style?.strokeLinejoin),
|
|
937
|
+
cap: style?.strokeLinecap ?? "butt",
|
|
938
|
+
miterLimit: style?.strokeMiterlimit ?? 10
|
|
939
|
+
};
|
|
940
|
+
}
|
|
851
941
|
const closePointEps = 1e-4;
|
|
852
942
|
const curveEps = 1e-4;
|
|
853
943
|
function strokeTriangulate(points, options = {}) {
|
|
854
944
|
const {
|
|
855
945
|
vertices = [],
|
|
856
946
|
indices = [],
|
|
857
|
-
lineStyle = {
|
|
858
|
-
alignment: 0.5,
|
|
859
|
-
cap: "butt",
|
|
860
|
-
join: "miter",
|
|
861
|
-
width: 1,
|
|
862
|
-
miterLimit: 10
|
|
863
|
-
},
|
|
864
947
|
flipAlignment = false,
|
|
865
948
|
closed = true
|
|
866
949
|
} = options;
|
|
950
|
+
const lineStyle = options.lineStyle ?? resolveLineStyle(options.style);
|
|
867
951
|
const eps = closePointEps;
|
|
868
952
|
if (points.length === 0) {
|
|
869
953
|
return { vertices, indices };
|
|
@@ -2647,6 +2731,41 @@ function svgToPath2DSet(svg) {
|
|
|
2647
2731
|
class Curve {
|
|
2648
2732
|
arcLengthDivision = 200;
|
|
2649
2733
|
_lengths = [];
|
|
2734
|
+
_adaptiveCache;
|
|
2735
|
+
/**
|
|
2736
|
+
* Parent composite, set lazily when a composite caches its children. Lets
|
|
2737
|
+
* {@link invalidate} propagate up so an ancestor's caches refresh too.
|
|
2738
|
+
*/
|
|
2739
|
+
_owner;
|
|
2740
|
+
_invalidating = false;
|
|
2741
|
+
/**
|
|
2742
|
+
* Drop cached arc lengths and the cached sampled outline used by hit testing, then
|
|
2743
|
+
* bubble up to {@link _owner}. Called automatically by {@link applyTransform} and the
|
|
2744
|
+
* `Path2D` mutators; call it manually after mutating control-point coordinates in place —
|
|
2745
|
+
* the caches cannot observe such mutations.
|
|
2746
|
+
*/
|
|
2747
|
+
invalidate() {
|
|
2748
|
+
if (this._invalidating) {
|
|
2749
|
+
return this;
|
|
2750
|
+
}
|
|
2751
|
+
this._invalidating = true;
|
|
2752
|
+
this._invalidateSelf();
|
|
2753
|
+
this._owner?.invalidate();
|
|
2754
|
+
this._invalidating = false;
|
|
2755
|
+
return this;
|
|
2756
|
+
}
|
|
2757
|
+
/** Clears this curve's own caches. Composites also clear their children (see override). */
|
|
2758
|
+
_invalidateSelf() {
|
|
2759
|
+
this._lengths.length = 0;
|
|
2760
|
+
this._adaptiveCache = void 0;
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Sampled outline cached for repeated hit tests (read-only — do not mutate the result).
|
|
2764
|
+
* Invalidated by {@link invalidate}.
|
|
2765
|
+
*/
|
|
2766
|
+
_getCachedAdaptiveVertices() {
|
|
2767
|
+
return this._adaptiveCache ??= this.getAdaptiveVertices();
|
|
2768
|
+
}
|
|
2650
2769
|
getPointAt(u, output = new Vector2()) {
|
|
2651
2770
|
return this.getPoint(this.getUToTMapping(u), output);
|
|
2652
2771
|
}
|
|
@@ -2656,6 +2775,22 @@ class Curve {
|
|
|
2656
2775
|
getControlPointRefs() {
|
|
2657
2776
|
return [];
|
|
2658
2777
|
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Reverse the traversal direction in place (start ↔ end, same geometry). The base
|
|
2780
|
+
* implementation reverses the order of the control-point *values*, which is correct for
|
|
2781
|
+
* line / Bézier / spline primitives whose {@link getControlPointRefs} order matches their
|
|
2782
|
+
* parametric order. {@link RoundCurve} (angle-based) and composites (child order) override it.
|
|
2783
|
+
*/
|
|
2784
|
+
reverse() {
|
|
2785
|
+
const refs = this.getControlPointRefs();
|
|
2786
|
+
const n = refs.length;
|
|
2787
|
+
const snapshot = refs.map((p) => p.clone());
|
|
2788
|
+
for (let i = 0; i < n; i++) {
|
|
2789
|
+
refs[i].copyFrom(snapshot[n - 1 - i]);
|
|
2790
|
+
}
|
|
2791
|
+
this.invalidate();
|
|
2792
|
+
return this;
|
|
2793
|
+
}
|
|
2659
2794
|
applyTransform(transform) {
|
|
2660
2795
|
const isFunction = typeof transform === "function";
|
|
2661
2796
|
this.getControlPointRefs().forEach((p) => {
|
|
@@ -2665,6 +2800,7 @@ class Curve {
|
|
|
2665
2800
|
transform.apply(p, p);
|
|
2666
2801
|
}
|
|
2667
2802
|
});
|
|
2803
|
+
this.invalidate();
|
|
2668
2804
|
return this;
|
|
2669
2805
|
}
|
|
2670
2806
|
getUnevenVertices(count = 5, output = []) {
|
|
@@ -2782,6 +2918,22 @@ class Curve {
|
|
|
2782
2918
|
getTangentAt(u, output) {
|
|
2783
2919
|
return this.getTangent(this.getUToTMapping(u), output);
|
|
2784
2920
|
}
|
|
2921
|
+
/**
|
|
2922
|
+
* PathKit-style sample at an absolute arc-length `distance` along the curve: the point, the unit
|
|
2923
|
+
* tangent, and the tangent `angle` in radians. `distance` is clamped to `[0, getLength()]`, so
|
|
2924
|
+
* passing `0`/`getLength()` always yields the endpoints. See {@link PathMeasure} for a wrapper.
|
|
2925
|
+
*/
|
|
2926
|
+
getPosTan(distance) {
|
|
2927
|
+
const length = this.getLength();
|
|
2928
|
+
const u = length > 0 ? Math.min(Math.max(distance / length, 0), 1) : 0;
|
|
2929
|
+
const t = this.getUToTMapping(u);
|
|
2930
|
+
const tangent = this.getTangent(t);
|
|
2931
|
+
return {
|
|
2932
|
+
position: this.getPoint(t),
|
|
2933
|
+
tangent,
|
|
2934
|
+
angle: Math.atan2(tangent.y, tangent.x)
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2785
2937
|
getNormal(t, output = new Vector2()) {
|
|
2786
2938
|
this.getTangent(t, output);
|
|
2787
2939
|
return output.set(-output.y, output.x).normalize();
|
|
@@ -2809,12 +2961,25 @@ class Curve {
|
|
|
2809
2961
|
return mid;
|
|
2810
2962
|
}
|
|
2811
2963
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
2812
|
-
const
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2964
|
+
const vertices = this.getAdaptiveVertices();
|
|
2965
|
+
let minX = min.x;
|
|
2966
|
+
let minY = min.y;
|
|
2967
|
+
let maxX = max.x;
|
|
2968
|
+
let maxY = max.y;
|
|
2969
|
+
for (let i = 0, len = vertices.length; i < len; i += 2) {
|
|
2970
|
+
const x = vertices[i];
|
|
2971
|
+
const y = vertices[i + 1];
|
|
2972
|
+
if (x < minX)
|
|
2973
|
+
minX = x;
|
|
2974
|
+
if (y < minY)
|
|
2975
|
+
minY = y;
|
|
2976
|
+
if (x > maxX)
|
|
2977
|
+
maxX = x;
|
|
2978
|
+
if (y > maxY)
|
|
2979
|
+
maxY = y;
|
|
2817
2980
|
}
|
|
2981
|
+
min.set(minX, minY);
|
|
2982
|
+
max.set(maxX, maxY);
|
|
2818
2983
|
return { min: min.finite(), max: max.finite() };
|
|
2819
2984
|
}
|
|
2820
2985
|
getBoundingBox() {
|
|
@@ -2832,7 +2997,7 @@ class Curve {
|
|
|
2832
2997
|
* are honored — a single `Curve` is always one ring.
|
|
2833
2998
|
*/
|
|
2834
2999
|
isPointInFill(point, options = {}) {
|
|
2835
|
-
return pointInPolygon(point, this.
|
|
3000
|
+
return pointInPolygon(point, this._getCachedAdaptiveVertices(), options.fillRule);
|
|
2836
3001
|
}
|
|
2837
3002
|
/**
|
|
2838
3003
|
* Test whether a point lies on this curve's stroke, i.e. within `strokeWidth / 2 + tolerance`
|
|
@@ -2845,7 +3010,7 @@ class Curve {
|
|
|
2845
3010
|
*/
|
|
2846
3011
|
isPointInStroke(point, options = {}) {
|
|
2847
3012
|
const { strokeWidth = 1, tolerance = 0, closed = false } = options;
|
|
2848
|
-
const distance = pointToPolylineDistance(point, this.
|
|
3013
|
+
const distance = pointToPolylineDistance(point, this._getCachedAdaptiveVertices(), closed);
|
|
2849
3014
|
return distance <= strokeWidth / 2 + tolerance;
|
|
2850
3015
|
}
|
|
2851
3016
|
/**
|
|
@@ -2864,10 +3029,28 @@ class Curve {
|
|
|
2864
3029
|
options
|
|
2865
3030
|
);
|
|
2866
3031
|
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Whether this curve forms a closed loop (its outline should be stroked without end caps,
|
|
3034
|
+
* stitching the last vertex back to the first). The base test is purely geometric — the first
|
|
3035
|
+
* sampled vertex coincides with the last. Curves that close without a duplicated endpoint
|
|
3036
|
+
* (a full-revolution {@link RoundCurve}, rectangles, polygons) override this.
|
|
3037
|
+
*/
|
|
3038
|
+
isClosed() {
|
|
3039
|
+
const v = this._getCachedAdaptiveVertices();
|
|
3040
|
+
const len = v.length;
|
|
3041
|
+
if (len < 6) {
|
|
3042
|
+
return false;
|
|
3043
|
+
}
|
|
3044
|
+
const eps = 1e-4;
|
|
3045
|
+
return Math.abs(v[0] - v[len - 2]) < eps && Math.abs(v[1] - v[len - 1]) < eps;
|
|
3046
|
+
}
|
|
2867
3047
|
strokeTriangulate(options) {
|
|
2868
3048
|
return strokeTriangulate(
|
|
2869
3049
|
this.getAdaptiveVertices(),
|
|
2870
|
-
|
|
3050
|
+
{
|
|
3051
|
+
...options,
|
|
3052
|
+
closed: options?.closed ?? this.isClosed()
|
|
3053
|
+
}
|
|
2871
3054
|
);
|
|
2872
3055
|
}
|
|
2873
3056
|
toCommands() {
|
|
@@ -2962,6 +3145,22 @@ class RoundCurve extends Curve {
|
|
|
2962
3145
|
isClockwise() {
|
|
2963
3146
|
return this.clockwise;
|
|
2964
3147
|
}
|
|
3148
|
+
/**
|
|
3149
|
+
* A circle/ellipse arc is closed when it sweeps (at least) a full revolution — the sampled
|
|
3150
|
+
* outline does not duplicate the start vertex, so the geometric first==last test in the base
|
|
3151
|
+
* class would wrongly report a full circle as open and leave a seam gap in the stroke.
|
|
3152
|
+
*/
|
|
3153
|
+
isClosed() {
|
|
3154
|
+
return Math.abs(this.endAngle - this.startAngle) >= Math.PI * 2 - 1e-9 || super.isClosed();
|
|
3155
|
+
}
|
|
3156
|
+
reverse() {
|
|
3157
|
+
const { startAngle, endAngle } = this;
|
|
3158
|
+
this.startAngle = endAngle;
|
|
3159
|
+
this.endAngle = startAngle;
|
|
3160
|
+
this.clockwise = !this.clockwise;
|
|
3161
|
+
this.invalidate();
|
|
3162
|
+
return this;
|
|
3163
|
+
}
|
|
2965
3164
|
_getDeltaAngle() {
|
|
2966
3165
|
const PI_2 = Math.PI * 2;
|
|
2967
3166
|
let deltaAngle = this.endAngle - this.startAngle;
|
|
@@ -2989,6 +3188,64 @@ class RoundCurve extends Curve {
|
|
|
2989
3188
|
}
|
|
2990
3189
|
return output.set(_x, _y);
|
|
2991
3190
|
}
|
|
3191
|
+
/**
|
|
3192
|
+
* Point on the ellipse at an absolute angle (mirrors {@link getPoint}'s parameterization,
|
|
3193
|
+
* ignoring `_diff`).
|
|
3194
|
+
*/
|
|
3195
|
+
_pointAtAngle(angle, output) {
|
|
3196
|
+
let x = this.cx + this.rx * Math.cos(angle);
|
|
3197
|
+
let y = this.cy + this.ry * Math.sin(angle);
|
|
3198
|
+
if (this.rotate !== 0) {
|
|
3199
|
+
const cos = Math.cos(this.rotate);
|
|
3200
|
+
const sin = Math.sin(this.rotate);
|
|
3201
|
+
const tx = x - this.cx;
|
|
3202
|
+
const ty = y - this.cy;
|
|
3203
|
+
x = tx * cos - ty * sin + this.cx;
|
|
3204
|
+
y = tx * sin + ty * cos + this.cy;
|
|
3205
|
+
}
|
|
3206
|
+
return output.set(x, y);
|
|
3207
|
+
}
|
|
3208
|
+
/**
|
|
3209
|
+
* Analytical bounds of the (elliptical) arc: the start/end points plus the per-axis
|
|
3210
|
+
* extrema angles that fall within the swept interval. Matches {@link getPoint}, so it is
|
|
3211
|
+
* exact for `ArcCurve`/`EllipseCurve`. The `_diff` offset (used only by the legacy
|
|
3212
|
+
* `_getAdaptiveVerticesByCircle` path) is intentionally ignored here.
|
|
3213
|
+
*/
|
|
3214
|
+
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
3215
|
+
const { startAngle, rotate } = this;
|
|
3216
|
+
const delta = this._getDeltaAngle();
|
|
3217
|
+
const cosT = Math.cos(rotate);
|
|
3218
|
+
const sinT = Math.sin(rotate);
|
|
3219
|
+
const p = tempV2;
|
|
3220
|
+
let minX = min.x;
|
|
3221
|
+
let minY = min.y;
|
|
3222
|
+
let maxX = max.x;
|
|
3223
|
+
let maxY = max.y;
|
|
3224
|
+
const consider = (angle) => {
|
|
3225
|
+
this._pointAtAngle(angle, p);
|
|
3226
|
+
if (p.x < minX)
|
|
3227
|
+
minX = p.x;
|
|
3228
|
+
if (p.y < minY)
|
|
3229
|
+
minY = p.y;
|
|
3230
|
+
if (p.x > maxX)
|
|
3231
|
+
maxX = p.x;
|
|
3232
|
+
if (p.y > maxY)
|
|
3233
|
+
maxY = p.y;
|
|
3234
|
+
};
|
|
3235
|
+
consider(startAngle);
|
|
3236
|
+
consider(startAngle + delta);
|
|
3237
|
+
const ax = Math.atan2(-this.ry * sinT, this.rx * cosT);
|
|
3238
|
+
const ay = Math.atan2(this.ry * cosT, this.rx * sinT);
|
|
3239
|
+
const bases = [ax, ax + Math.PI, ay, ay + Math.PI];
|
|
3240
|
+
for (let i = 0; i < 4; i++) {
|
|
3241
|
+
if (angleInSweep(bases[i], startAngle, delta)) {
|
|
3242
|
+
consider(bases[i]);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
min.set(minX, minY);
|
|
3246
|
+
max.set(maxX, maxY);
|
|
3247
|
+
return { min: min.finite(), max: max.finite() };
|
|
3248
|
+
}
|
|
2992
3249
|
toCommands() {
|
|
2993
3250
|
const { cx, cy, rx, ry, startAngle, endAngle, clockwise, rotate } = this;
|
|
2994
3251
|
const startX = cx + rx * Math.cos(startAngle) * Math.cos(rotate) - ry * Math.sin(startAngle) * Math.sin(rotate);
|
|
@@ -3039,6 +3296,7 @@ class RoundCurve extends Curve {
|
|
|
3039
3296
|
} else {
|
|
3040
3297
|
transfEllipseNoSkew(this, transform);
|
|
3041
3298
|
}
|
|
3299
|
+
this.invalidate();
|
|
3042
3300
|
return this;
|
|
3043
3301
|
}
|
|
3044
3302
|
getControlPointRefs() {
|
|
@@ -3159,9 +3417,25 @@ class RoundCurve extends Curve {
|
|
|
3159
3417
|
return output;
|
|
3160
3418
|
}
|
|
3161
3419
|
getAdaptiveVertices(output = []) {
|
|
3162
|
-
|
|
3420
|
+
const PI2 = Math.PI * 2;
|
|
3421
|
+
if (this.startAngle === 0 && this.endAngle === PI2) {
|
|
3163
3422
|
return this._getAdaptiveVerticesByCircle(output);
|
|
3164
3423
|
}
|
|
3424
|
+
if (Math.abs(this.endAngle - this.startAngle) >= PI2 - 1e-9) {
|
|
3425
|
+
const tmp = this._getAdaptiveVerticesByCircle([]);
|
|
3426
|
+
const n = tmp.length / 2;
|
|
3427
|
+
if (this.endAngle > this.startAngle) {
|
|
3428
|
+
for (let i = 0; i < tmp.length; i++) {
|
|
3429
|
+
output.push(tmp[i]);
|
|
3430
|
+
}
|
|
3431
|
+
} else {
|
|
3432
|
+
output.push(tmp[0], tmp[1]);
|
|
3433
|
+
for (let i = n - 1; i >= 1; i--) {
|
|
3434
|
+
output.push(tmp[i * 2], tmp[i * 2 + 1]);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
return output;
|
|
3438
|
+
}
|
|
3165
3439
|
return this._getAdaptiveVerticesByArc(output);
|
|
3166
3440
|
}
|
|
3167
3441
|
copyFrom(source) {
|
|
@@ -3179,6 +3453,23 @@ class RoundCurve extends Curve {
|
|
|
3179
3453
|
return this;
|
|
3180
3454
|
}
|
|
3181
3455
|
}
|
|
3456
|
+
function angleInSweep(a, start, delta) {
|
|
3457
|
+
const PI_2 = Math.PI * 2;
|
|
3458
|
+
const eps = 1e-9;
|
|
3459
|
+
if (Math.abs(delta) >= PI_2 - eps) {
|
|
3460
|
+
return true;
|
|
3461
|
+
}
|
|
3462
|
+
let off = (a - start) % PI_2;
|
|
3463
|
+
if (delta >= 0) {
|
|
3464
|
+
if (off < -eps)
|
|
3465
|
+
off += PI_2;
|
|
3466
|
+
return off >= -eps && off <= delta + eps;
|
|
3467
|
+
}
|
|
3468
|
+
if (off > eps) {
|
|
3469
|
+
off -= PI_2;
|
|
3470
|
+
}
|
|
3471
|
+
return off <= eps && off >= delta - eps;
|
|
3472
|
+
}
|
|
3182
3473
|
function transfEllipseGeneric(curve, m) {
|
|
3183
3474
|
const a = curve.rx;
|
|
3184
3475
|
const b = curve.ry;
|
|
@@ -3354,6 +3645,15 @@ class LineCurve extends Curve {
|
|
|
3354
3645
|
getControlPointRefs() {
|
|
3355
3646
|
return [this.p1, this.p2];
|
|
3356
3647
|
}
|
|
3648
|
+
// Swap endpoint *references* (not values) so corner Vector2s shared with adjacent
|
|
3649
|
+
// segments stay intact and simply re-associate with the reversed segment.
|
|
3650
|
+
reverse() {
|
|
3651
|
+
const { p1, p2 } = this;
|
|
3652
|
+
this.p1 = p2;
|
|
3653
|
+
this.p2 = p1;
|
|
3654
|
+
this.invalidate();
|
|
3655
|
+
return this;
|
|
3656
|
+
}
|
|
3357
3657
|
getAdaptiveVertices(output = []) {
|
|
3358
3658
|
output.push(
|
|
3359
3659
|
this.p1.x,
|
|
@@ -3417,6 +3717,22 @@ class CompositeCurve extends Curve {
|
|
|
3417
3717
|
super();
|
|
3418
3718
|
this.curves = curves;
|
|
3419
3719
|
}
|
|
3720
|
+
_adaptiveCacheLen = -1;
|
|
3721
|
+
_invalidateSelf() {
|
|
3722
|
+
super._invalidateSelf();
|
|
3723
|
+
this._adaptiveCacheLen = -1;
|
|
3724
|
+
this.curves.forEach((curve) => curve.invalidate());
|
|
3725
|
+
}
|
|
3726
|
+
_getCachedAdaptiveVertices() {
|
|
3727
|
+
if (!this._adaptiveCache || this._adaptiveCacheLen !== this.curves.length) {
|
|
3728
|
+
this.curves.forEach((curve) => {
|
|
3729
|
+
curve._owner = this;
|
|
3730
|
+
});
|
|
3731
|
+
this._adaptiveCache = this.getAdaptiveVertices();
|
|
3732
|
+
this._adaptiveCacheLen = this.curves.length;
|
|
3733
|
+
}
|
|
3734
|
+
return this._adaptiveCache;
|
|
3735
|
+
}
|
|
3420
3736
|
getFlatCurves() {
|
|
3421
3737
|
return this.curves.flatMap((curve) => {
|
|
3422
3738
|
if (curve instanceof CompositeCurve) {
|
|
@@ -3462,6 +3778,7 @@ class CompositeCurve extends Curve {
|
|
|
3462
3778
|
updateLengths() {
|
|
3463
3779
|
const lengths = [];
|
|
3464
3780
|
for (let i = 0, sum = 0, len = this.curves.length; i < len; i++) {
|
|
3781
|
+
this.curves[i]._owner = this;
|
|
3465
3782
|
sum += this.curves[i].getLength();
|
|
3466
3783
|
lengths.push(sum);
|
|
3467
3784
|
}
|
|
@@ -3500,6 +3817,16 @@ class CompositeCurve extends Curve {
|
|
|
3500
3817
|
});
|
|
3501
3818
|
return output;
|
|
3502
3819
|
}
|
|
3820
|
+
/**
|
|
3821
|
+
* A composite is closed when its single child is closed (e.g. a lone full-circle arc), or when
|
|
3822
|
+
* its assembled outline returns to its start (rectangles, polygons, multi-segment loops).
|
|
3823
|
+
*/
|
|
3824
|
+
isClosed() {
|
|
3825
|
+
if (this.curves.length === 1) {
|
|
3826
|
+
return this.curves[0].isClosed();
|
|
3827
|
+
}
|
|
3828
|
+
return super.isClosed();
|
|
3829
|
+
}
|
|
3503
3830
|
strokeTriangulate(options) {
|
|
3504
3831
|
if (this.curves.length === 1) {
|
|
3505
3832
|
return this.curves[0].strokeTriangulate(options);
|
|
@@ -3507,6 +3834,13 @@ class CompositeCurve extends Curve {
|
|
|
3507
3834
|
return super.strokeTriangulate(options);
|
|
3508
3835
|
}
|
|
3509
3836
|
}
|
|
3837
|
+
/** Reverse the sub-curve order and reverse each sub-curve, so the whole outline runs backwards. */
|
|
3838
|
+
reverse() {
|
|
3839
|
+
this.curves.reverse();
|
|
3840
|
+
this.curves.forEach((curve) => curve.reverse());
|
|
3841
|
+
this.invalidate();
|
|
3842
|
+
return this;
|
|
3843
|
+
}
|
|
3510
3844
|
getFillVertices(options) {
|
|
3511
3845
|
if (this.curves.length === 1) {
|
|
3512
3846
|
return this.curves[0].getFillVertices(options);
|
|
@@ -3530,7 +3864,10 @@ class CompositeCurve extends Curve {
|
|
|
3530
3864
|
}
|
|
3531
3865
|
}
|
|
3532
3866
|
applyTransform(transform) {
|
|
3867
|
+
this._invalidating = true;
|
|
3533
3868
|
this.curves.forEach((curve) => curve.applyTransform(transform));
|
|
3869
|
+
this._invalidating = false;
|
|
3870
|
+
this.invalidate();
|
|
3534
3871
|
return this;
|
|
3535
3872
|
}
|
|
3536
3873
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
@@ -3599,6 +3936,16 @@ class CubicBezierCurve extends Curve {
|
|
|
3599
3936
|
getControlPointRefs() {
|
|
3600
3937
|
return [this.p1, this.cp1, this.cp2, this.p2];
|
|
3601
3938
|
}
|
|
3939
|
+
// Swap endpoint and control-point references; keeps shared corner Vector2s intact.
|
|
3940
|
+
reverse() {
|
|
3941
|
+
const { p1, cp1, cp2, p2 } = this;
|
|
3942
|
+
this.p1 = p2;
|
|
3943
|
+
this.cp1 = cp2;
|
|
3944
|
+
this.cp2 = cp1;
|
|
3945
|
+
this.p2 = p1;
|
|
3946
|
+
this.invalidate();
|
|
3947
|
+
return this;
|
|
3948
|
+
}
|
|
3602
3949
|
_solveQuadratic(a, b, c) {
|
|
3603
3950
|
if (Math.abs(a) < 1e-12) {
|
|
3604
3951
|
if (Math.abs(b) < 1e-12)
|
|
@@ -3759,6 +4106,14 @@ class QuadraticBezierCurve extends Curve {
|
|
|
3759
4106
|
getControlPointRefs() {
|
|
3760
4107
|
return [this.p1, this.cp, this.p2];
|
|
3761
4108
|
}
|
|
4109
|
+
// Swap endpoint references (cp is symmetric); keeps shared corner Vector2s intact.
|
|
4110
|
+
reverse() {
|
|
4111
|
+
const { p1, p2 } = this;
|
|
4112
|
+
this.p1 = p2;
|
|
4113
|
+
this.p2 = p1;
|
|
4114
|
+
this.invalidate();
|
|
4115
|
+
return this;
|
|
4116
|
+
}
|
|
3762
4117
|
getAdaptiveVertices(output = []) {
|
|
3763
4118
|
return getAdaptiveQuadraticBezierCurvePoints(
|
|
3764
4119
|
this.p1.x,
|
|
@@ -3869,7 +4224,7 @@ class RectangleCurve extends PolygonCurve {
|
|
|
3869
4224
|
}
|
|
3870
4225
|
}
|
|
3871
4226
|
|
|
3872
|
-
class RoundRectangleCurve extends
|
|
4227
|
+
class RoundRectangleCurve extends CompositeCurve {
|
|
3873
4228
|
constructor(x = 0, y = 0, width = 1, height = 1, radius = 1) {
|
|
3874
4229
|
super();
|
|
3875
4230
|
this.x = x;
|
|
@@ -3880,25 +4235,53 @@ class RoundRectangleCurve extends RoundCurve {
|
|
|
3880
4235
|
this.update();
|
|
3881
4236
|
}
|
|
3882
4237
|
update() {
|
|
3883
|
-
const { x, y, width, height
|
|
3884
|
-
const
|
|
3885
|
-
const
|
|
3886
|
-
const
|
|
3887
|
-
const
|
|
3888
|
-
const
|
|
3889
|
-
const
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
4238
|
+
const { x, y, width, height } = this;
|
|
4239
|
+
const r = Math.max(0, Math.min(this.radius, Math.abs(width) / 2, Math.abs(height) / 2));
|
|
4240
|
+
const x0 = x;
|
|
4241
|
+
const x1 = x + r;
|
|
4242
|
+
const x2 = x + width - r;
|
|
4243
|
+
const x3 = x + width;
|
|
4244
|
+
const y0 = y;
|
|
4245
|
+
const y1 = y + r;
|
|
4246
|
+
const y2 = y + height - r;
|
|
4247
|
+
const y3 = y + height;
|
|
4248
|
+
if (r <= 0) {
|
|
4249
|
+
this.curves = [
|
|
4250
|
+
LineCurve.from(x0, y0, x3, y0),
|
|
4251
|
+
LineCurve.from(x3, y0, x3, y3),
|
|
4252
|
+
LineCurve.from(x3, y3, x0, y3),
|
|
4253
|
+
LineCurve.from(x0, y3, x0, y0)
|
|
4254
|
+
];
|
|
4255
|
+
} else {
|
|
4256
|
+
const HALF_PI = Math.PI / 2;
|
|
4257
|
+
this.curves = [
|
|
4258
|
+
LineCurve.from(x1, y0, x2, y0),
|
|
4259
|
+
// top edge
|
|
4260
|
+
new ArcCurve(x2, y1, r, -HALF_PI, 0, true),
|
|
4261
|
+
// top-right corner
|
|
4262
|
+
LineCurve.from(x3, y1, x3, y2),
|
|
4263
|
+
// right edge
|
|
4264
|
+
new ArcCurve(x2, y2, r, 0, HALF_PI, true),
|
|
4265
|
+
// bottom-right corner
|
|
4266
|
+
LineCurve.from(x2, y3, x1, y3),
|
|
4267
|
+
// bottom edge
|
|
4268
|
+
new ArcCurve(x1, y2, r, HALF_PI, Math.PI, true),
|
|
4269
|
+
// bottom-left corner
|
|
4270
|
+
LineCurve.from(x0, y2, x0, y1),
|
|
4271
|
+
// left edge
|
|
4272
|
+
new ArcCurve(x1, y1, r, Math.PI, Math.PI * 1.5, true)
|
|
4273
|
+
// top-left corner
|
|
4274
|
+
];
|
|
4275
|
+
}
|
|
4276
|
+
this.invalidate();
|
|
3893
4277
|
return this;
|
|
3894
4278
|
}
|
|
3895
4279
|
drawTo(ctx) {
|
|
3896
|
-
|
|
3897
|
-
ctx.roundRect(x, y, width, height, radius);
|
|
4280
|
+
ctx.roundRect(this.x, this.y, this.width, this.height, this.radius);
|
|
3898
4281
|
return this;
|
|
3899
4282
|
}
|
|
3900
4283
|
copyFrom(source) {
|
|
3901
|
-
|
|
4284
|
+
this.arcLengthDivision = source.arcLengthDivision;
|
|
3902
4285
|
this.x = source.x;
|
|
3903
4286
|
this.y = source.y;
|
|
3904
4287
|
this.width = source.width;
|
|
@@ -3932,6 +4315,11 @@ class SplineCurve extends Curve {
|
|
|
3932
4315
|
getControlPointRefs() {
|
|
3933
4316
|
return this.points;
|
|
3934
4317
|
}
|
|
4318
|
+
reverse() {
|
|
4319
|
+
this.points.reverse();
|
|
4320
|
+
this.invalidate();
|
|
4321
|
+
return this;
|
|
4322
|
+
}
|
|
3935
4323
|
copyFrom(source) {
|
|
3936
4324
|
super.copyFrom(source);
|
|
3937
4325
|
this.points = [];
|
|
@@ -3968,6 +4356,22 @@ class CurvePath extends CompositeCurve {
|
|
|
3968
4356
|
this.addCommands(svgPathDataToCommands(data));
|
|
3969
4357
|
return this;
|
|
3970
4358
|
}
|
|
4359
|
+
/**
|
|
4360
|
+
* A sub-path is closed if it was explicitly closed (`autoClose`, i.e. a `Z`/`closePath`), or if
|
|
4361
|
+
* it forms a geometric loop / wraps a single closed primitive (handled by the base class).
|
|
4362
|
+
*/
|
|
4363
|
+
isClosed() {
|
|
4364
|
+
return this.autoClose || super.isClosed();
|
|
4365
|
+
}
|
|
4366
|
+
/** Reverse direction, then refresh the {@link startPoint}/{@link currentPoint} cursors. */
|
|
4367
|
+
reverse() {
|
|
4368
|
+
super.reverse();
|
|
4369
|
+
if (this.curves.length) {
|
|
4370
|
+
this.startPoint = this.getPoint(0);
|
|
4371
|
+
this.currentPoint = this.getPoint(1);
|
|
4372
|
+
}
|
|
4373
|
+
return this;
|
|
4374
|
+
}
|
|
3971
4375
|
_closeVertices(output) {
|
|
3972
4376
|
if (this.autoClose && output.length >= 4 && (output[0] !== output[output.length - 2] && output[1] !== output[output.length - 1])) {
|
|
3973
4377
|
output.push(output[0], output[1]);
|
|
@@ -4000,7 +4404,7 @@ class CurvePath extends CompositeCurve {
|
|
|
4000
4404
|
*/
|
|
4001
4405
|
isPointInStroke(point, options = {}) {
|
|
4002
4406
|
const { strokeWidth = 1, tolerance = 0 } = options;
|
|
4003
|
-
const vertices = this.
|
|
4407
|
+
const vertices = this._getCachedAdaptiveVertices();
|
|
4004
4408
|
const len = vertices.length;
|
|
4005
4409
|
const closed = options.closed ?? (this.autoClose || len >= 6 && vertices[0] === vertices[len - 2] && vertices[1] === vertices[len - 1]);
|
|
4006
4410
|
return pointToPolylineDistance(point, vertices, closed) <= strokeWidth / 2 + tolerance;
|
|
@@ -4178,6 +4582,8 @@ class CurvePath extends CompositeCurve {
|
|
|
4178
4582
|
|
|
4179
4583
|
class Path2D extends CompositeCurve {
|
|
4180
4584
|
_meta;
|
|
4585
|
+
_ringsCache;
|
|
4586
|
+
_ringsCacheLen = -1;
|
|
4181
4587
|
currentCurve = new CurvePath();
|
|
4182
4588
|
style;
|
|
4183
4589
|
get startPoint() {
|
|
@@ -4296,18 +4702,21 @@ class Path2D extends CompositeCurve {
|
|
|
4296
4702
|
this.getControlPointRefs().forEach((point) => {
|
|
4297
4703
|
point.scale(sx, sy, target);
|
|
4298
4704
|
});
|
|
4705
|
+
this.invalidate();
|
|
4299
4706
|
return this;
|
|
4300
4707
|
}
|
|
4301
4708
|
skew(ax, ay = 0, target = { x: 0, y: 0 }) {
|
|
4302
4709
|
this.getControlPointRefs().forEach((point) => {
|
|
4303
4710
|
point.skew(ax, ay, target);
|
|
4304
4711
|
});
|
|
4712
|
+
this.invalidate();
|
|
4305
4713
|
return this;
|
|
4306
4714
|
}
|
|
4307
4715
|
rotate(rad, target = { x: 0, y: 0 }) {
|
|
4308
4716
|
this.getControlPointRefs().forEach((point) => {
|
|
4309
4717
|
point.rotate(rad, target);
|
|
4310
4718
|
});
|
|
4719
|
+
this.invalidate();
|
|
4311
4720
|
return this;
|
|
4312
4721
|
}
|
|
4313
4722
|
bold(b) {
|
|
@@ -4366,6 +4775,7 @@ class Path2D extends CompositeCurve {
|
|
|
4366
4775
|
}
|
|
4367
4776
|
});
|
|
4368
4777
|
});
|
|
4778
|
+
this.invalidate();
|
|
4369
4779
|
return this;
|
|
4370
4780
|
}
|
|
4371
4781
|
/**
|
|
@@ -4378,13 +4788,70 @@ class Path2D extends CompositeCurve {
|
|
|
4378
4788
|
*
|
|
4379
4789
|
* Defaults `fillRule` to `style.fillRule`, then `'nonzero'` (matching SVG/Canvas).
|
|
4380
4790
|
*/
|
|
4791
|
+
_invalidateSelf() {
|
|
4792
|
+
super._invalidateSelf();
|
|
4793
|
+
this._ringsCache = void 0;
|
|
4794
|
+
this._ringsCacheLen = -1;
|
|
4795
|
+
}
|
|
4796
|
+
/** Per-sub-path sampled rings, cached for repeated hit tests. */
|
|
4797
|
+
_getRings() {
|
|
4798
|
+
if (!this._ringsCache || this._ringsCacheLen !== this.curves.length) {
|
|
4799
|
+
this._ringsCache = this.curves.map((curve) => {
|
|
4800
|
+
curve._owner = this;
|
|
4801
|
+
return curve.getAdaptiveVertices();
|
|
4802
|
+
});
|
|
4803
|
+
this._ringsCacheLen = this.curves.length;
|
|
4804
|
+
}
|
|
4805
|
+
return this._ringsCache;
|
|
4806
|
+
}
|
|
4381
4807
|
isPointInFill(point, options = {}) {
|
|
4382
4808
|
const fillRule = options.fillRule ?? this.style.fillRule ?? "nonzero";
|
|
4383
|
-
return pointInPolygons(
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
);
|
|
4809
|
+
return pointInPolygons(point, this._getRings(), fillRule);
|
|
4810
|
+
}
|
|
4811
|
+
/** Build a `Path2D` from flat rings (`[x0,y0,…]` per sub-path); closed-and-filled as sub-paths. */
|
|
4812
|
+
static fromRings(rings, style = {}) {
|
|
4813
|
+
const path = new Path2D(void 0, style);
|
|
4814
|
+
for (const ring of rings) {
|
|
4815
|
+
if (ring.length < 6) {
|
|
4816
|
+
continue;
|
|
4817
|
+
}
|
|
4818
|
+
let end = ring.length;
|
|
4819
|
+
if (ring[0] === ring[end - 2] && ring[1] === ring[end - 1]) {
|
|
4820
|
+
end -= 2;
|
|
4821
|
+
}
|
|
4822
|
+
path.moveTo(ring[0], ring[1]);
|
|
4823
|
+
for (let i = 2; i < end; i += 2) {
|
|
4824
|
+
path.lineTo(ring[i], ring[i + 1]);
|
|
4825
|
+
}
|
|
4826
|
+
path.closePath();
|
|
4827
|
+
}
|
|
4828
|
+
return path;
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* Boolean (path) operation against another path, returning a NEW `Path2D` whose outline is the
|
|
4832
|
+
* polygonal result. Curves are sampled before clipping, so the result is a polygonal
|
|
4833
|
+
* approximation (see {@link polygonBoolean}). The result inherits this path's `style` unless
|
|
4834
|
+
* overridden via `style`. Holes are emitted as oppositely-wound sub-paths (nonzero fill).
|
|
4835
|
+
*/
|
|
4836
|
+
booleanOp(op, other, style) {
|
|
4837
|
+
const rings = polygonBoolean(op, this._getRings(), other._getRings());
|
|
4838
|
+
return Path2D.fromRings(rings, { ...this.style, ...style });
|
|
4839
|
+
}
|
|
4840
|
+
/** `this ∪ other` — the combined filled area. */
|
|
4841
|
+
union(other, style) {
|
|
4842
|
+
return this.booleanOp("union", other, style);
|
|
4843
|
+
}
|
|
4844
|
+
/** `this ∩ other` — only the overlapping area. */
|
|
4845
|
+
intersection(other, style) {
|
|
4846
|
+
return this.booleanOp("intersection", other, style);
|
|
4847
|
+
}
|
|
4848
|
+
/** `this − other` — this path with `other` cut away. */
|
|
4849
|
+
difference(other, style) {
|
|
4850
|
+
return this.booleanOp("difference", other, style);
|
|
4851
|
+
}
|
|
4852
|
+
/** `this ⊕ other` — areas covered by exactly one of the two paths. */
|
|
4853
|
+
xor(other, style) {
|
|
4854
|
+
return this.booleanOp("xor", other, style);
|
|
4388
4855
|
}
|
|
4389
4856
|
/**
|
|
4390
4857
|
* Test whether a point lies on this path's stroke. A hit on any sub-path counts.
|
|
@@ -4403,35 +4870,17 @@ class Path2D extends CompositeCurve {
|
|
|
4403
4870
|
}));
|
|
4404
4871
|
}
|
|
4405
4872
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN, withStyle = true) {
|
|
4406
|
-
const strokeWidth = this.strokeWidth;
|
|
4407
4873
|
this.curves.forEach((curve) => {
|
|
4408
4874
|
curve.getMinMax(min, max);
|
|
4409
|
-
if (withStyle) {
|
|
4410
|
-
if (strokeWidth > 1) {
|
|
4411
|
-
const halfStrokeWidth = strokeWidth / 2;
|
|
4412
|
-
const isClockwise = curve.isClockwise();
|
|
4413
|
-
const points = [];
|
|
4414
|
-
for (let t = 0; t <= 1; t += 1 / curve.arcLengthDivision) {
|
|
4415
|
-
const point = curve.getPoint(t);
|
|
4416
|
-
const normal = curve.getNormal(t);
|
|
4417
|
-
const dist1 = normal.clone().scale(isClockwise ? halfStrokeWidth : -halfStrokeWidth);
|
|
4418
|
-
const dist2 = normal.clone().scale(isClockwise ? -halfStrokeWidth : halfStrokeWidth);
|
|
4419
|
-
points.push(
|
|
4420
|
-
point.clone().add(dist1),
|
|
4421
|
-
point.clone().add(dist2),
|
|
4422
|
-
point.clone().add({ x: halfStrokeWidth, y: 0 }),
|
|
4423
|
-
point.clone().add({ x: -halfStrokeWidth, y: 0 }),
|
|
4424
|
-
point.clone().add({ x: 0, y: halfStrokeWidth }),
|
|
4425
|
-
point.clone().add({ x: 0, y: -halfStrokeWidth }),
|
|
4426
|
-
point.clone().add({ x: halfStrokeWidth, y: halfStrokeWidth }),
|
|
4427
|
-
point.clone().add({ x: -halfStrokeWidth, y: -halfStrokeWidth })
|
|
4428
|
-
);
|
|
4429
|
-
}
|
|
4430
|
-
min.clampMin(...points);
|
|
4431
|
-
max.clampMax(...points);
|
|
4432
|
-
}
|
|
4433
|
-
}
|
|
4434
4875
|
});
|
|
4876
|
+
if (withStyle) {
|
|
4877
|
+
const strokeWidth = this.strokeWidth;
|
|
4878
|
+
if (strokeWidth > 1 && Number.isFinite(min.x)) {
|
|
4879
|
+
const half = strokeWidth / 2;
|
|
4880
|
+
min.set(min.x - half, min.y - half);
|
|
4881
|
+
max.set(max.x + half, max.y + half);
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4435
4884
|
return { min: min.finite(), max: max.finite() };
|
|
4436
4885
|
}
|
|
4437
4886
|
strokeTriangulate(options) {
|
|
@@ -4503,7 +4952,7 @@ class Path2D extends CompositeCurve {
|
|
|
4503
4952
|
}
|
|
4504
4953
|
drawTo(ctx, style = {}) {
|
|
4505
4954
|
style = { ...this.style, ...style };
|
|
4506
|
-
const { fill = "#000", stroke = "none" } = style;
|
|
4955
|
+
const { fill = "#000", stroke = "none", fillRule = "nonzero" } = style;
|
|
4507
4956
|
ctx.beginPath();
|
|
4508
4957
|
ctx.save();
|
|
4509
4958
|
setCanvasContext(ctx, style);
|
|
@@ -4511,7 +4960,7 @@ class Path2D extends CompositeCurve {
|
|
|
4511
4960
|
path.drawTo(ctx);
|
|
4512
4961
|
});
|
|
4513
4962
|
if (fill !== "none") {
|
|
4514
|
-
ctx.fill();
|
|
4963
|
+
ctx.fill(fillRule);
|
|
4515
4964
|
}
|
|
4516
4965
|
if (stroke !== "none") {
|
|
4517
4966
|
ctx.stroke();
|
|
@@ -4717,6 +5166,44 @@ ${content}
|
|
|
4717
5166
|
}
|
|
4718
5167
|
}
|
|
4719
5168
|
|
|
5169
|
+
class PathMeasure {
|
|
5170
|
+
constructor(curve) {
|
|
5171
|
+
this.curve = curve;
|
|
5172
|
+
}
|
|
5173
|
+
/** Total arc length of the path. */
|
|
5174
|
+
getLength() {
|
|
5175
|
+
return this.curve.getLength();
|
|
5176
|
+
}
|
|
5177
|
+
/** Whether the path forms a closed loop (see {@link Curve.isClosed}). */
|
|
5178
|
+
isClosed() {
|
|
5179
|
+
return this.curve.isClosed();
|
|
5180
|
+
}
|
|
5181
|
+
/** Point + unit tangent + tangent angle at an absolute arc-length `distance` (clamped). */
|
|
5182
|
+
getPosTan(distance) {
|
|
5183
|
+
return this.curve.getPosTan(distance);
|
|
5184
|
+
}
|
|
5185
|
+
/** Point at an absolute arc-length `distance` (clamped to `[0, getLength()]`). */
|
|
5186
|
+
getPosition(distance) {
|
|
5187
|
+
return this.curve.getPosTan(distance).position;
|
|
5188
|
+
}
|
|
5189
|
+
/** Point + tangent at a normalized progress `t ∈ [0, 1]` along the path. */
|
|
5190
|
+
getPosTanAtProgress(t) {
|
|
5191
|
+
return this.curve.getPosTan(this.getLength() * t);
|
|
5192
|
+
}
|
|
5193
|
+
/**
|
|
5194
|
+
* Evenly sample the path into `count + 1` {@link PosTan} entries (arc-length spaced), e.g. to
|
|
5195
|
+
* lay glyphs along a path or drive an `animate(progress)`-style traversal.
|
|
5196
|
+
*/
|
|
5197
|
+
sample(count = 100) {
|
|
5198
|
+
const length = this.getLength();
|
|
5199
|
+
const out = [];
|
|
5200
|
+
for (let i = 0; i <= count; i++) {
|
|
5201
|
+
out.push(this.curve.getPosTan(length * i / count));
|
|
5202
|
+
}
|
|
5203
|
+
return out;
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
5206
|
+
|
|
4720
5207
|
class FFDControlGrid {
|
|
4721
5208
|
constructor(rows, cols, width = 1, height = 1) {
|
|
4722
5209
|
this.rows = rows;
|
|
@@ -4772,4 +5259,4 @@ function applyFFD(point, grid, width = grid.width, height = grid.height) {
|
|
|
4772
5259
|
point.set(x, y);
|
|
4773
5260
|
}
|
|
4774
5261
|
|
|
4775
|
-
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 };
|
|
5262
|
+
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 };
|