@pixerate/editor 0.36.0 → 0.38.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.
@@ -275,6 +275,25 @@ function buildOverlay(inputs, options) {
275
275
  const edges = [...byKey.values()].filter((e) => e.mult.some((m) => m !== 0));
276
276
  const right = [];
277
277
  const left = [];
278
+ const bandCount = Math.max(1, Math.min(4096, Math.ceil(Math.sqrt(edges.length))));
279
+ const bandIndex = (horizontal) => {
280
+ const lo = (e) => horizontal ? Math.min(-e.a.x, -e.b.x) : Math.min(e.a.y, e.b.y);
281
+ const hi = (e) => horizontal ? Math.max(-e.a.x, -e.b.x) : Math.max(e.a.y, e.b.y);
282
+ let min = Infinity;
283
+ let max = -Infinity;
284
+ for (const e of edges) {
285
+ min = Math.min(min, lo(e));
286
+ max = Math.max(max, hi(e));
287
+ }
288
+ const size = max > min ? (max - min) / bandCount : 1;
289
+ const band = (v) => Math.min(bandCount - 1, Math.max(0, Math.floor((v - min) / size)));
290
+ const bands = Array.from({ length: bandCount }, () => []);
291
+ edges.forEach((e, j) => {
292
+ for (let k = band(lo(e)); k <= band(hi(e)); k++) bands[k].push(j);
293
+ });
294
+ return (v) => bands[band(v)];
295
+ };
296
+ const bandsFor = /* @__PURE__ */ new Map();
278
297
  for (let i = 0; i < edges.length; i++) {
279
298
  const edge = edges[i];
280
299
  const horizontal = Math.abs(edge.b.y - edge.a.y) < Math.abs(edge.b.x - edge.a.x);
@@ -287,7 +306,8 @@ function buildOverlay(inputs, options) {
287
306
  const mx = (ax + bx) / 2;
288
307
  const my = (ay + by) / 2;
289
308
  const winding = new Array(count).fill(0);
290
- for (let j = 0; j < edges.length; j++) {
309
+ if (!bandsFor.has(horizontal)) bandsFor.set(horizontal, bandIndex(horizontal));
310
+ for (const j of bandsFor.get(horizontal)(my)) {
291
311
  if (j === i) continue;
292
312
  const other = edges[j];
293
313
  const oax = tx(other.a);
@@ -2416,10 +2436,10 @@ function normalizeOperand(input) {
2416
2436
  }
2417
2437
  return { chains, fillRule };
2418
2438
  }
2419
- function scaleOf(operands) {
2439
+ function scaleOf(operandChains) {
2420
2440
  let max = 0;
2421
- for (const op of operands) {
2422
- for (const chain of op.chains) {
2441
+ for (const chains of operandChains) {
2442
+ for (const chain of chains) {
2423
2443
  for (const seg of chain) {
2424
2444
  for (const [x, y] of seg) max = Math.max(max, Math.abs(x), Math.abs(y));
2425
2445
  }
@@ -2825,15 +2845,14 @@ function splitPass2(pieces, pool, ctx) {
2825
2845
  }
2826
2846
  return { pieces: out, changed: true };
2827
2847
  }
2828
- function buildArrangement(inputs, options) {
2829
- const operands = inputs.map(normalizeOperand);
2830
- const scale2 = scaleOf(operands);
2848
+ function splitChains(operandChains, options) {
2849
+ const scale2 = scaleOf(operandChains);
2831
2850
  const eps = options.epsilon && options.epsilon > 0 ? options.epsilon : 1e-9 * scale2;
2832
2851
  const pool = new VertexPool(eps);
2833
2852
  const sources = [];
2834
2853
  let pieces = [];
2835
- operands.forEach((op, operand) => {
2836
- for (const chain of op.chains) {
2854
+ operandChains.forEach((chains, operand) => {
2855
+ for (const chain of chains) {
2837
2856
  for (const raw of chain) {
2838
2857
  const straight = raw.length > 2 && isStraightSegment(raw, eps * 0.01);
2839
2858
  const seg = straight ? [raw[0], raw[raw.length - 1]] : raw;
@@ -2861,6 +2880,18 @@ function buildArrangement(inputs, options) {
2861
2880
  }
2862
2881
  }
2863
2882
  if (!settled) throw new PathOpsError("intersections did not settle");
2883
+ return { pieces, pool, sources, eps, scale: scale2 };
2884
+ }
2885
+ function samePiece(e, pts, eps) {
2886
+ if (e.length === 2 && pts.length === 2) return true;
2887
+ return segmentNearest(e, segmentPoint(pts, 0.5)).distance <= eps * 4 && segmentNearest(pts, segmentPoint(e, 0.5)).distance <= eps * 4;
2888
+ }
2889
+ function buildArrangement(inputs, options) {
2890
+ const operands = inputs.map(normalizeOperand);
2891
+ const { pieces, sources, eps, scale: scale2 } = splitChains(
2892
+ operands.map((o) => o.chains),
2893
+ options
2894
+ );
2864
2895
  const count = operands.length;
2865
2896
  const groups = /* @__PURE__ */ new Map();
2866
2897
  const edges = [];
@@ -2872,11 +2903,7 @@ function buildArrangement(inputs, options) {
2872
2903
  const key = `${a.id},${b.id}`;
2873
2904
  const group = groups.get(key) ?? [];
2874
2905
  if (!groups.has(key)) groups.set(key, group);
2875
- const mid = segmentPoint(pts, 0.5);
2876
- let edge = group.find((e) => {
2877
- if (e.pts.length === 2 && pts.length === 2) return true;
2878
- return segmentNearest(e.pts, mid).distance <= eps * 4 && segmentNearest(pts, segmentPoint(e.pts, 0.5)).distance <= eps * 4;
2879
- });
2906
+ let edge = group.find((e) => samePiece(e.pts, pts, eps));
2880
2907
  if (!edge) {
2881
2908
  edge = {
2882
2909
  a,
@@ -3269,6 +3296,63 @@ function nestedRingsToPath(rings, keep = () => true) {
3269
3296
  });
3270
3297
  return contoursToPath(contours);
3271
3298
  }
3299
+ function planarSplit(inputs, options = {}) {
3300
+ for (const chains of inputs) {
3301
+ for (const chain of chains) {
3302
+ for (const seg of chain) for (const p of seg) if (!Number.isFinite(p[0]) || !Number.isFinite(p[1])) throw new PathOpsError(NON_FINITE);
3303
+ }
3304
+ }
3305
+ const { pieces, pool, sources, eps, scale: scale2 } = splitChains(inputs, options);
3306
+ const groups = /* @__PURE__ */ new Map();
3307
+ const edges = [];
3308
+ for (const piece of pieces) {
3309
+ const forward = piece.a.id < piece.b.id;
3310
+ const a = forward ? piece.a : piece.b;
3311
+ const b = forward ? piece.b : piece.a;
3312
+ const pts = forward ? piece.pts : [...piece.pts].reverse();
3313
+ const key = `${a.id},${b.id}`;
3314
+ let group = groups.get(key);
3315
+ if (!group) groups.set(key, group = []);
3316
+ const edge = group.find((e) => samePiece(e.pts, pts, eps));
3317
+ if (edge) {
3318
+ if (!edge.operands.includes(piece.operand)) edge.operands = [...edge.operands, piece.operand].sort((x, y) => x - y);
3319
+ continue;
3320
+ }
3321
+ const created = { a: a.id, b: b.id, pts, operands: [piece.operand], src: piece.src, sa: forward ? piece.s0 : piece.s1, sb: forward ? piece.s1 : piece.s0 };
3322
+ group.push(created);
3323
+ edges.push(created);
3324
+ }
3325
+ return { vertices: pool.vertices.map((v) => v.p), edges, sources: sources.map((s) => s.seg), eps, scale: scale2 };
3326
+ }
3327
+ function pathBooleanSelect(operands, selectors, options = {}) {
3328
+ if (operands.length === 0) return { parts: selectors.map(() => []), engine: "curve" };
3329
+ const covering = (flags) => flags.flatMap((f, k) => f ? [k] : []);
3330
+ try {
3331
+ const arr = buildArrangement(operands, options);
3332
+ const parts = selectors.map((select) => {
3333
+ const rings = extractRings(arr, (flags) => select(covering(flags)));
3334
+ return groupRings(rings).sort((a, b) => b[0].area - a[0].area).map(ringsToPath2);
3335
+ });
3336
+ return { parts, engine: "curve" };
3337
+ } catch (error) {
3338
+ const reason = reasonOf(error);
3339
+ if (reason === NON_FINITE) return { parts: selectors.map(() => []), engine: "curve", error: reason };
3340
+ if (options.fallback === false) throw error;
3341
+ options.onFallback?.(reason);
3342
+ const tolerance = options.fallbackTolerance ?? 0.05;
3343
+ const pieces = dividePolygons(
3344
+ operands.map((o) => flattenOperand(o, tolerance)),
3345
+ { epsilon: options.epsilon }
3346
+ );
3347
+ const parts = selectors.map((select) => {
3348
+ const picked = pieces.filter((p) => select(p.operands));
3349
+ if (!picked.length) return [];
3350
+ const multi = polygonBoolean("union", [picked.map((p) => p.polygon)], { epsilon: options.epsilon });
3351
+ return multi.map((polygon) => multiToPath([polygon], tolerance));
3352
+ });
3353
+ return { parts, engine: "polygon", fallbackReason: reason };
3354
+ }
3355
+ }
3272
3356
 
3273
3357
  // src/canvas/pathops/stroke.ts
3274
3358
  var DEFAULT_TOLERANCE = 0.01;
@@ -3711,6 +3795,567 @@ function offsetPath(input, distance, options = {}) {
3711
3795
  }
3712
3796
  }
3713
3797
 
3798
+ // src/canvas/pathops/planar.ts
3799
+ var twin = (h) => h ^ 1;
3800
+ function boundsOf(segs) {
3801
+ let minX = Infinity;
3802
+ let minY = Infinity;
3803
+ let maxX = -Infinity;
3804
+ let maxY = -Infinity;
3805
+ for (const s of segs) {
3806
+ for (const [x, y] of s) {
3807
+ if (x < minX) minX = x;
3808
+ if (x > maxX) maxX = x;
3809
+ if (y < minY) minY = y;
3810
+ if (y > maxY) maxY = y;
3811
+ }
3812
+ }
3813
+ return { minX, minY, maxX, maxY };
3814
+ }
3815
+ var inBounds = (b, p) => p[0] >= b.minX && p[0] <= b.maxX && p[1] >= b.minY && p[1] <= b.maxY;
3816
+ function withoutSpurs(cycle) {
3817
+ const out = [];
3818
+ for (const h of cycle) {
3819
+ if (out.length && out[out.length - 1] === twin(h)) out.pop();
3820
+ else out.push(h);
3821
+ }
3822
+ while (out.length >= 2 && twin(out[0]) === out[out.length - 1]) {
3823
+ out.pop();
3824
+ out.shift();
3825
+ }
3826
+ return out;
3827
+ }
3828
+ function pinEnds(pts, a, b) {
3829
+ const n = pts.length;
3830
+ if (n === 2) return [a, b];
3831
+ const out = pts.map((p) => [p[0], p[1]]);
3832
+ const d0 = [a[0] - pts[0][0], a[1] - pts[0][1]];
3833
+ const d1 = [b[0] - pts[n - 1][0], b[1] - pts[n - 1][1]];
3834
+ out[0] = a;
3835
+ out[n - 1] = b;
3836
+ if (n === 3) {
3837
+ out[1] = [pts[1][0] + (d0[0] + d1[0]) / 2, pts[1][1] + (d0[1] + d1[1]) / 2];
3838
+ } else {
3839
+ out[1] = [pts[1][0] + d0[0], pts[1][1] + d0[1]];
3840
+ out[2] = [pts[2][0] + d1[0], pts[2][1] + d1[1]];
3841
+ }
3842
+ return out;
3843
+ }
3844
+ function interiorPoint(path) {
3845
+ const chains = Array.isArray(path) && path.length > 0 && Array.isArray(path[0]) && Array.isArray(path[0][0]?.[0]) ? path : pathToContours(path).map((c) => closeContour(c, true));
3846
+ const b = boundsOf(chains.flat());
3847
+ if (!(b.maxX > b.minX) || !(b.maxY > b.minY)) return null;
3848
+ const tol = Math.max(b.maxX - b.minX, b.maxY - b.minY) * 1e-3;
3849
+ const rings = chains.filter((c) => c.length).map((c) => flattenChain(c, tol));
3850
+ let best = null;
3851
+ for (const f of [0.5, 0.25, 0.75, 0.125, 0.375, 0.625, 0.875, 0.0625, 0.9375]) {
3852
+ const y = b.minY + (b.maxY - b.minY) * (f + 61803e-7);
3853
+ const xs = [];
3854
+ for (const ring of rings) {
3855
+ for (let i = 0; i < ring.length; i++) {
3856
+ const a = ring[i];
3857
+ const c = ring[(i + 1) % ring.length];
3858
+ if (a[1] <= y !== c[1] <= y) xs.push(a[0] + (y - a[1]) / (c[1] - a[1]) * (c[0] - a[0]));
3859
+ }
3860
+ }
3861
+ xs.sort((p, q) => p - q);
3862
+ for (let i = 0; i + 1 < xs.length; i++) {
3863
+ const width = xs[i + 1] - xs[i];
3864
+ if (!(width > (best?.width ?? 0))) continue;
3865
+ const m = [(xs[i] + xs[i + 1]) / 2, y];
3866
+ if (windingNumber(chains, m) !== 0) best = { width, p: m };
3867
+ }
3868
+ }
3869
+ return best?.p ?? null;
3870
+ }
3871
+ var PlanarMap = class _PlanarMap {
3872
+ constructor(vertices, edges, sources, eps, inputCount, inputs) {
3873
+ this.vertices = vertices;
3874
+ this.edges = edges;
3875
+ this.sources = sources;
3876
+ this.eps = eps;
3877
+ this.inputCount = inputCount;
3878
+ this.fills = inputs.map((input) => {
3879
+ const chains = input.fills === false ? [] : pathToContours(input.path).filter((c) => c.closed).map((c) => closeContour(c));
3880
+ return { chains, fillRule: input.fillRule ?? "nonzero", bounds: boundsOf(chains.flat()) };
3881
+ });
3882
+ const nv = vertices.length;
3883
+ this.around = Array.from({ length: nv }, () => []);
3884
+ for (let h = 0; h < edges.length * 2; h++) this.around[this.origin(h)].push(h);
3885
+ this.slot = new Array(edges.length * 2).fill(0);
3886
+ this.around.forEach((list, v) => {
3887
+ if (list.length === 0) return;
3888
+ const at = vertices[v];
3889
+ let r = Infinity;
3890
+ for (const h of list) r = Math.min(r, dist2(at, vertices[this.origin(twin(h))]));
3891
+ r *= 0.5;
3892
+ const keyed = list.map((h) => ({ h, angle: this.leavingAngle(h, at, r) }));
3893
+ keyed.sort((a, b) => a.angle - b.angle);
3894
+ list.length = 0;
3895
+ keyed.forEach(({ h }, k) => {
3896
+ list.push(h);
3897
+ this.slot[h] = k;
3898
+ });
3899
+ });
3900
+ const parent = Array.from({ length: nv }, (_, i) => i);
3901
+ const find = (v) => {
3902
+ while (parent[v] !== v) {
3903
+ parent[v] = parent[parent[v]];
3904
+ v = parent[v];
3905
+ }
3906
+ return v;
3907
+ };
3908
+ for (const e of edges) parent[find(e.a)] = find(e.b);
3909
+ this.component = Array.from({ length: nv }, (_, v) => find(v));
3910
+ }
3911
+ vertices;
3912
+ edges;
3913
+ sources;
3914
+ eps;
3915
+ inputCount;
3916
+ around;
3917
+ slot;
3918
+ component;
3919
+ cyclesCache = null;
3920
+ sourcesCache = [];
3921
+ fills;
3922
+ /**
3923
+ * Maps `inputs` (their subpaths as given: open ones stay open). Null when they cannot be mapped
3924
+ * (non-finite points, more than `maxSegments` segments or an intersection search that does
3925
+ * not settle) or have no edges.
3926
+ */
3927
+ static build(inputs, options = {}) {
3928
+ const chains = inputs.map((input) => pathToContours(input.path).map((c) => closeContour(c)).filter((c) => c.length));
3929
+ const count = chains.reduce((n, cs) => n + cs.reduce((m, c) => m + c.length, 0), 0);
3930
+ if (count === 0 || count > (options.maxSegments ?? 4096)) return null;
3931
+ let split;
3932
+ try {
3933
+ split = planarSplit(chains, options);
3934
+ } catch {
3935
+ return null;
3936
+ }
3937
+ const edges = split.edges.filter((e) => e.a !== e.b).map((e) => ({ ...e, source: e.operands[e.operands.length - 1] ?? -1 }));
3938
+ if (!edges.length) return null;
3939
+ const gap = options.gap ?? 0;
3940
+ if (gap > split.eps) closeGaps(split.vertices, edges, gap);
3941
+ return new _PlanarMap(split.vertices, edges, split.sources, split.eps, inputs.length, inputs);
3942
+ }
3943
+ // -- half-edges ------------------------------------------------------------
3944
+ origin(h) {
3945
+ const e = this.edges[h >> 1];
3946
+ return h & 1 ? e.b : e.a;
3947
+ }
3948
+ halfPts(h) {
3949
+ const pts = this.edges[h >> 1].pts;
3950
+ return h & 1 ? [...pts].reverse() : pts;
3951
+ }
3952
+ /** The half-edge after `h` on the face to its left: the sharpest left turn at its end. */
3953
+ next(h) {
3954
+ const back = twin(h);
3955
+ const list = this.around[this.origin(back)];
3956
+ return list[(this.slot[back] + list.length - 1) % list.length];
3957
+ }
3958
+ /** Direction (angle) in which half-edge `h`, leaving `at`, first gets `r` away from it. */
3959
+ leavingAngle(h, at, r) {
3960
+ const pts = this.halfPts(h);
3961
+ const d = (t2) => dist2(segmentPoint(pts, t2), at);
3962
+ let lo = 0;
3963
+ let hi = 1;
3964
+ const steps = pts.length === 2 ? 1 : 16;
3965
+ for (let k = 1; k <= steps; k++) {
3966
+ if (d(k / steps) >= r) {
3967
+ hi = k / steps;
3968
+ break;
3969
+ }
3970
+ lo = k / steps;
3971
+ }
3972
+ for (let i = 0; i < 40 && hi - lo > 1e-12; i++) {
3973
+ const m = (lo + hi) / 2;
3974
+ if (d(m) >= r) hi = m;
3975
+ else lo = m;
3976
+ }
3977
+ const p = segmentPoint(pts, hi);
3978
+ if (dist2(p, at) > 0) return Math.atan2(p[1] - at[1], p[0] - at[0]);
3979
+ const t = segmentTangent(pts, 0);
3980
+ return Math.atan2(t[1], t[0]);
3981
+ }
3982
+ /** Segments of a run of half-edges (a loop unless `closed` is false), pieces of one input segment joined again. */
3983
+ cycleSegments(halves, closed = true) {
3984
+ const runs = halves.map((h) => {
3985
+ const e = this.edges[h >> 1];
3986
+ return { h, src: e.source < 0 ? -1 : e.src, from: h & 1 ? e.sb : e.sa, to: h & 1 ? e.sa : e.sb };
3987
+ });
3988
+ const joins = (a, b) => a.src >= 0 && a.src === b.src && Math.abs(a.to - b.from) < 1e-9 && a.to - a.from > 0 === b.to - b.from > 0;
3989
+ const n = runs.length;
3990
+ let start = 0;
3991
+ for (let k = 0; closed && k < n; k++) {
3992
+ if (!joins(runs[(k + n - 1) % n], runs[k])) {
3993
+ start = k;
3994
+ break;
3995
+ }
3996
+ }
3997
+ const ordered = [...runs.slice(start), ...runs.slice(0, start)];
3998
+ const out = [];
3999
+ let i = 0;
4000
+ while (i < ordered.length) {
4001
+ let j = i + 1;
4002
+ while (j < ordered.length && joins(ordered[j - 1], ordered[j])) j++;
4003
+ const first = ordered[i];
4004
+ const last = ordered[j - 1];
4005
+ if (j - i === 1) out.push(this.halfPts(first.h));
4006
+ else {
4007
+ const a = this.vertices[this.origin(first.h)];
4008
+ const b = this.vertices[this.origin(twin(last.h))];
4009
+ const seg = this.sources[first.src];
4010
+ out.push(seg.length === 2 ? [a, b] : pinEnds(subSegment(seg, first.from, last.to), a, b));
4011
+ }
4012
+ i = j;
4013
+ }
4014
+ return out;
4015
+ }
4016
+ cycles() {
4017
+ if (this.cyclesCache) return this.cyclesCache;
4018
+ const halves = this.edges.length * 2;
4019
+ const seen = new Uint8Array(halves);
4020
+ const faces = [];
4021
+ const holes = [];
4022
+ const sliver = this.eps * 10;
4023
+ for (let start = 0; start < halves; start++) {
4024
+ if (seen[start]) continue;
4025
+ const walk = [];
4026
+ let h = start;
4027
+ while (!seen[h]) {
4028
+ seen[h] = 1;
4029
+ walk.push(h);
4030
+ h = this.next(h);
4031
+ }
4032
+ const cycle = withoutSpurs(walk);
4033
+ if (!cycle.length) continue;
4034
+ const segments = this.cycleSegments(cycle);
4035
+ const area = contourSignedArea(segments);
4036
+ let perimeter = 0;
4037
+ for (const s of segments) perimeter += s.length === 2 ? dist2(s[0], s[1]) : segmentLength(s, 0, 1, 1e-6);
4038
+ if (!(perimeter > 0)) continue;
4039
+ const entry = { halves: cycle, segments, area, bounds: boundsOf(segments) };
4040
+ if (area > 0 && 2 * area / perimeter >= sliver) faces.push(entry);
4041
+ else if (area < 0 && -2 * area / perimeter >= sliver) holes.push(entry);
4042
+ }
4043
+ const faceHoles = faces.map(() => []);
4044
+ holes.forEach((hole, k) => {
4045
+ const v = this.origin(hole.halves[0]);
4046
+ const p = this.vertices[v];
4047
+ const own = this.component[v];
4048
+ let best = -1;
4049
+ faces.forEach((face, i) => {
4050
+ if (this.component[this.origin(face.halves[0])] === own || !inBounds(face.bounds, p)) return;
4051
+ if (best >= 0 && faces[best].area <= face.area) return;
4052
+ if (windingNumber([face.segments], p) !== 0) best = i;
4053
+ });
4054
+ if (best >= 0) faceHoles[best].push(k);
4055
+ });
4056
+ const faceOf = new Int32Array(halves).fill(-1);
4057
+ faces.forEach((face, i) => {
4058
+ for (const h of face.halves) faceOf[h] = i;
4059
+ for (const k of faceHoles[i]) for (const h of holes[k].halves) faceOf[h] = i;
4060
+ });
4061
+ this.cyclesCache = { faces, holes, faceHoles, faceOf };
4062
+ this.sourcesCache = faces.map(() => null);
4063
+ return this.cyclesCache;
4064
+ }
4065
+ faceChains(i) {
4066
+ const { faces, holes, faceHoles } = this.cycles();
4067
+ return [faces[i].segments, ...faceHoles[i].map((k) => holes[k].segments)];
4068
+ }
4069
+ /** The bounded faces of the map. */
4070
+ get faces() {
4071
+ const { faces, holes, faceHoles } = this.cycles();
4072
+ return faces.map((f, i) => {
4073
+ const chains = this.faceChains(i);
4074
+ const area = f.area + faceHoles[i].reduce((sum, k) => sum + holes[k].area, 0);
4075
+ return { path: contoursToPath(chains.map((segments) => ({ segments, closed: true }))), area, bounds: f.bounds };
4076
+ });
4077
+ }
4078
+ /** The inputs whose fill covers face `i`, ascending (empty for an area only outlines enclose). */
4079
+ faceSources(i) {
4080
+ this.cycles();
4081
+ const cached = this.sourcesCache[i];
4082
+ if (cached) return cached;
4083
+ const p = interiorPoint(this.faceChains(i));
4084
+ const out = [];
4085
+ if (p) {
4086
+ this.fills.forEach((fill, k) => {
4087
+ if (!fill.chains.length || !inBounds(fill.bounds, p)) return;
4088
+ const w = windingNumber(fill.chains, p);
4089
+ if (fill.fillRule === "evenodd" ? (w & 1) === 1 : w !== 0) out.push(k);
4090
+ });
4091
+ }
4092
+ this.sourcesCache[i] = out;
4093
+ return out;
4094
+ }
4095
+ /** A point well inside face `i`. */
4096
+ faceInterior(i) {
4097
+ return interiorPoint(this.faceChains(i));
4098
+ }
4099
+ /** The face under `p` (the smallest one around it), or -1. */
4100
+ faceAt(p) {
4101
+ const { faces } = this.cycles();
4102
+ let best = -1;
4103
+ faces.forEach((face, i) => {
4104
+ if (!inBounds(face.bounds, p) || best >= 0 && faces[best].area <= face.area) return;
4105
+ if (windingNumber(this.faceChains(i), p) !== 0) best = i;
4106
+ });
4107
+ return best;
4108
+ }
4109
+ /**
4110
+ * The union of faces, traced along the map's own edges: those between two merged faces drop
4111
+ * out exactly, so no seam or sliver is left where a boolean union of their outlines could
4112
+ * round shared corners apart. Outer subpaths come out counter-clockwise (y-up), holes clockwise.
4113
+ */
4114
+ merge(faceIndices) {
4115
+ const { faceOf } = this.cycles();
4116
+ const picked = new Set(faceIndices);
4117
+ const merged = (h) => faceOf[h] >= 0 && picked.has(faceOf[h]);
4118
+ const boundary = (h) => merged(h) && !merged(twin(h));
4119
+ const used = new Uint8Array(this.edges.length * 2);
4120
+ const contours = [];
4121
+ for (let start = 0; start < used.length; start++) {
4122
+ if (used[start] || !boundary(start)) continue;
4123
+ const cycle = [];
4124
+ let h = start;
4125
+ let ok = true;
4126
+ while (!used[h]) {
4127
+ used[h] = 1;
4128
+ cycle.push(h);
4129
+ const back = twin(h);
4130
+ const list = this.around[this.origin(back)];
4131
+ const k = this.slot[back];
4132
+ const n = list.length;
4133
+ let found = -1;
4134
+ for (let j = 1; j <= n; j++) {
4135
+ const g = list[(k - j + n * 2) % n];
4136
+ if (boundary(g)) {
4137
+ found = g;
4138
+ break;
4139
+ }
4140
+ }
4141
+ if (found < 0) {
4142
+ ok = false;
4143
+ break;
4144
+ }
4145
+ h = found;
4146
+ }
4147
+ if (!ok || h !== start) continue;
4148
+ contours.push({ segments: this.cycleSegments(cycle), closed: true });
4149
+ }
4150
+ return contoursToPath(contours);
4151
+ }
4152
+ /**
4153
+ * The edges joined into runs between junctions: vertices where other than two edges meet, or
4154
+ * where the two edges lie on different inputs. Each run is keyed by its input.
4155
+ */
4156
+ chains() {
4157
+ const passes = (v) => {
4158
+ const list = this.around[v];
4159
+ if (list.length !== 2) return null;
4160
+ return this.edges[list[0] >> 1].source === this.edges[list[1] >> 1].source ? [list[0], list[1]] : null;
4161
+ };
4162
+ const used = new Uint8Array(this.edges.length);
4163
+ const out = [];
4164
+ const walk = (h0) => {
4165
+ const source = this.edges[h0 >> 1].source;
4166
+ const halves = [];
4167
+ let h = h0;
4168
+ let closed = false;
4169
+ for (; ; ) {
4170
+ if (used[h >> 1]) {
4171
+ closed = true;
4172
+ break;
4173
+ }
4174
+ used[h >> 1] = 1;
4175
+ halves.push(h);
4176
+ const v = this.origin(twin(h));
4177
+ const pass = passes(v);
4178
+ if (!pass) {
4179
+ closed = v === this.origin(h0);
4180
+ break;
4181
+ }
4182
+ h = pass[0] === twin(h) ? pass[1] : pass[0];
4183
+ }
4184
+ if (source < 0) return;
4185
+ const segments = this.cycleSegments(halves, closed);
4186
+ out.push({ segments, closed, source });
4187
+ };
4188
+ this.around.forEach((list, v) => {
4189
+ if (passes(v)) return;
4190
+ for (const h of list) if (!used[h >> 1]) walk(h);
4191
+ });
4192
+ for (let e = 0; e < this.edges.length; e++) if (!used[e]) walk(e * 2);
4193
+ return out;
4194
+ }
4195
+ // -- fill regions (cadcraft `boundary_at_gap`) -------------------------------
4196
+ /**
4197
+ * The region enclosing `p`, traced as cadcraft does it: a ray from `p` towards +x meets the
4198
+ * edges in order; for each, the half-edge with `p` on its left is followed by always taking the
4199
+ * sharpest left turn, until a loop that runs counter-clockwise round `p` comes back. The pieces
4200
+ * of the map nested inside that loop (islands) become its holes. Null when nothing encloses `p`.
4201
+ */
4202
+ regionAt(p) {
4203
+ const { faces, faceOf } = this.cycles();
4204
+ const hits = [];
4205
+ this.edges.forEach((e, i) => {
4206
+ const ay = this.vertices[e.a][1];
4207
+ const by = this.vertices[e.b][1];
4208
+ if (ay > p[1] === by > p[1]) return;
4209
+ const t = solveMonotone(e.pts, 1, p[1]);
4210
+ const x = segmentPoint(e.pts, t)[0];
4211
+ if (x > p[0]) hits.push({ x, e: i });
4212
+ });
4213
+ hits.sort((a, b) => a.x - b.x);
4214
+ for (const { e } of hits.slice(0, 64)) {
4215
+ const ay = this.vertices[this.edges[e].a][1];
4216
+ const by = this.vertices[this.edges[e].b][1];
4217
+ const h = by > ay ? e * 2 : e * 2 + 1;
4218
+ const walk = [];
4219
+ const seen = /* @__PURE__ */ new Set();
4220
+ let g = h;
4221
+ while (!seen.has(g)) {
4222
+ seen.add(g);
4223
+ walk.push(g);
4224
+ g = this.next(g);
4225
+ }
4226
+ if (g !== h) continue;
4227
+ const cycle = withoutSpurs(walk);
4228
+ if (!cycle.length) continue;
4229
+ const segments = this.cycleSegments(cycle);
4230
+ if (!(contourSignedArea(segments) > 0) || windingNumber([segments], p) === 0) continue;
4231
+ const face = faceOf[cycle[0]];
4232
+ if (face >= 0 && faces[face].halves.includes(cycle[0])) {
4233
+ return contoursToPath(this.faceChains(face).map((s) => ({ segments: s, closed: true })));
4234
+ }
4235
+ return contoursToPath([{ segments, closed: true }]);
4236
+ }
4237
+ return null;
4238
+ }
4239
+ };
4240
+ function closeGaps(vertices, edges, gap) {
4241
+ const degree = new Array(vertices.length).fill(0);
4242
+ const linked = /* @__PURE__ */ new Set();
4243
+ for (const e of edges) {
4244
+ degree[e.a]++;
4245
+ degree[e.b]++;
4246
+ linked.add(`${Math.min(e.a, e.b)},${Math.max(e.a, e.b)}`);
4247
+ }
4248
+ const ends = degree.flatMap((d, v) => d === 1 ? [v] : []).sort((a, b) => vertices[a][0] - vertices[b][0]);
4249
+ const joined = /* @__PURE__ */ new Set();
4250
+ let lo = 0;
4251
+ for (let i = 0; i < ends.length; i++) {
4252
+ const u = ends[i];
4253
+ if (joined.has(u)) continue;
4254
+ const pu = vertices[u];
4255
+ while (lo < ends.length && vertices[ends[lo]][0] < pu[0] - gap) lo++;
4256
+ let best = -1;
4257
+ let bestD = Infinity;
4258
+ for (let j = lo; j < ends.length; j++) {
4259
+ const v = ends[j];
4260
+ const pv = vertices[v];
4261
+ if (pv[0] > pu[0] + gap) break;
4262
+ if (v === u || joined.has(v) || linked.has(`${Math.min(u, v)},${Math.max(u, v)}`)) continue;
4263
+ const d = dist2(pu, pv);
4264
+ if (d <= gap && d < bestD) {
4265
+ best = v;
4266
+ bestD = d;
4267
+ }
4268
+ }
4269
+ if (best < 0) continue;
4270
+ const a = Math.min(u, best);
4271
+ const b = Math.max(u, best);
4272
+ edges.push({ a, b, pts: [vertices[a], vertices[b]], operands: [], src: -1, sa: 0, sb: 1, source: -1 });
4273
+ joined.add(u);
4274
+ joined.add(best);
4275
+ }
4276
+ }
4277
+
4278
+ // src/canvas/pathops/pathfinder.ts
4279
+ var PATHFINDER_OPERATIONS = ["divide", "trim", "merge", "crop", "outline", "minusBack"];
4280
+ var top = (covering) => covering[covering.length - 1];
4281
+ function pathfinder(op, shapes, options = {}) {
4282
+ const n = shapes.length;
4283
+ if (n === 0) return { pieces: [], engine: "curve" };
4284
+ const filled = shapes.filter((s) => !s.line);
4285
+ const operands = shapes.map((s) => ({ path: s.path, fillRule: s.fillRule ?? "nonzero" }));
4286
+ if (op === "outline") {
4287
+ const map = PlanarMap.build(
4288
+ shapes.map((s) => ({ path: s.line ? s.path : closeAll(s.path), fills: false })),
4289
+ options
4290
+ );
4291
+ if (!map) return { pieces: [], engine: "curve", error: "could not map the outlines" };
4292
+ const pieces = map.chains().map((c) => ({ path: contoursToPath([{ segments: c.segments, closed: c.closed }]), source: c.source, open: !c.closed }));
4293
+ return { pieces, engine: "curve" };
4294
+ }
4295
+ if (op === "divide") {
4296
+ if (filled.length !== n) {
4297
+ const map = PlanarMap.build(shapes.map((s) => ({ path: s.line ? s.path : closeAll(s.path), fillRule: s.fillRule, fills: !s.line })), options);
4298
+ if (!map) return { pieces: [], engine: "curve", error: "could not map the shapes" };
4299
+ const pieces = [];
4300
+ map.faces.forEach((face, i) => {
4301
+ const covering = map.faceSources(i);
4302
+ if (covering.length) pieces.push({ path: face.path, source: top(covering) });
4303
+ });
4304
+ return { pieces, engine: "curve" };
4305
+ }
4306
+ const result = dividePaths(operands, options);
4307
+ return { ...report(result), pieces: result.pieces.map((p) => ({ path: p.path, source: top(p.operands) })) };
4308
+ }
4309
+ const index = shapes.flatMap((s, i) => s.line ? [] : [i]);
4310
+ const m = index.length;
4311
+ if (m === 0) return { pieces: [], engine: "curve" };
4312
+ const ops = index.map((i) => operands[i]);
4313
+ const key = (k) => shapes[index[k]].key ?? index[k];
4314
+ const select = (selectors) => pathBooleanSelect(ops, selectors, options);
4315
+ const joined = (parts) => parts.flat();
4316
+ switch (op) {
4317
+ case "trim": {
4318
+ const result = select(index.map((_, k) => (c) => c.length > 0 && top(c) === k));
4319
+ const pieces = result.parts.flatMap((parts, k) => parts.length ? [{ path: joined(parts), source: index[k] }] : []);
4320
+ return { ...report(result), pieces };
4321
+ }
4322
+ case "merge": {
4323
+ const keys = [];
4324
+ for (let k = 0; k < m; k++) if (!keys.includes(key(k))) keys.push(key(k));
4325
+ const result = select(keys.map((kk) => (c) => c.length > 0 && key(top(c)) === kk));
4326
+ const pieces = [];
4327
+ result.parts.forEach((parts, j) => {
4328
+ let source = 0;
4329
+ for (let k = 0; k < m; k++) if (key(k) === keys[j]) source = index[k];
4330
+ for (const part of parts) pieces.push({ path: part, source });
4331
+ });
4332
+ return { ...report(result), pieces };
4333
+ }
4334
+ case "crop": {
4335
+ if (m < 2) return { pieces: [], engine: "curve" };
4336
+ const front = m - 1;
4337
+ const result = select(
4338
+ index.slice(0, front).map((_, k) => (c) => c.length >= 2 && top(c) === front && c[c.length - 2] === k)
4339
+ );
4340
+ const pieces = result.parts.flatMap((parts, k) => parts.length ? [{ path: joined(parts), source: index[k] }] : []);
4341
+ return { ...report(result), pieces };
4342
+ }
4343
+ case "minusBack": {
4344
+ const front = m - 1;
4345
+ const result = select([(c) => c.length === 1 && c[0] === front]);
4346
+ const parts = result.parts[0];
4347
+ return { ...report(result), pieces: parts.length ? [{ path: joined(parts), source: index[front] }] : [] };
4348
+ }
4349
+ }
4350
+ return { pieces: [], engine: "curve" };
4351
+ }
4352
+ function report(r) {
4353
+ return { engine: r.engine, ...r.fallbackReason ? { fallbackReason: r.fallbackReason } : {}, ...r.error ? { error: r.error } : {} };
4354
+ }
4355
+ function closeAll(path) {
4356
+ return contoursToPath(pathToContours(path).map((c) => ({ segments: c.segments, closed: true })));
4357
+ }
4358
+
3714
4359
  export {
3715
4360
  ringSignedArea,
3716
4361
  polygonArea,
@@ -3776,6 +4421,7 @@ export {
3776
4421
  windingNumber,
3777
4422
  pointInBezierPath,
3778
4423
  distanceToBezierPath,
4424
+ flattenChain,
3779
4425
  PathOpsError,
3780
4426
  segmentIntersections,
3781
4427
  pathBoolean,
@@ -3786,9 +4432,15 @@ export {
3786
4432
  dividePaths,
3787
4433
  normalizeBezierPath,
3788
4434
  bezierPathArea,
4435
+ planarSplit,
4436
+ pathBooleanSelect,
3789
4437
  dashContours,
3790
4438
  strokeOutlineRings,
3791
4439
  outlineStroke,
3792
- offsetPath
4440
+ offsetPath,
4441
+ interiorPoint,
4442
+ PlanarMap,
4443
+ PATHFINDER_OPERATIONS,
4444
+ pathfinder
3793
4445
  };
3794
- //# sourceMappingURL=chunk-EGPIVUEF.js.map
4446
+ //# sourceMappingURL=chunk-ZCCU3VNI.js.map