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.cjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const earcut = require('earcut');
|
|
4
|
+
const polygonClipping = require('polygon-clipping');
|
|
4
5
|
|
|
5
6
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
6
7
|
|
|
7
8
|
const earcut__default = /*#__PURE__*/_interopDefaultCompat(earcut);
|
|
9
|
+
const polygonClipping__default = /*#__PURE__*/_interopDefaultCompat(polygonClipping);
|
|
8
10
|
|
|
9
11
|
function drawPoint(ctx, x, y, options = {}) {
|
|
10
12
|
const { radius = 1 } = options;
|
|
@@ -126,7 +128,10 @@ class Vector2 {
|
|
|
126
128
|
return this.set(this._x * x, this._y * y);
|
|
127
129
|
}
|
|
128
130
|
divide(x = 0, y = x) {
|
|
129
|
-
return this.set(
|
|
131
|
+
return this.set(
|
|
132
|
+
x === 0 ? this._x : this._x / x,
|
|
133
|
+
y === 0 ? this._y : this._y / y
|
|
134
|
+
);
|
|
130
135
|
}
|
|
131
136
|
cross(p) {
|
|
132
137
|
return this._x * p.y - this._y * p.x;
|
|
@@ -300,6 +305,63 @@ class BoundingBox {
|
|
|
300
305
|
}
|
|
301
306
|
}
|
|
302
307
|
|
|
308
|
+
function flatRingToPairs(r) {
|
|
309
|
+
const ring = [];
|
|
310
|
+
for (let i = 0; i < r.length; i += 2) {
|
|
311
|
+
ring.push([r[i], r[i + 1]]);
|
|
312
|
+
}
|
|
313
|
+
const first = ring[0];
|
|
314
|
+
const last = ring[ring.length - 1];
|
|
315
|
+
if (first && last && (first[0] !== last[0] || first[1] !== last[1])) {
|
|
316
|
+
ring.push([first[0], first[1]]);
|
|
317
|
+
}
|
|
318
|
+
return ring;
|
|
319
|
+
}
|
|
320
|
+
function ringsToGeom(rings) {
|
|
321
|
+
const valid = rings.filter((r) => r.length >= 6).map(flatRingToPairs);
|
|
322
|
+
if (!valid.length) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
let geom = [[valid[0]]];
|
|
326
|
+
for (let i = 1; i < valid.length; i++) {
|
|
327
|
+
geom = polygonClipping__default.xor(geom, [[valid[i]]]);
|
|
328
|
+
}
|
|
329
|
+
return geom;
|
|
330
|
+
}
|
|
331
|
+
function geomToRings(geom) {
|
|
332
|
+
const out = [];
|
|
333
|
+
for (const poly of geom) {
|
|
334
|
+
for (const ring of poly) {
|
|
335
|
+
const flat = [];
|
|
336
|
+
for (const [x, y] of ring) {
|
|
337
|
+
flat.push(x, y);
|
|
338
|
+
}
|
|
339
|
+
out.push(flat);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
function polygonBoolean(op, ringsA, ringsB) {
|
|
345
|
+
const a = ringsToGeom(ringsA);
|
|
346
|
+
const b = ringsToGeom(ringsB);
|
|
347
|
+
let res;
|
|
348
|
+
switch (op) {
|
|
349
|
+
case "union":
|
|
350
|
+
res = polygonClipping__default.union(a, b);
|
|
351
|
+
break;
|
|
352
|
+
case "intersection":
|
|
353
|
+
res = polygonClipping__default.intersection(a, b);
|
|
354
|
+
break;
|
|
355
|
+
case "difference":
|
|
356
|
+
res = polygonClipping__default.difference(a, b);
|
|
357
|
+
break;
|
|
358
|
+
case "xor":
|
|
359
|
+
res = polygonClipping__default.xor(a, b);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
return geomToRings(res);
|
|
363
|
+
}
|
|
364
|
+
|
|
303
365
|
function catmullRom(t, p0, p1, p2, p3) {
|
|
304
366
|
const v0 = (p2 - p0) * 0.5;
|
|
305
367
|
const v1 = (p3 - p1) * 0.5;
|
|
@@ -319,17 +381,11 @@ function getIntersectionPoint(p1, p2, q1, q2) {
|
|
|
319
381
|
const q1p1 = q1.clone().sub(p1);
|
|
320
382
|
const crossRS = r.cross(s);
|
|
321
383
|
if (crossRS === 0) {
|
|
322
|
-
return
|
|
323
|
-
(p1.x + q1.x) / 2,
|
|
324
|
-
(p1.y + q1.y) / 2
|
|
325
|
-
);
|
|
384
|
+
return null;
|
|
326
385
|
}
|
|
327
386
|
const t = q1p1.cross(s) / crossRS;
|
|
328
387
|
if (Math.abs(t) > 1) {
|
|
329
|
-
return
|
|
330
|
-
(p1.x + q1.x) / 2,
|
|
331
|
-
(p1.y + q1.y) / 2
|
|
332
|
-
);
|
|
388
|
+
return null;
|
|
333
389
|
}
|
|
334
390
|
return new Vector2(
|
|
335
391
|
p1.x + t * r.x,
|
|
@@ -621,11 +677,16 @@ function distance(p1, p2) {
|
|
|
621
677
|
const dy = p2[1] - p1[1];
|
|
622
678
|
return Math.sqrt(dx * dx + dy * dy);
|
|
623
679
|
}
|
|
680
|
+
function aabbIntersects(a, b) {
|
|
681
|
+
return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY;
|
|
682
|
+
}
|
|
624
683
|
function nonzeroFillRule(paths) {
|
|
625
684
|
const results = paths.map((_, i) => ({ index: i }));
|
|
626
|
-
const
|
|
685
|
+
const bboxes = [];
|
|
686
|
+
const testPointsGroups = paths.map((path, pathIndex) => {
|
|
627
687
|
const len = path.length;
|
|
628
688
|
if (!len) {
|
|
689
|
+
bboxes[pathIndex] = null;
|
|
629
690
|
return [];
|
|
630
691
|
}
|
|
631
692
|
let xMinYAuto = [Number.MAX_SAFE_INTEGER, 0];
|
|
@@ -648,6 +709,12 @@ function nonzeroFillRule(paths) {
|
|
|
648
709
|
xAutoYMax = [x, y];
|
|
649
710
|
}
|
|
650
711
|
}
|
|
712
|
+
bboxes[pathIndex] = {
|
|
713
|
+
minX: xMinYAuto[0],
|
|
714
|
+
minY: xAutoYMin[1],
|
|
715
|
+
maxX: xMaxYAuto[0],
|
|
716
|
+
maxY: xAutoYMax[1]
|
|
717
|
+
};
|
|
651
718
|
const mid = [
|
|
652
719
|
(xMinYAuto[0] + xMaxYAuto[0]) / 2,
|
|
653
720
|
(xAutoYMin[1] + xAutoYMax[1]) / 2
|
|
@@ -696,9 +763,14 @@ function nonzeroFillRule(paths) {
|
|
|
696
763
|
for (let i = 0, len = paths.length; i < len; i++) {
|
|
697
764
|
const _results = [];
|
|
698
765
|
const testPoints = testPointsGroups[i];
|
|
766
|
+
const boxI = bboxes[i];
|
|
699
767
|
for (let j = 0; j < len; j++) {
|
|
700
768
|
if (i === j)
|
|
701
769
|
continue;
|
|
770
|
+
const boxJ = bboxes[j];
|
|
771
|
+
if (!boxI || !boxJ || !aabbIntersects(boxI, boxJ)) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
702
774
|
const wnMap = {};
|
|
703
775
|
const wnList = [];
|
|
704
776
|
for (let p = 0, pLen = testPoints.length; p < pLen; p++) {
|
|
@@ -854,22 +926,35 @@ function quadraticBezier(t, p0, p1, p2) {
|
|
|
854
926
|
return quadraticBezierP0(t, p0) + quadraticBezierP1(t, p1) + quadraticBezierP2(t, p2);
|
|
855
927
|
}
|
|
856
928
|
|
|
929
|
+
function resolveLineJoin(join) {
|
|
930
|
+
switch (join) {
|
|
931
|
+
case "round":
|
|
932
|
+
case "bevel":
|
|
933
|
+
case "miter":
|
|
934
|
+
return join;
|
|
935
|
+
default:
|
|
936
|
+
return "miter";
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function resolveLineStyle(style) {
|
|
940
|
+
return {
|
|
941
|
+
width: style?.strokeWidth ?? 1,
|
|
942
|
+
alignment: 0.5,
|
|
943
|
+
join: resolveLineJoin(style?.strokeLinejoin),
|
|
944
|
+
cap: style?.strokeLinecap ?? "butt",
|
|
945
|
+
miterLimit: style?.strokeMiterlimit ?? 10
|
|
946
|
+
};
|
|
947
|
+
}
|
|
857
948
|
const closePointEps = 1e-4;
|
|
858
949
|
const curveEps = 1e-4;
|
|
859
950
|
function strokeTriangulate(points, options = {}) {
|
|
860
951
|
const {
|
|
861
952
|
vertices = [],
|
|
862
953
|
indices = [],
|
|
863
|
-
lineStyle = {
|
|
864
|
-
alignment: 0.5,
|
|
865
|
-
cap: "butt",
|
|
866
|
-
join: "miter",
|
|
867
|
-
width: 1,
|
|
868
|
-
miterLimit: 10
|
|
869
|
-
},
|
|
870
954
|
flipAlignment = false,
|
|
871
955
|
closed = true
|
|
872
956
|
} = options;
|
|
957
|
+
const lineStyle = options.lineStyle ?? resolveLineStyle(options.style);
|
|
873
958
|
const eps = closePointEps;
|
|
874
959
|
if (points.length === 0) {
|
|
875
960
|
return { vertices, indices };
|
|
@@ -2653,6 +2738,41 @@ function svgToPath2DSet(svg) {
|
|
|
2653
2738
|
class Curve {
|
|
2654
2739
|
arcLengthDivision = 200;
|
|
2655
2740
|
_lengths = [];
|
|
2741
|
+
_adaptiveCache;
|
|
2742
|
+
/**
|
|
2743
|
+
* Parent composite, set lazily when a composite caches its children. Lets
|
|
2744
|
+
* {@link invalidate} propagate up so an ancestor's caches refresh too.
|
|
2745
|
+
*/
|
|
2746
|
+
_owner;
|
|
2747
|
+
_invalidating = false;
|
|
2748
|
+
/**
|
|
2749
|
+
* Drop cached arc lengths and the cached sampled outline used by hit testing, then
|
|
2750
|
+
* bubble up to {@link _owner}. Called automatically by {@link applyTransform} and the
|
|
2751
|
+
* `Path2D` mutators; call it manually after mutating control-point coordinates in place —
|
|
2752
|
+
* the caches cannot observe such mutations.
|
|
2753
|
+
*/
|
|
2754
|
+
invalidate() {
|
|
2755
|
+
if (this._invalidating) {
|
|
2756
|
+
return this;
|
|
2757
|
+
}
|
|
2758
|
+
this._invalidating = true;
|
|
2759
|
+
this._invalidateSelf();
|
|
2760
|
+
this._owner?.invalidate();
|
|
2761
|
+
this._invalidating = false;
|
|
2762
|
+
return this;
|
|
2763
|
+
}
|
|
2764
|
+
/** Clears this curve's own caches. Composites also clear their children (see override). */
|
|
2765
|
+
_invalidateSelf() {
|
|
2766
|
+
this._lengths.length = 0;
|
|
2767
|
+
this._adaptiveCache = void 0;
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Sampled outline cached for repeated hit tests (read-only — do not mutate the result).
|
|
2771
|
+
* Invalidated by {@link invalidate}.
|
|
2772
|
+
*/
|
|
2773
|
+
_getCachedAdaptiveVertices() {
|
|
2774
|
+
return this._adaptiveCache ??= this.getAdaptiveVertices();
|
|
2775
|
+
}
|
|
2656
2776
|
getPointAt(u, output = new Vector2()) {
|
|
2657
2777
|
return this.getPoint(this.getUToTMapping(u), output);
|
|
2658
2778
|
}
|
|
@@ -2662,6 +2782,22 @@ class Curve {
|
|
|
2662
2782
|
getControlPointRefs() {
|
|
2663
2783
|
return [];
|
|
2664
2784
|
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Reverse the traversal direction in place (start ↔ end, same geometry). The base
|
|
2787
|
+
* implementation reverses the order of the control-point *values*, which is correct for
|
|
2788
|
+
* line / Bézier / spline primitives whose {@link getControlPointRefs} order matches their
|
|
2789
|
+
* parametric order. {@link RoundCurve} (angle-based) and composites (child order) override it.
|
|
2790
|
+
*/
|
|
2791
|
+
reverse() {
|
|
2792
|
+
const refs = this.getControlPointRefs();
|
|
2793
|
+
const n = refs.length;
|
|
2794
|
+
const snapshot = refs.map((p) => p.clone());
|
|
2795
|
+
for (let i = 0; i < n; i++) {
|
|
2796
|
+
refs[i].copyFrom(snapshot[n - 1 - i]);
|
|
2797
|
+
}
|
|
2798
|
+
this.invalidate();
|
|
2799
|
+
return this;
|
|
2800
|
+
}
|
|
2665
2801
|
applyTransform(transform) {
|
|
2666
2802
|
const isFunction = typeof transform === "function";
|
|
2667
2803
|
this.getControlPointRefs().forEach((p) => {
|
|
@@ -2671,6 +2807,7 @@ class Curve {
|
|
|
2671
2807
|
transform.apply(p, p);
|
|
2672
2808
|
}
|
|
2673
2809
|
});
|
|
2810
|
+
this.invalidate();
|
|
2674
2811
|
return this;
|
|
2675
2812
|
}
|
|
2676
2813
|
getUnevenVertices(count = 5, output = []) {
|
|
@@ -2788,6 +2925,22 @@ class Curve {
|
|
|
2788
2925
|
getTangentAt(u, output) {
|
|
2789
2926
|
return this.getTangent(this.getUToTMapping(u), output);
|
|
2790
2927
|
}
|
|
2928
|
+
/**
|
|
2929
|
+
* PathKit-style sample at an absolute arc-length `distance` along the curve: the point, the unit
|
|
2930
|
+
* tangent, and the tangent `angle` in radians. `distance` is clamped to `[0, getLength()]`, so
|
|
2931
|
+
* passing `0`/`getLength()` always yields the endpoints. See {@link PathMeasure} for a wrapper.
|
|
2932
|
+
*/
|
|
2933
|
+
getPosTan(distance) {
|
|
2934
|
+
const length = this.getLength();
|
|
2935
|
+
const u = length > 0 ? Math.min(Math.max(distance / length, 0), 1) : 0;
|
|
2936
|
+
const t = this.getUToTMapping(u);
|
|
2937
|
+
const tangent = this.getTangent(t);
|
|
2938
|
+
return {
|
|
2939
|
+
position: this.getPoint(t),
|
|
2940
|
+
tangent,
|
|
2941
|
+
angle: Math.atan2(tangent.y, tangent.x)
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2791
2944
|
getNormal(t, output = new Vector2()) {
|
|
2792
2945
|
this.getTangent(t, output);
|
|
2793
2946
|
return output.set(-output.y, output.x).normalize();
|
|
@@ -2815,12 +2968,25 @@ class Curve {
|
|
|
2815
2968
|
return mid;
|
|
2816
2969
|
}
|
|
2817
2970
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
2818
|
-
const
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2971
|
+
const vertices = this.getAdaptiveVertices();
|
|
2972
|
+
let minX = min.x;
|
|
2973
|
+
let minY = min.y;
|
|
2974
|
+
let maxX = max.x;
|
|
2975
|
+
let maxY = max.y;
|
|
2976
|
+
for (let i = 0, len = vertices.length; i < len; i += 2) {
|
|
2977
|
+
const x = vertices[i];
|
|
2978
|
+
const y = vertices[i + 1];
|
|
2979
|
+
if (x < minX)
|
|
2980
|
+
minX = x;
|
|
2981
|
+
if (y < minY)
|
|
2982
|
+
minY = y;
|
|
2983
|
+
if (x > maxX)
|
|
2984
|
+
maxX = x;
|
|
2985
|
+
if (y > maxY)
|
|
2986
|
+
maxY = y;
|
|
2823
2987
|
}
|
|
2988
|
+
min.set(minX, minY);
|
|
2989
|
+
max.set(maxX, maxY);
|
|
2824
2990
|
return { min: min.finite(), max: max.finite() };
|
|
2825
2991
|
}
|
|
2826
2992
|
getBoundingBox() {
|
|
@@ -2838,7 +3004,7 @@ class Curve {
|
|
|
2838
3004
|
* are honored — a single `Curve` is always one ring.
|
|
2839
3005
|
*/
|
|
2840
3006
|
isPointInFill(point, options = {}) {
|
|
2841
|
-
return pointInPolygon(point, this.
|
|
3007
|
+
return pointInPolygon(point, this._getCachedAdaptiveVertices(), options.fillRule);
|
|
2842
3008
|
}
|
|
2843
3009
|
/**
|
|
2844
3010
|
* Test whether a point lies on this curve's stroke, i.e. within `strokeWidth / 2 + tolerance`
|
|
@@ -2851,7 +3017,7 @@ class Curve {
|
|
|
2851
3017
|
*/
|
|
2852
3018
|
isPointInStroke(point, options = {}) {
|
|
2853
3019
|
const { strokeWidth = 1, tolerance = 0, closed = false } = options;
|
|
2854
|
-
const distance = pointToPolylineDistance(point, this.
|
|
3020
|
+
const distance = pointToPolylineDistance(point, this._getCachedAdaptiveVertices(), closed);
|
|
2855
3021
|
return distance <= strokeWidth / 2 + tolerance;
|
|
2856
3022
|
}
|
|
2857
3023
|
/**
|
|
@@ -2870,10 +3036,28 @@ class Curve {
|
|
|
2870
3036
|
options
|
|
2871
3037
|
);
|
|
2872
3038
|
}
|
|
3039
|
+
/**
|
|
3040
|
+
* Whether this curve forms a closed loop (its outline should be stroked without end caps,
|
|
3041
|
+
* stitching the last vertex back to the first). The base test is purely geometric — the first
|
|
3042
|
+
* sampled vertex coincides with the last. Curves that close without a duplicated endpoint
|
|
3043
|
+
* (a full-revolution {@link RoundCurve}, rectangles, polygons) override this.
|
|
3044
|
+
*/
|
|
3045
|
+
isClosed() {
|
|
3046
|
+
const v = this._getCachedAdaptiveVertices();
|
|
3047
|
+
const len = v.length;
|
|
3048
|
+
if (len < 6) {
|
|
3049
|
+
return false;
|
|
3050
|
+
}
|
|
3051
|
+
const eps = 1e-4;
|
|
3052
|
+
return Math.abs(v[0] - v[len - 2]) < eps && Math.abs(v[1] - v[len - 1]) < eps;
|
|
3053
|
+
}
|
|
2873
3054
|
strokeTriangulate(options) {
|
|
2874
3055
|
return strokeTriangulate(
|
|
2875
3056
|
this.getAdaptiveVertices(),
|
|
2876
|
-
|
|
3057
|
+
{
|
|
3058
|
+
...options,
|
|
3059
|
+
closed: options?.closed ?? this.isClosed()
|
|
3060
|
+
}
|
|
2877
3061
|
);
|
|
2878
3062
|
}
|
|
2879
3063
|
toCommands() {
|
|
@@ -2968,6 +3152,22 @@ class RoundCurve extends Curve {
|
|
|
2968
3152
|
isClockwise() {
|
|
2969
3153
|
return this.clockwise;
|
|
2970
3154
|
}
|
|
3155
|
+
/**
|
|
3156
|
+
* A circle/ellipse arc is closed when it sweeps (at least) a full revolution — the sampled
|
|
3157
|
+
* outline does not duplicate the start vertex, so the geometric first==last test in the base
|
|
3158
|
+
* class would wrongly report a full circle as open and leave a seam gap in the stroke.
|
|
3159
|
+
*/
|
|
3160
|
+
isClosed() {
|
|
3161
|
+
return Math.abs(this.endAngle - this.startAngle) >= Math.PI * 2 - 1e-9 || super.isClosed();
|
|
3162
|
+
}
|
|
3163
|
+
reverse() {
|
|
3164
|
+
const { startAngle, endAngle } = this;
|
|
3165
|
+
this.startAngle = endAngle;
|
|
3166
|
+
this.endAngle = startAngle;
|
|
3167
|
+
this.clockwise = !this.clockwise;
|
|
3168
|
+
this.invalidate();
|
|
3169
|
+
return this;
|
|
3170
|
+
}
|
|
2971
3171
|
_getDeltaAngle() {
|
|
2972
3172
|
const PI_2 = Math.PI * 2;
|
|
2973
3173
|
let deltaAngle = this.endAngle - this.startAngle;
|
|
@@ -2995,6 +3195,64 @@ class RoundCurve extends Curve {
|
|
|
2995
3195
|
}
|
|
2996
3196
|
return output.set(_x, _y);
|
|
2997
3197
|
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Point on the ellipse at an absolute angle (mirrors {@link getPoint}'s parameterization,
|
|
3200
|
+
* ignoring `_diff`).
|
|
3201
|
+
*/
|
|
3202
|
+
_pointAtAngle(angle, output) {
|
|
3203
|
+
let x = this.cx + this.rx * Math.cos(angle);
|
|
3204
|
+
let y = this.cy + this.ry * Math.sin(angle);
|
|
3205
|
+
if (this.rotate !== 0) {
|
|
3206
|
+
const cos = Math.cos(this.rotate);
|
|
3207
|
+
const sin = Math.sin(this.rotate);
|
|
3208
|
+
const tx = x - this.cx;
|
|
3209
|
+
const ty = y - this.cy;
|
|
3210
|
+
x = tx * cos - ty * sin + this.cx;
|
|
3211
|
+
y = tx * sin + ty * cos + this.cy;
|
|
3212
|
+
}
|
|
3213
|
+
return output.set(x, y);
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* Analytical bounds of the (elliptical) arc: the start/end points plus the per-axis
|
|
3217
|
+
* extrema angles that fall within the swept interval. Matches {@link getPoint}, so it is
|
|
3218
|
+
* exact for `ArcCurve`/`EllipseCurve`. The `_diff` offset (used only by the legacy
|
|
3219
|
+
* `_getAdaptiveVerticesByCircle` path) is intentionally ignored here.
|
|
3220
|
+
*/
|
|
3221
|
+
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
3222
|
+
const { startAngle, rotate } = this;
|
|
3223
|
+
const delta = this._getDeltaAngle();
|
|
3224
|
+
const cosT = Math.cos(rotate);
|
|
3225
|
+
const sinT = Math.sin(rotate);
|
|
3226
|
+
const p = tempV2;
|
|
3227
|
+
let minX = min.x;
|
|
3228
|
+
let minY = min.y;
|
|
3229
|
+
let maxX = max.x;
|
|
3230
|
+
let maxY = max.y;
|
|
3231
|
+
const consider = (angle) => {
|
|
3232
|
+
this._pointAtAngle(angle, p);
|
|
3233
|
+
if (p.x < minX)
|
|
3234
|
+
minX = p.x;
|
|
3235
|
+
if (p.y < minY)
|
|
3236
|
+
minY = p.y;
|
|
3237
|
+
if (p.x > maxX)
|
|
3238
|
+
maxX = p.x;
|
|
3239
|
+
if (p.y > maxY)
|
|
3240
|
+
maxY = p.y;
|
|
3241
|
+
};
|
|
3242
|
+
consider(startAngle);
|
|
3243
|
+
consider(startAngle + delta);
|
|
3244
|
+
const ax = Math.atan2(-this.ry * sinT, this.rx * cosT);
|
|
3245
|
+
const ay = Math.atan2(this.ry * cosT, this.rx * sinT);
|
|
3246
|
+
const bases = [ax, ax + Math.PI, ay, ay + Math.PI];
|
|
3247
|
+
for (let i = 0; i < 4; i++) {
|
|
3248
|
+
if (angleInSweep(bases[i], startAngle, delta)) {
|
|
3249
|
+
consider(bases[i]);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
min.set(minX, minY);
|
|
3253
|
+
max.set(maxX, maxY);
|
|
3254
|
+
return { min: min.finite(), max: max.finite() };
|
|
3255
|
+
}
|
|
2998
3256
|
toCommands() {
|
|
2999
3257
|
const { cx, cy, rx, ry, startAngle, endAngle, clockwise, rotate } = this;
|
|
3000
3258
|
const startX = cx + rx * Math.cos(startAngle) * Math.cos(rotate) - ry * Math.sin(startAngle) * Math.sin(rotate);
|
|
@@ -3045,6 +3303,7 @@ class RoundCurve extends Curve {
|
|
|
3045
3303
|
} else {
|
|
3046
3304
|
transfEllipseNoSkew(this, transform);
|
|
3047
3305
|
}
|
|
3306
|
+
this.invalidate();
|
|
3048
3307
|
return this;
|
|
3049
3308
|
}
|
|
3050
3309
|
getControlPointRefs() {
|
|
@@ -3165,9 +3424,25 @@ class RoundCurve extends Curve {
|
|
|
3165
3424
|
return output;
|
|
3166
3425
|
}
|
|
3167
3426
|
getAdaptiveVertices(output = []) {
|
|
3168
|
-
|
|
3427
|
+
const PI2 = Math.PI * 2;
|
|
3428
|
+
if (this.startAngle === 0 && this.endAngle === PI2) {
|
|
3169
3429
|
return this._getAdaptiveVerticesByCircle(output);
|
|
3170
3430
|
}
|
|
3431
|
+
if (Math.abs(this.endAngle - this.startAngle) >= PI2 - 1e-9) {
|
|
3432
|
+
const tmp = this._getAdaptiveVerticesByCircle([]);
|
|
3433
|
+
const n = tmp.length / 2;
|
|
3434
|
+
if (this.endAngle > this.startAngle) {
|
|
3435
|
+
for (let i = 0; i < tmp.length; i++) {
|
|
3436
|
+
output.push(tmp[i]);
|
|
3437
|
+
}
|
|
3438
|
+
} else {
|
|
3439
|
+
output.push(tmp[0], tmp[1]);
|
|
3440
|
+
for (let i = n - 1; i >= 1; i--) {
|
|
3441
|
+
output.push(tmp[i * 2], tmp[i * 2 + 1]);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
return output;
|
|
3445
|
+
}
|
|
3171
3446
|
return this._getAdaptiveVerticesByArc(output);
|
|
3172
3447
|
}
|
|
3173
3448
|
copyFrom(source) {
|
|
@@ -3185,6 +3460,23 @@ class RoundCurve extends Curve {
|
|
|
3185
3460
|
return this;
|
|
3186
3461
|
}
|
|
3187
3462
|
}
|
|
3463
|
+
function angleInSweep(a, start, delta) {
|
|
3464
|
+
const PI_2 = Math.PI * 2;
|
|
3465
|
+
const eps = 1e-9;
|
|
3466
|
+
if (Math.abs(delta) >= PI_2 - eps) {
|
|
3467
|
+
return true;
|
|
3468
|
+
}
|
|
3469
|
+
let off = (a - start) % PI_2;
|
|
3470
|
+
if (delta >= 0) {
|
|
3471
|
+
if (off < -eps)
|
|
3472
|
+
off += PI_2;
|
|
3473
|
+
return off >= -eps && off <= delta + eps;
|
|
3474
|
+
}
|
|
3475
|
+
if (off > eps) {
|
|
3476
|
+
off -= PI_2;
|
|
3477
|
+
}
|
|
3478
|
+
return off <= eps && off >= delta - eps;
|
|
3479
|
+
}
|
|
3188
3480
|
function transfEllipseGeneric(curve, m) {
|
|
3189
3481
|
const a = curve.rx;
|
|
3190
3482
|
const b = curve.ry;
|
|
@@ -3360,6 +3652,15 @@ class LineCurve extends Curve {
|
|
|
3360
3652
|
getControlPointRefs() {
|
|
3361
3653
|
return [this.p1, this.p2];
|
|
3362
3654
|
}
|
|
3655
|
+
// Swap endpoint *references* (not values) so corner Vector2s shared with adjacent
|
|
3656
|
+
// segments stay intact and simply re-associate with the reversed segment.
|
|
3657
|
+
reverse() {
|
|
3658
|
+
const { p1, p2 } = this;
|
|
3659
|
+
this.p1 = p2;
|
|
3660
|
+
this.p2 = p1;
|
|
3661
|
+
this.invalidate();
|
|
3662
|
+
return this;
|
|
3663
|
+
}
|
|
3363
3664
|
getAdaptiveVertices(output = []) {
|
|
3364
3665
|
output.push(
|
|
3365
3666
|
this.p1.x,
|
|
@@ -3423,6 +3724,22 @@ class CompositeCurve extends Curve {
|
|
|
3423
3724
|
super();
|
|
3424
3725
|
this.curves = curves;
|
|
3425
3726
|
}
|
|
3727
|
+
_adaptiveCacheLen = -1;
|
|
3728
|
+
_invalidateSelf() {
|
|
3729
|
+
super._invalidateSelf();
|
|
3730
|
+
this._adaptiveCacheLen = -1;
|
|
3731
|
+
this.curves.forEach((curve) => curve.invalidate());
|
|
3732
|
+
}
|
|
3733
|
+
_getCachedAdaptiveVertices() {
|
|
3734
|
+
if (!this._adaptiveCache || this._adaptiveCacheLen !== this.curves.length) {
|
|
3735
|
+
this.curves.forEach((curve) => {
|
|
3736
|
+
curve._owner = this;
|
|
3737
|
+
});
|
|
3738
|
+
this._adaptiveCache = this.getAdaptiveVertices();
|
|
3739
|
+
this._adaptiveCacheLen = this.curves.length;
|
|
3740
|
+
}
|
|
3741
|
+
return this._adaptiveCache;
|
|
3742
|
+
}
|
|
3426
3743
|
getFlatCurves() {
|
|
3427
3744
|
return this.curves.flatMap((curve) => {
|
|
3428
3745
|
if (curve instanceof CompositeCurve) {
|
|
@@ -3468,6 +3785,7 @@ class CompositeCurve extends Curve {
|
|
|
3468
3785
|
updateLengths() {
|
|
3469
3786
|
const lengths = [];
|
|
3470
3787
|
for (let i = 0, sum = 0, len = this.curves.length; i < len; i++) {
|
|
3788
|
+
this.curves[i]._owner = this;
|
|
3471
3789
|
sum += this.curves[i].getLength();
|
|
3472
3790
|
lengths.push(sum);
|
|
3473
3791
|
}
|
|
@@ -3506,6 +3824,16 @@ class CompositeCurve extends Curve {
|
|
|
3506
3824
|
});
|
|
3507
3825
|
return output;
|
|
3508
3826
|
}
|
|
3827
|
+
/**
|
|
3828
|
+
* A composite is closed when its single child is closed (e.g. a lone full-circle arc), or when
|
|
3829
|
+
* its assembled outline returns to its start (rectangles, polygons, multi-segment loops).
|
|
3830
|
+
*/
|
|
3831
|
+
isClosed() {
|
|
3832
|
+
if (this.curves.length === 1) {
|
|
3833
|
+
return this.curves[0].isClosed();
|
|
3834
|
+
}
|
|
3835
|
+
return super.isClosed();
|
|
3836
|
+
}
|
|
3509
3837
|
strokeTriangulate(options) {
|
|
3510
3838
|
if (this.curves.length === 1) {
|
|
3511
3839
|
return this.curves[0].strokeTriangulate(options);
|
|
@@ -3513,6 +3841,13 @@ class CompositeCurve extends Curve {
|
|
|
3513
3841
|
return super.strokeTriangulate(options);
|
|
3514
3842
|
}
|
|
3515
3843
|
}
|
|
3844
|
+
/** Reverse the sub-curve order and reverse each sub-curve, so the whole outline runs backwards. */
|
|
3845
|
+
reverse() {
|
|
3846
|
+
this.curves.reverse();
|
|
3847
|
+
this.curves.forEach((curve) => curve.reverse());
|
|
3848
|
+
this.invalidate();
|
|
3849
|
+
return this;
|
|
3850
|
+
}
|
|
3516
3851
|
getFillVertices(options) {
|
|
3517
3852
|
if (this.curves.length === 1) {
|
|
3518
3853
|
return this.curves[0].getFillVertices(options);
|
|
@@ -3536,7 +3871,10 @@ class CompositeCurve extends Curve {
|
|
|
3536
3871
|
}
|
|
3537
3872
|
}
|
|
3538
3873
|
applyTransform(transform) {
|
|
3874
|
+
this._invalidating = true;
|
|
3539
3875
|
this.curves.forEach((curve) => curve.applyTransform(transform));
|
|
3876
|
+
this._invalidating = false;
|
|
3877
|
+
this.invalidate();
|
|
3540
3878
|
return this;
|
|
3541
3879
|
}
|
|
3542
3880
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN) {
|
|
@@ -3605,6 +3943,16 @@ class CubicBezierCurve extends Curve {
|
|
|
3605
3943
|
getControlPointRefs() {
|
|
3606
3944
|
return [this.p1, this.cp1, this.cp2, this.p2];
|
|
3607
3945
|
}
|
|
3946
|
+
// Swap endpoint and control-point references; keeps shared corner Vector2s intact.
|
|
3947
|
+
reverse() {
|
|
3948
|
+
const { p1, cp1, cp2, p2 } = this;
|
|
3949
|
+
this.p1 = p2;
|
|
3950
|
+
this.cp1 = cp2;
|
|
3951
|
+
this.cp2 = cp1;
|
|
3952
|
+
this.p2 = p1;
|
|
3953
|
+
this.invalidate();
|
|
3954
|
+
return this;
|
|
3955
|
+
}
|
|
3608
3956
|
_solveQuadratic(a, b, c) {
|
|
3609
3957
|
if (Math.abs(a) < 1e-12) {
|
|
3610
3958
|
if (Math.abs(b) < 1e-12)
|
|
@@ -3765,6 +4113,14 @@ class QuadraticBezierCurve extends Curve {
|
|
|
3765
4113
|
getControlPointRefs() {
|
|
3766
4114
|
return [this.p1, this.cp, this.p2];
|
|
3767
4115
|
}
|
|
4116
|
+
// Swap endpoint references (cp is symmetric); keeps shared corner Vector2s intact.
|
|
4117
|
+
reverse() {
|
|
4118
|
+
const { p1, p2 } = this;
|
|
4119
|
+
this.p1 = p2;
|
|
4120
|
+
this.p2 = p1;
|
|
4121
|
+
this.invalidate();
|
|
4122
|
+
return this;
|
|
4123
|
+
}
|
|
3768
4124
|
getAdaptiveVertices(output = []) {
|
|
3769
4125
|
return getAdaptiveQuadraticBezierCurvePoints(
|
|
3770
4126
|
this.p1.x,
|
|
@@ -3875,7 +4231,7 @@ class RectangleCurve extends PolygonCurve {
|
|
|
3875
4231
|
}
|
|
3876
4232
|
}
|
|
3877
4233
|
|
|
3878
|
-
class RoundRectangleCurve extends
|
|
4234
|
+
class RoundRectangleCurve extends CompositeCurve {
|
|
3879
4235
|
constructor(x = 0, y = 0, width = 1, height = 1, radius = 1) {
|
|
3880
4236
|
super();
|
|
3881
4237
|
this.x = x;
|
|
@@ -3886,25 +4242,53 @@ class RoundRectangleCurve extends RoundCurve {
|
|
|
3886
4242
|
this.update();
|
|
3887
4243
|
}
|
|
3888
4244
|
update() {
|
|
3889
|
-
const { x, y, width, height
|
|
3890
|
-
const
|
|
3891
|
-
const
|
|
3892
|
-
const
|
|
3893
|
-
const
|
|
3894
|
-
const
|
|
3895
|
-
const
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
4245
|
+
const { x, y, width, height } = this;
|
|
4246
|
+
const r = Math.max(0, Math.min(this.radius, Math.abs(width) / 2, Math.abs(height) / 2));
|
|
4247
|
+
const x0 = x;
|
|
4248
|
+
const x1 = x + r;
|
|
4249
|
+
const x2 = x + width - r;
|
|
4250
|
+
const x3 = x + width;
|
|
4251
|
+
const y0 = y;
|
|
4252
|
+
const y1 = y + r;
|
|
4253
|
+
const y2 = y + height - r;
|
|
4254
|
+
const y3 = y + height;
|
|
4255
|
+
if (r <= 0) {
|
|
4256
|
+
this.curves = [
|
|
4257
|
+
LineCurve.from(x0, y0, x3, y0),
|
|
4258
|
+
LineCurve.from(x3, y0, x3, y3),
|
|
4259
|
+
LineCurve.from(x3, y3, x0, y3),
|
|
4260
|
+
LineCurve.from(x0, y3, x0, y0)
|
|
4261
|
+
];
|
|
4262
|
+
} else {
|
|
4263
|
+
const HALF_PI = Math.PI / 2;
|
|
4264
|
+
this.curves = [
|
|
4265
|
+
LineCurve.from(x1, y0, x2, y0),
|
|
4266
|
+
// top edge
|
|
4267
|
+
new ArcCurve(x2, y1, r, -HALF_PI, 0, true),
|
|
4268
|
+
// top-right corner
|
|
4269
|
+
LineCurve.from(x3, y1, x3, y2),
|
|
4270
|
+
// right edge
|
|
4271
|
+
new ArcCurve(x2, y2, r, 0, HALF_PI, true),
|
|
4272
|
+
// bottom-right corner
|
|
4273
|
+
LineCurve.from(x2, y3, x1, y3),
|
|
4274
|
+
// bottom edge
|
|
4275
|
+
new ArcCurve(x1, y2, r, HALF_PI, Math.PI, true),
|
|
4276
|
+
// bottom-left corner
|
|
4277
|
+
LineCurve.from(x0, y2, x0, y1),
|
|
4278
|
+
// left edge
|
|
4279
|
+
new ArcCurve(x1, y1, r, Math.PI, Math.PI * 1.5, true)
|
|
4280
|
+
// top-left corner
|
|
4281
|
+
];
|
|
4282
|
+
}
|
|
4283
|
+
this.invalidate();
|
|
3899
4284
|
return this;
|
|
3900
4285
|
}
|
|
3901
4286
|
drawTo(ctx) {
|
|
3902
|
-
|
|
3903
|
-
ctx.roundRect(x, y, width, height, radius);
|
|
4287
|
+
ctx.roundRect(this.x, this.y, this.width, this.height, this.radius);
|
|
3904
4288
|
return this;
|
|
3905
4289
|
}
|
|
3906
4290
|
copyFrom(source) {
|
|
3907
|
-
|
|
4291
|
+
this.arcLengthDivision = source.arcLengthDivision;
|
|
3908
4292
|
this.x = source.x;
|
|
3909
4293
|
this.y = source.y;
|
|
3910
4294
|
this.width = source.width;
|
|
@@ -3938,6 +4322,11 @@ class SplineCurve extends Curve {
|
|
|
3938
4322
|
getControlPointRefs() {
|
|
3939
4323
|
return this.points;
|
|
3940
4324
|
}
|
|
4325
|
+
reverse() {
|
|
4326
|
+
this.points.reverse();
|
|
4327
|
+
this.invalidate();
|
|
4328
|
+
return this;
|
|
4329
|
+
}
|
|
3941
4330
|
copyFrom(source) {
|
|
3942
4331
|
super.copyFrom(source);
|
|
3943
4332
|
this.points = [];
|
|
@@ -3974,6 +4363,22 @@ class CurvePath extends CompositeCurve {
|
|
|
3974
4363
|
this.addCommands(svgPathDataToCommands(data));
|
|
3975
4364
|
return this;
|
|
3976
4365
|
}
|
|
4366
|
+
/**
|
|
4367
|
+
* A sub-path is closed if it was explicitly closed (`autoClose`, i.e. a `Z`/`closePath`), or if
|
|
4368
|
+
* it forms a geometric loop / wraps a single closed primitive (handled by the base class).
|
|
4369
|
+
*/
|
|
4370
|
+
isClosed() {
|
|
4371
|
+
return this.autoClose || super.isClosed();
|
|
4372
|
+
}
|
|
4373
|
+
/** Reverse direction, then refresh the {@link startPoint}/{@link currentPoint} cursors. */
|
|
4374
|
+
reverse() {
|
|
4375
|
+
super.reverse();
|
|
4376
|
+
if (this.curves.length) {
|
|
4377
|
+
this.startPoint = this.getPoint(0);
|
|
4378
|
+
this.currentPoint = this.getPoint(1);
|
|
4379
|
+
}
|
|
4380
|
+
return this;
|
|
4381
|
+
}
|
|
3977
4382
|
_closeVertices(output) {
|
|
3978
4383
|
if (this.autoClose && output.length >= 4 && (output[0] !== output[output.length - 2] && output[1] !== output[output.length - 1])) {
|
|
3979
4384
|
output.push(output[0], output[1]);
|
|
@@ -4006,7 +4411,7 @@ class CurvePath extends CompositeCurve {
|
|
|
4006
4411
|
*/
|
|
4007
4412
|
isPointInStroke(point, options = {}) {
|
|
4008
4413
|
const { strokeWidth = 1, tolerance = 0 } = options;
|
|
4009
|
-
const vertices = this.
|
|
4414
|
+
const vertices = this._getCachedAdaptiveVertices();
|
|
4010
4415
|
const len = vertices.length;
|
|
4011
4416
|
const closed = options.closed ?? (this.autoClose || len >= 6 && vertices[0] === vertices[len - 2] && vertices[1] === vertices[len - 1]);
|
|
4012
4417
|
return pointToPolylineDistance(point, vertices, closed) <= strokeWidth / 2 + tolerance;
|
|
@@ -4184,6 +4589,8 @@ class CurvePath extends CompositeCurve {
|
|
|
4184
4589
|
|
|
4185
4590
|
class Path2D extends CompositeCurve {
|
|
4186
4591
|
_meta;
|
|
4592
|
+
_ringsCache;
|
|
4593
|
+
_ringsCacheLen = -1;
|
|
4187
4594
|
currentCurve = new CurvePath();
|
|
4188
4595
|
style;
|
|
4189
4596
|
get startPoint() {
|
|
@@ -4302,18 +4709,21 @@ class Path2D extends CompositeCurve {
|
|
|
4302
4709
|
this.getControlPointRefs().forEach((point) => {
|
|
4303
4710
|
point.scale(sx, sy, target);
|
|
4304
4711
|
});
|
|
4712
|
+
this.invalidate();
|
|
4305
4713
|
return this;
|
|
4306
4714
|
}
|
|
4307
4715
|
skew(ax, ay = 0, target = { x: 0, y: 0 }) {
|
|
4308
4716
|
this.getControlPointRefs().forEach((point) => {
|
|
4309
4717
|
point.skew(ax, ay, target);
|
|
4310
4718
|
});
|
|
4719
|
+
this.invalidate();
|
|
4311
4720
|
return this;
|
|
4312
4721
|
}
|
|
4313
4722
|
rotate(rad, target = { x: 0, y: 0 }) {
|
|
4314
4723
|
this.getControlPointRefs().forEach((point) => {
|
|
4315
4724
|
point.rotate(rad, target);
|
|
4316
4725
|
});
|
|
4726
|
+
this.invalidate();
|
|
4317
4727
|
return this;
|
|
4318
4728
|
}
|
|
4319
4729
|
bold(b) {
|
|
@@ -4372,6 +4782,7 @@ class Path2D extends CompositeCurve {
|
|
|
4372
4782
|
}
|
|
4373
4783
|
});
|
|
4374
4784
|
});
|
|
4785
|
+
this.invalidate();
|
|
4375
4786
|
return this;
|
|
4376
4787
|
}
|
|
4377
4788
|
/**
|
|
@@ -4384,13 +4795,70 @@ class Path2D extends CompositeCurve {
|
|
|
4384
4795
|
*
|
|
4385
4796
|
* Defaults `fillRule` to `style.fillRule`, then `'nonzero'` (matching SVG/Canvas).
|
|
4386
4797
|
*/
|
|
4798
|
+
_invalidateSelf() {
|
|
4799
|
+
super._invalidateSelf();
|
|
4800
|
+
this._ringsCache = void 0;
|
|
4801
|
+
this._ringsCacheLen = -1;
|
|
4802
|
+
}
|
|
4803
|
+
/** Per-sub-path sampled rings, cached for repeated hit tests. */
|
|
4804
|
+
_getRings() {
|
|
4805
|
+
if (!this._ringsCache || this._ringsCacheLen !== this.curves.length) {
|
|
4806
|
+
this._ringsCache = this.curves.map((curve) => {
|
|
4807
|
+
curve._owner = this;
|
|
4808
|
+
return curve.getAdaptiveVertices();
|
|
4809
|
+
});
|
|
4810
|
+
this._ringsCacheLen = this.curves.length;
|
|
4811
|
+
}
|
|
4812
|
+
return this._ringsCache;
|
|
4813
|
+
}
|
|
4387
4814
|
isPointInFill(point, options = {}) {
|
|
4388
4815
|
const fillRule = options.fillRule ?? this.style.fillRule ?? "nonzero";
|
|
4389
|
-
return pointInPolygons(
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
);
|
|
4816
|
+
return pointInPolygons(point, this._getRings(), fillRule);
|
|
4817
|
+
}
|
|
4818
|
+
/** Build a `Path2D` from flat rings (`[x0,y0,…]` per sub-path); closed-and-filled as sub-paths. */
|
|
4819
|
+
static fromRings(rings, style = {}) {
|
|
4820
|
+
const path = new Path2D(void 0, style);
|
|
4821
|
+
for (const ring of rings) {
|
|
4822
|
+
if (ring.length < 6) {
|
|
4823
|
+
continue;
|
|
4824
|
+
}
|
|
4825
|
+
let end = ring.length;
|
|
4826
|
+
if (ring[0] === ring[end - 2] && ring[1] === ring[end - 1]) {
|
|
4827
|
+
end -= 2;
|
|
4828
|
+
}
|
|
4829
|
+
path.moveTo(ring[0], ring[1]);
|
|
4830
|
+
for (let i = 2; i < end; i += 2) {
|
|
4831
|
+
path.lineTo(ring[i], ring[i + 1]);
|
|
4832
|
+
}
|
|
4833
|
+
path.closePath();
|
|
4834
|
+
}
|
|
4835
|
+
return path;
|
|
4836
|
+
}
|
|
4837
|
+
/**
|
|
4838
|
+
* Boolean (path) operation against another path, returning a NEW `Path2D` whose outline is the
|
|
4839
|
+
* polygonal result. Curves are sampled before clipping, so the result is a polygonal
|
|
4840
|
+
* approximation (see {@link polygonBoolean}). The result inherits this path's `style` unless
|
|
4841
|
+
* overridden via `style`. Holes are emitted as oppositely-wound sub-paths (nonzero fill).
|
|
4842
|
+
*/
|
|
4843
|
+
booleanOp(op, other, style) {
|
|
4844
|
+
const rings = polygonBoolean(op, this._getRings(), other._getRings());
|
|
4845
|
+
return Path2D.fromRings(rings, { ...this.style, ...style });
|
|
4846
|
+
}
|
|
4847
|
+
/** `this ∪ other` — the combined filled area. */
|
|
4848
|
+
union(other, style) {
|
|
4849
|
+
return this.booleanOp("union", other, style);
|
|
4850
|
+
}
|
|
4851
|
+
/** `this ∩ other` — only the overlapping area. */
|
|
4852
|
+
intersection(other, style) {
|
|
4853
|
+
return this.booleanOp("intersection", other, style);
|
|
4854
|
+
}
|
|
4855
|
+
/** `this − other` — this path with `other` cut away. */
|
|
4856
|
+
difference(other, style) {
|
|
4857
|
+
return this.booleanOp("difference", other, style);
|
|
4858
|
+
}
|
|
4859
|
+
/** `this ⊕ other` — areas covered by exactly one of the two paths. */
|
|
4860
|
+
xor(other, style) {
|
|
4861
|
+
return this.booleanOp("xor", other, style);
|
|
4394
4862
|
}
|
|
4395
4863
|
/**
|
|
4396
4864
|
* Test whether a point lies on this path's stroke. A hit on any sub-path counts.
|
|
@@ -4409,35 +4877,17 @@ class Path2D extends CompositeCurve {
|
|
|
4409
4877
|
}));
|
|
4410
4878
|
}
|
|
4411
4879
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN, withStyle = true) {
|
|
4412
|
-
const strokeWidth = this.strokeWidth;
|
|
4413
4880
|
this.curves.forEach((curve) => {
|
|
4414
4881
|
curve.getMinMax(min, max);
|
|
4415
|
-
if (withStyle) {
|
|
4416
|
-
if (strokeWidth > 1) {
|
|
4417
|
-
const halfStrokeWidth = strokeWidth / 2;
|
|
4418
|
-
const isClockwise = curve.isClockwise();
|
|
4419
|
-
const points = [];
|
|
4420
|
-
for (let t = 0; t <= 1; t += 1 / curve.arcLengthDivision) {
|
|
4421
|
-
const point = curve.getPoint(t);
|
|
4422
|
-
const normal = curve.getNormal(t);
|
|
4423
|
-
const dist1 = normal.clone().scale(isClockwise ? halfStrokeWidth : -halfStrokeWidth);
|
|
4424
|
-
const dist2 = normal.clone().scale(isClockwise ? -halfStrokeWidth : halfStrokeWidth);
|
|
4425
|
-
points.push(
|
|
4426
|
-
point.clone().add(dist1),
|
|
4427
|
-
point.clone().add(dist2),
|
|
4428
|
-
point.clone().add({ x: halfStrokeWidth, y: 0 }),
|
|
4429
|
-
point.clone().add({ x: -halfStrokeWidth, y: 0 }),
|
|
4430
|
-
point.clone().add({ x: 0, y: halfStrokeWidth }),
|
|
4431
|
-
point.clone().add({ x: 0, y: -halfStrokeWidth }),
|
|
4432
|
-
point.clone().add({ x: halfStrokeWidth, y: halfStrokeWidth }),
|
|
4433
|
-
point.clone().add({ x: -halfStrokeWidth, y: -halfStrokeWidth })
|
|
4434
|
-
);
|
|
4435
|
-
}
|
|
4436
|
-
min.clampMin(...points);
|
|
4437
|
-
max.clampMax(...points);
|
|
4438
|
-
}
|
|
4439
|
-
}
|
|
4440
4882
|
});
|
|
4883
|
+
if (withStyle) {
|
|
4884
|
+
const strokeWidth = this.strokeWidth;
|
|
4885
|
+
if (strokeWidth > 1 && Number.isFinite(min.x)) {
|
|
4886
|
+
const half = strokeWidth / 2;
|
|
4887
|
+
min.set(min.x - half, min.y - half);
|
|
4888
|
+
max.set(max.x + half, max.y + half);
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4441
4891
|
return { min: min.finite(), max: max.finite() };
|
|
4442
4892
|
}
|
|
4443
4893
|
strokeTriangulate(options) {
|
|
@@ -4509,7 +4959,7 @@ class Path2D extends CompositeCurve {
|
|
|
4509
4959
|
}
|
|
4510
4960
|
drawTo(ctx, style = {}) {
|
|
4511
4961
|
style = { ...this.style, ...style };
|
|
4512
|
-
const { fill = "#000", stroke = "none" } = style;
|
|
4962
|
+
const { fill = "#000", stroke = "none", fillRule = "nonzero" } = style;
|
|
4513
4963
|
ctx.beginPath();
|
|
4514
4964
|
ctx.save();
|
|
4515
4965
|
setCanvasContext(ctx, style);
|
|
@@ -4517,7 +4967,7 @@ class Path2D extends CompositeCurve {
|
|
|
4517
4967
|
path.drawTo(ctx);
|
|
4518
4968
|
});
|
|
4519
4969
|
if (fill !== "none") {
|
|
4520
|
-
ctx.fill();
|
|
4970
|
+
ctx.fill(fillRule);
|
|
4521
4971
|
}
|
|
4522
4972
|
if (stroke !== "none") {
|
|
4523
4973
|
ctx.stroke();
|
|
@@ -4723,6 +5173,44 @@ ${content}
|
|
|
4723
5173
|
}
|
|
4724
5174
|
}
|
|
4725
5175
|
|
|
5176
|
+
class PathMeasure {
|
|
5177
|
+
constructor(curve) {
|
|
5178
|
+
this.curve = curve;
|
|
5179
|
+
}
|
|
5180
|
+
/** Total arc length of the path. */
|
|
5181
|
+
getLength() {
|
|
5182
|
+
return this.curve.getLength();
|
|
5183
|
+
}
|
|
5184
|
+
/** Whether the path forms a closed loop (see {@link Curve.isClosed}). */
|
|
5185
|
+
isClosed() {
|
|
5186
|
+
return this.curve.isClosed();
|
|
5187
|
+
}
|
|
5188
|
+
/** Point + unit tangent + tangent angle at an absolute arc-length `distance` (clamped). */
|
|
5189
|
+
getPosTan(distance) {
|
|
5190
|
+
return this.curve.getPosTan(distance);
|
|
5191
|
+
}
|
|
5192
|
+
/** Point at an absolute arc-length `distance` (clamped to `[0, getLength()]`). */
|
|
5193
|
+
getPosition(distance) {
|
|
5194
|
+
return this.curve.getPosTan(distance).position;
|
|
5195
|
+
}
|
|
5196
|
+
/** Point + tangent at a normalized progress `t ∈ [0, 1]` along the path. */
|
|
5197
|
+
getPosTanAtProgress(t) {
|
|
5198
|
+
return this.curve.getPosTan(this.getLength() * t);
|
|
5199
|
+
}
|
|
5200
|
+
/**
|
|
5201
|
+
* Evenly sample the path into `count + 1` {@link PosTan} entries (arc-length spaced), e.g. to
|
|
5202
|
+
* lay glyphs along a path or drive an `animate(progress)`-style traversal.
|
|
5203
|
+
*/
|
|
5204
|
+
sample(count = 100) {
|
|
5205
|
+
const length = this.getLength();
|
|
5206
|
+
const out = [];
|
|
5207
|
+
for (let i = 0; i <= count; i++) {
|
|
5208
|
+
out.push(this.curve.getPosTan(length * i / count));
|
|
5209
|
+
}
|
|
5210
|
+
return out;
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
|
|
4726
5214
|
class FFDControlGrid {
|
|
4727
5215
|
constructor(rows, cols, width = 1, height = 1) {
|
|
4728
5216
|
this.rows = rows;
|
|
@@ -4792,6 +5280,7 @@ exports.PI = PI;
|
|
|
4792
5280
|
exports.PI_2 = PI_2;
|
|
4793
5281
|
exports.Path2D = Path2D;
|
|
4794
5282
|
exports.Path2DSet = Path2DSet;
|
|
5283
|
+
exports.PathMeasure = PathMeasure;
|
|
4795
5284
|
exports.PolygonCurve = PolygonCurve;
|
|
4796
5285
|
exports.QuadraticBezierCurve = QuadraticBezierCurve;
|
|
4797
5286
|
exports.RectangleCurve = RectangleCurve;
|
|
@@ -4818,7 +5307,9 @@ exports.pointInPolygon = pointInPolygon;
|
|
|
4818
5307
|
exports.pointInPolygons = pointInPolygons;
|
|
4819
5308
|
exports.pointToPolylineDistance = pointToPolylineDistance;
|
|
4820
5309
|
exports.pointToSegmentDistance = pointToSegmentDistance;
|
|
5310
|
+
exports.polygonBoolean = polygonBoolean;
|
|
4821
5311
|
exports.quadraticBezier = quadraticBezier;
|
|
5312
|
+
exports.resolveLineStyle = resolveLineStyle;
|
|
4822
5313
|
exports.setCanvasContext = setCanvasContext;
|
|
4823
5314
|
exports.strokeTriangulate = strokeTriangulate;
|
|
4824
5315
|
exports.svgPathCommandsAddToPath2D = svgPathCommandsAddToPath2D;
|