@flighthq/path-boolean 0.5.1-next.3302.76ece6b → 0.5.1-next.4304.322dc96

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.
Files changed (66) hide show
  1. package/README.md +2 -2
  2. package/dist/.tsbuildinfo.contract +1 -0
  3. package/dist/.tsbuildinfo.runtime +1 -0
  4. package/dist/booleanPaths.d.ts +33 -6
  5. package/dist/booleanPaths.d.ts.map +1 -1
  6. package/dist/booleanPaths.js +42 -3
  7. package/dist/booleanPaths.js.map +1 -1
  8. package/dist/contract.d.ts +12 -8
  9. package/dist/contract.d.ts.map +1 -1
  10. package/dist/contract.js +11 -8
  11. package/dist/contract.js.map +1 -1
  12. package/dist/contract.path-boolean-types.d.ts +4 -0
  13. package/dist/contract.path-boolean-types.d.ts.map +1 -0
  14. package/dist/contract.path-boolean-types.js +2 -0
  15. package/dist/contract.path-boolean-types.js.map +1 -0
  16. package/dist/explainPathBoolean.d.ts +15 -3
  17. package/dist/explainPathBoolean.d.ts.map +1 -1
  18. package/dist/explainPathBoolean.js +35 -9
  19. package/dist/explainPathBoolean.js.map +1 -1
  20. package/dist/index.d.ts +3 -8
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -8
  23. package/dist/index.js.map +1 -1
  24. package/dist/martinezKernel.d.ts +44 -4
  25. package/dist/martinezKernel.d.ts.map +1 -1
  26. package/dist/martinezKernel.js +777 -559
  27. package/dist/martinezKernel.js.map +1 -1
  28. package/dist/offsetPath.d.ts +15 -2
  29. package/dist/offsetPath.d.ts.map +1 -1
  30. package/dist/offsetPath.js +205 -215
  31. package/dist/offsetPath.js.map +1 -1
  32. package/dist/pathBoolean.d.ts +53 -0
  33. package/dist/pathBoolean.d.ts.map +1 -0
  34. package/dist/pathBoolean.js +115 -0
  35. package/dist/pathBoolean.js.map +1 -0
  36. package/dist/pathBooleanCompat.d.ts +3 -0
  37. package/dist/pathBooleanCompat.d.ts.map +1 -0
  38. package/dist/pathBooleanCompat.js +2 -0
  39. package/dist/pathBooleanCompat.js.map +1 -0
  40. package/dist/resolvePathRegions.d.ts +8 -2
  41. package/dist/resolvePathRegions.d.ts.map +1 -1
  42. package/dist/resolvePathRegions.js +11 -3
  43. package/dist/resolvePathRegions.js.map +1 -1
  44. package/dist/simplifyPath.d.ts +9 -2
  45. package/dist/simplifyPath.d.ts.map +1 -1
  46. package/dist/simplifyPath.js +12 -2
  47. package/dist/simplifyPath.js.map +1 -1
  48. package/dist/unionAllPaths.d.ts +9 -2
  49. package/dist/unionAllPaths.d.ts.map +1 -1
  50. package/dist/unionAllPaths.js +23 -8
  51. package/dist/unionAllPaths.js.map +1 -1
  52. package/dist/writePathBooleanContours.d.ts +8 -2
  53. package/dist/writePathBooleanContours.d.ts.map +1 -1
  54. package/dist/writePathBooleanContours.js +16 -4
  55. package/dist/writePathBooleanContours.js.map +1 -1
  56. package/package.json +5 -4
  57. package/src/booleanPaths.test.ts +237 -114
  58. package/src/explainPathBoolean.test.ts +35 -21
  59. package/src/fuzzInvariants.test.ts +241 -154
  60. package/src/martinezKernel.test.ts +334 -328
  61. package/src/offsetPath.test.ts +296 -166
  62. package/src/pathBoolean.test.ts +95 -0
  63. package/src/resolvePathRegions.test.ts +41 -30
  64. package/src/simplifyPath.test.ts +143 -112
  65. package/src/unionAllPaths.test.ts +93 -61
  66. package/src/writePathBooleanContours.test.ts +44 -6
@@ -1,330 +1,778 @@
1
- import { allocateEntity, finishEntity } from '@flighthq/entity/contract';
2
- export function createMartinezPathBooleanKernel() {
3
- const out = allocateEntity();
4
- initializeMartinezPathBooleanKernel(out);
5
- return finishEntity(out);
6
- }
7
- // The Martinez–Rueda–Feito kernel: a from-scratch, floating-point sweep-line over caller-owned contours.
8
- //
9
- //
10
- // The pipeline runs in two stages, deliberately separated so the correctness-critical classification
11
- // never races the arrangement construction:
12
- //
13
- // 1. Arrangement (the sweep). An event priority queue holds the left/right endpoint events of every
14
- // input edge. Sweeping left-to-right maintains a status of the active segments ordered by their y
15
- // at the sweep; each segment is only ever tested for intersection against its immediate status
16
- // neighbors. A proper crossing splits both segments at the crossing point; a collinear overlap
17
- // splits each at the other's interior endpoints so the shared span becomes geometrically identical
18
- // sub-segments. When a segment's right endpoint is reached it is final and recorded.
19
- // 2. Classification + assembly (a static pass over the final segments). Coincident sub-segments are
20
- // merged by geometry, summing each operand's winding contribution. Every unique segment is then
21
- // classified by the winding number just below vs just above it (a downward ray from its midpoint),
22
- // under the fill rule, for the operation — the boundary test. In-result segments are oriented so
23
- // the kept region lies on their left and traced into closed rings.
24
- //
25
- // Handling coincidence in the static pass rather than mid-sweep is the key robustness decision: winding
26
- // deltas are all final before any membership is computed, so both fill rules and both same/opposite
27
- // edge orientations fall out of the same arithmetic with no field-staleness. It trades the classic
28
- // algorithm's O(n log n) inline classification for an O(n²) post-pass, which is the right call for a
29
- // correctness-first kernel on the modest polygon sizes booleans see.
30
- export function initializeMartinezPathBooleanKernel(out) {
31
- out.computePathBoolean = (subject, clip, operation, fillRule) => {
32
- return computeMartinezBoolean(subject, clip, operation, fillRule);
33
- };
34
- }
35
- // The kernel the boolean operations use unless the caller passes another one. A stateless value, so it is
36
- // one shared instance rather than a per-call construction: a second instance would be indistinguishable
37
- // and would only break the identity a caller may use to recognise the stock kernel. Annotated with its
38
- // concrete type because an exported const whose type is inferred lowers to an anonymous struct of
39
- // type-erased closures in the C++ emitter; see agents/portability.md.
40
- export const martinezPathBooleanKernel = createMartinezPathBooleanKernel();
41
- // Relative factor for the vertex-merge grid: two vertices collapse to one when they fall in the same
42
- // cell of a grid this fraction of the input's coordinate extent. Large enough to catch the last-bit
43
- // numerical twins a crossing computation produces (a few ulps of the extent), far below any real
44
- // feature separation, so it only ever fuses points that were meant to be identical. Made relative to
45
- // the coordinate magnitude (rather than a fixed absolute) so the same topology resolves whether the
46
- // polygon is expressed in pixels, metres, or micrometres.
1
+ // Generated from crates/flight-path-boolean/src/martinez_kernel.rs by flight-compiler.
2
+ // Rust is authoritative; run `npm run generate` after changing it.
3
+ import { allocateEntity } from '@flighthq/entity/contract';
4
+ /**
5
+ * Relative factor for the vertex-merge grid: two vertices collapse to one when
6
+ * they fall in the same cell of a grid this fraction of the input's coordinate
7
+ * extent.
8
+ */
47
9
  const VERTEX_SNAP_RELATIVE = 1e-9;
48
- // Fallback absolute snap for degenerate all-coincident input where the extent is zero; no segments
49
- // survive such input, so the exact value only guards the division in `snap`.
10
+ /**
11
+ * Fallback absolute snap for degenerate all-coincident input where the extent is
12
+ * zero.
13
+ */
50
14
  const VERTEX_SNAP_FALLBACK = 1e-9;
51
- // Relative tolerance for the intersection orientation tests, scaled by squared edge lengths in each
52
- // comparison so it already behaves consistently across coordinate magnitudes.
15
+ /**
16
+ * Relative tolerance for the intersection orientation tests, scaled by squared
17
+ * edge lengths in each comparison.
18
+ */
53
19
  const INTERSECTION_EPS = 1e-12;
54
- let nextEventId = 0;
55
- // The active vertex-merge grid cell size for the boolean currently being computed, derived from the
56
- // input's coordinate extent at the top of `computeMartinezBoolean`. Module-scoped rather than threaded
57
- // through every `approxEqual`/`snap` call (the computation is synchronous and single-entrant, like
58
- // `nextEventId`); it is never read at import time, so importing the package stays side-effect-free.
59
- let vertexSnap = VERTEX_SNAP_FALLBACK;
60
- function computeMartinezBoolean(subject, clip, operation, fillRule) {
61
- nextEventId = 0;
62
- vertexSnap = computeVertexSnap(subject, clip);
63
- const segments = buildArrangement(subject, clip);
64
- if (segments.length === 0)
65
- return [];
66
- const unique = mergeCoincidentSegments(segments);
67
- const kept = classifySegments(unique, operation, fillRule);
68
- if (kept.length === 0)
69
- return [];
70
- return traceRings(kept);
71
- }
72
- // Scene2D 1: the sweep. Returns every final (non-crossing, non-overlapping) segment of the combined
73
- // subject+clip arrangement.
74
- function buildArrangement(subject, clip) {
75
- const heap = new EventHeap();
76
- fillQueue(subject, true, heap);
77
- fillQueue(clip, false, heap);
78
- const status = [];
79
- const segments = [];
80
- while (!heap.isEmpty()) {
81
- const event = heap.pop();
82
- if (event.left) {
83
- const index = insertStatus(status, event);
84
- const prev = index > 0 ? status[index - 1] : null;
85
- const next = index < status.length - 1 ? status[index + 1] : null;
86
- if (next !== null)
87
- possibleIntersection(event, next, heap);
88
- if (prev !== null)
89
- possibleIntersection(prev, event, heap);
20
+ /**
21
+ * The kernel the boolean operations use unless the caller passes another one. A stateless value, so
22
+ * it is one shared instance rather than a per-call construction (TS `martinezPathBooleanKernel`;
23
+ * entity-valued singleton as `LazyLock`, D12).
24
+ */
25
+ export const martinezPathBooleanKernel = createMartinezPathBooleanKernel();
26
+ function createEvent(events, x, y, isSubject) {
27
+ const id = events.length;
28
+ events.push({ x: x, y: y, left: false, otherEvent: undefined, isSubject: isSubject, id: id, windingDelta: 0 });
29
+ return id;
30
+ }
31
+ export function createMartinezPathBooleanKernel() {
32
+ let out = allocateEntity();
33
+ initializeMartinezPathBooleanKernel(out);
34
+ return out;
35
+ }
36
+ export function getPathBooleanFillRuleString(value) {
37
+ return (() => {
38
+ const matchValue = value;
39
+ switch (matchValue) {
40
+ case 'evenOdd':
41
+ return 'evenOdd';
42
+ case 'nonZero':
43
+ return 'nonZero';
44
+ case 'negative':
45
+ return 'negative';
46
+ case 'positive':
47
+ return 'positive';
48
+ }
49
+ })();
50
+ }
51
+ function addEdge(ax, ay, bx, by, isSubject, events, heap) {
52
+ const a = createEvent(events, ax, ay, isSubject);
53
+ const b = createEvent(events, bx, by, isSubject);
54
+ events[a].otherEvent = b;
55
+ events[b].otherEvent = a;
56
+ if (pointOrder(ax, ay, bx, by) < 0) {
57
+ events[a].left = true;
58
+ events[a].windingDelta = 1;
59
+ }
60
+ else {
61
+ events[b].left = true;
62
+ events[b].windingDelta = -1;
63
+ }
64
+ eventHeapPush(heap, events, a);
65
+ eventHeapPush(heap, events, b);
66
+ }
67
+ function approxEqual(a, b, vertexSnap) {
68
+ return Math.abs(a - b) <= vertexSnap;
69
+ }
70
+ function buildArrangement(subject, clip, vertexSnap) {
71
+ let events = [];
72
+ let heap = { data: [], length: 0 };
73
+ fillQueue(subject, true, events, heap, vertexSnap);
74
+ fillQueue(clip, false, events, heap, vertexSnap);
75
+ let status = [];
76
+ let segments = [];
77
+ while (!eventHeapIsEmpty(heap)) {
78
+ const event = eventHeapPop(heap, events);
79
+ if (events[event].left) {
80
+ const index = insertStatus(status, events, event);
81
+ const prev = index > 0 ? status[index - 1] : undefined;
82
+ const next = index < status.length - 1 ? status[index + 1] : undefined;
83
+ if (next !== undefined) {
84
+ const nextEv = next;
85
+ possibleIntersection(events, heap, vertexSnap, event, nextEv);
86
+ }
87
+ if (prev !== undefined) {
88
+ const prevEv = prev;
89
+ possibleIntersection(events, heap, vertexSnap, prevEv, event);
90
+ }
90
91
  }
91
92
  else {
92
- const leftEvent = getOtherEvent(event);
93
+ const leftEvent = getOtherEvent(events, event);
93
94
  const index = findStatus(status, leftEvent);
94
- if (index !== -1) {
95
- const prev = index > 0 ? status[index - 1] : null;
96
- const next = index < status.length - 1 ? status[index + 1] : null;
97
- status.splice(index, 1);
98
- if (prev !== null && next !== null)
99
- possibleIntersection(prev, next, heap);
95
+ if (index >= 0) {
96
+ const idx = index >>> 0;
97
+ const hasNeighbors = idx > 0 && idx + 1 < status.length;
98
+ const prevEv = idx > 0 ? status[idx - 1] : 0;
99
+ const nextEv = idx + 1 < status.length ? status[idx + 1] : 0;
100
+ let kept = [...status.slice(0, idx)];
101
+ {
102
+ const source = status.slice(idx + 1, status.length);
103
+ for (let index = 0, count = source.length; index < count; index++) {
104
+ kept.push(source[index]);
105
+ }
106
+ }
107
+ status = kept;
108
+ if (hasNeighbors) {
109
+ possibleIntersection(events, heap, vertexSnap, prevEv, nextEv);
110
+ }
100
111
  }
101
- recordSegment(leftEvent, event, segments);
112
+ recordSegment(events, leftEvent, event, segments, vertexSnap);
102
113
  }
103
114
  }
104
115
  return segments;
105
116
  }
106
- function recordSegment(leftEvent, rightEvent, segments) {
107
- if (!leftEvent.left)
108
- return;
109
- if (approxEqual(leftEvent.x, rightEvent.x) && approxEqual(leftEvent.y, rightEvent.y))
117
+ function classifySegments(unique, operation, fillRule) {
118
+ let kept = [];
119
+ for (const seg of unique) {
120
+ const mx = (seg.ax + seg.bx) / 2.0;
121
+ const my = (seg.ay + seg.by) / 2.0;
122
+ const dx = seg.bx - seg.ax;
123
+ const dy = seg.by - seg.ay;
124
+ const len = Math.sqrt(dx * dx + dy * dy);
125
+ if (len === 0.0) {
126
+ continue;
127
+ }
128
+ const eps = len * 1e-4;
129
+ const nlx = (-dy / len) * eps;
130
+ const nly = (dx / len) * eps;
131
+ const leftInSubject = isInside(windingAt(unique, mx + nlx, my + nly, true), fillRule);
132
+ const leftInClip = isInside(windingAt(unique, mx + nlx, my + nly, false), fillRule);
133
+ const rightInSubject = isInside(windingAt(unique, mx - nlx, my - nly, true), fillRule);
134
+ const rightInClip = isInside(windingAt(unique, mx - nlx, my - nly, false), fillRule);
135
+ const filledLeft = combine(operation, leftInSubject, leftInClip);
136
+ const filledRight = combine(operation, rightInSubject, rightInClip);
137
+ if (filledLeft === filledRight) {
138
+ continue;
139
+ }
140
+ if (filledLeft) {
141
+ kept.push(new Float64Array([seg.ax, seg.ay, seg.bx, seg.by]));
142
+ }
143
+ else {
144
+ kept.push(new Float64Array([seg.bx, seg.by, seg.ax, seg.ay]));
145
+ }
146
+ }
147
+ return kept;
148
+ }
149
+ function combine(operation, inSubject, inClip) {
150
+ return (() => {
151
+ const matchValue = operation;
152
+ switch (matchValue) {
153
+ case 'union':
154
+ return inSubject || inClip;
155
+ case 'intersection':
156
+ return inSubject && inClip;
157
+ case 'difference':
158
+ return inSubject && !inClip;
159
+ case 'xor':
160
+ return inSubject !== inClip;
161
+ }
162
+ })();
163
+ }
164
+ /**
165
+ * Priority order of two events.
166
+ */
167
+ function compareEvents(events, e1, e2) {
168
+ if (events[e1].x !== events[e2].x) {
169
+ return events[e1].x < events[e2].x ? -1 : 1;
170
+ }
171
+ if (events[e1].y !== events[e2].y) {
172
+ return events[e1].y < events[e2].y ? -1 : 1;
173
+ }
174
+ if (e1 === e2) {
175
+ return 0;
176
+ }
177
+ if (events[e1].left !== events[e2].left) {
178
+ return events[e1].left ? 1 : -1;
179
+ }
180
+ const e1Other = getOtherEvent(events, e1);
181
+ const e2Other = getOtherEvent(events, e2);
182
+ const area = signedArea(events[e1].x, events[e1].y, events[e1Other].x, events[e1Other].y, events[e2Other].x, events[e2Other].y);
183
+ if (area !== 0.0) {
184
+ return isBelow(events, e1, events[e2Other].x, events[e2Other].y) ? -1 : 1;
185
+ }
186
+ if (events[e1].isSubject !== events[e2].isSubject) {
187
+ return events[e1].isSubject ? -1 : 1;
188
+ }
189
+ return events[e1].id < events[e2].id ? -1 : 1;
190
+ }
191
+ /**
192
+ * Orders two active segments by vertical position at the sweep (Martinez segment
193
+ * comparator).
194
+ */
195
+ function compareSegments(events, le1, le2) {
196
+ if (le1 === le2) {
197
+ return 0;
198
+ }
199
+ const a1x = events[le1].x;
200
+ const a1y = events[le1].y;
201
+ const le2Other = getOtherEvent(events, le2);
202
+ const le1Other = getOtherEvent(events, le1);
203
+ const a2x = events[le1Other].x;
204
+ const a2y = events[le1Other].y;
205
+ const s1 = signedArea(a1x, a1y, a2x, a2y, events[le2].x, events[le2].y);
206
+ const s2 = signedArea(a1x, a1y, a2x, a2y, events[le2Other].x, events[le2Other].y);
207
+ if (s1 !== 0.0 || s2 !== 0.0) {
208
+ if (a1x === events[le2].x && a1y === events[le2].y) {
209
+ return isBelow(events, le1, events[le2Other].x, events[le2Other].y) ? -1 : 1;
210
+ }
211
+ if (a1x === events[le2].x) {
212
+ return a1y < events[le2].y ? -1 : 1;
213
+ }
214
+ if (compareEvents(events, le1, le2) === 1) {
215
+ return isBelow(events, le2, a1x, a1y) ? 1 : -1;
216
+ }
217
+ return isBelow(events, le1, events[le2].x, events[le2].y) ? -1 : 1;
218
+ }
219
+ if (events[le1].isSubject !== events[le2].isSubject) {
220
+ return events[le1].isSubject ? -1 : 1;
221
+ }
222
+ if (a1x === events[le2].x && a1y === events[le2].y) {
223
+ return events[le1].id < events[le2].id ? -1 : 1;
224
+ }
225
+ return compareEvents(events, le1, le2) === 1 ? 1 : -1;
226
+ }
227
+ function compareSnappedKeys(keys, stride, a, b) {
228
+ for (let offset = 0; offset < stride; offset += 1) {
229
+ const left = keys[a * stride + offset];
230
+ const right = keys[b * stride + offset];
231
+ if (left < right) {
232
+ return -1;
233
+ }
234
+ if (left > right) {
235
+ return 1;
236
+ }
237
+ }
238
+ return a < b ? -1 : a > b ? 1 : 0;
239
+ }
240
+ function computeMartinezBoolean(subject, clip, operation, fillRule) {
241
+ const vertexSnap = computeVertexSnap(subject, clip);
242
+ const segments = buildArrangement(subject, clip, vertexSnap);
243
+ if (segments.length === 0) {
244
+ return [];
245
+ }
246
+ const unique = mergeCoincidentSegments(segments, vertexSnap);
247
+ const kept = classifySegments(unique, operation, fillRule);
248
+ if (kept.length === 0) {
249
+ return [];
250
+ }
251
+ return traceRings(kept, vertexSnap);
252
+ }
253
+ function computeVertexSnap(subject, clip) {
254
+ let minX = Number.POSITIVE_INFINITY;
255
+ let minY = Number.POSITIVE_INFINITY;
256
+ let maxX = Number.NEGATIVE_INFINITY;
257
+ let maxY = Number.NEGATIVE_INFINITY;
258
+ for (const contours of [subject, clip]) {
259
+ for (const contour of contours) {
260
+ let i = 0;
261
+ while (i < contour.length) {
262
+ const x = contour[i];
263
+ const y = contour[i + 1];
264
+ if (x < minX) {
265
+ minX = x;
266
+ }
267
+ if (x > maxX) {
268
+ maxX = x;
269
+ }
270
+ if (y < minY) {
271
+ minY = y;
272
+ }
273
+ if (y > maxY) {
274
+ maxY = y;
275
+ }
276
+ i += 2;
277
+ }
278
+ }
279
+ }
280
+ const extent = Math.max(maxX - minX, maxY - minY);
281
+ return extent > 0.0 ? extent * VERTEX_SNAP_RELATIVE : VERTEX_SNAP_FALLBACK;
282
+ }
283
+ /**
284
+ * `from` and `to` are snapped-vertex ids in first-occurrence order (see `firstOccurrenceIds`).
285
+ */
286
+ function directedGraphAddEdge(graph, from, to, edge) {
287
+ from = directedGraphVertex(graph, from, edge[0], edge[1]);
288
+ to = directedGraphVertex(graph, to, edge[2], edge[3]);
289
+ if (from === to) {
110
290
  return;
111
- segments.push({
112
- ax: leftEvent.x,
113
- ay: leftEvent.y,
114
- bx: rightEvent.x,
115
- by: rightEvent.y,
116
- isSubject: leftEvent.isSubject,
117
- windingDelta: leftEvent.windingDelta,
118
- });
291
+ }
292
+ const edgeIndex = graph.edgeFrom.length;
293
+ graph.edgeFrom.push(from);
294
+ graph.edgeTo.push(to);
295
+ graph.used.push(false);
296
+ graph.outgoing[from].push(edgeIndex);
297
+ }
298
+ function directedGraphNew() {
299
+ return { xs: [], ys: [], outgoing: [], edgeTo: [], edgeFrom: [], used: [] };
300
+ }
301
+ /**
302
+ * At vertex `at`, arriving from `cameFrom`, pick the unused outgoing edge that
303
+ * turns most clockwise from the reversed incoming direction.
304
+ */
305
+ function directedGraphNextEdge(graph, at, cameFrom) {
306
+ const incomingAngle = Math.atan2(graph.ys[cameFrom] - graph.ys[at], graph.xs[cameFrom] - graph.xs[at]);
307
+ let best = -1;
308
+ let bestGap = Number.POSITIVE_INFINITY;
309
+ const twoPi = Math.PI * 2.0;
310
+ for (const edge of graph.outgoing[at]) {
311
+ if (graph.used[edge]) {
312
+ continue;
313
+ }
314
+ const to = graph.edgeTo[edge];
315
+ const angle = Math.atan2(graph.ys[to] - graph.ys[at], graph.xs[to] - graph.xs[at]);
316
+ let gap = incomingAngle - angle;
317
+ while (gap <= 0.0) {
318
+ gap += twoPi;
319
+ }
320
+ while (gap > twoPi) {
321
+ gap -= twoPi;
322
+ }
323
+ if (gap < bestGap) {
324
+ bestGap = gap;
325
+ best = edge | 0;
326
+ }
327
+ }
328
+ return best;
329
+ }
330
+ function directedGraphTraceRings(graph) {
331
+ let rings = [];
332
+ for (let start = 0; start < graph.edgeFrom.length; start += 1) {
333
+ if (graph.used[start]) {
334
+ continue;
335
+ }
336
+ const ring = directedGraphWalk(graph, start);
337
+ if (ring !== undefined) {
338
+ const r = ring;
339
+ if (r.length >= 6) {
340
+ rings.push(r);
341
+ }
342
+ }
343
+ }
344
+ return rings;
119
345
  }
120
- // Appends every edge of every contour as a left/right event pair. A trailing vertex repeating the first
121
- // (flattenPath appends one on CLOSE) is dropped; zero-length edges are skipped.
122
- function fillQueue(contours, isSubject, heap) {
346
+ function directedGraphVertex(graph, id, x, y) {
347
+ if (id === graph.xs.length) {
348
+ graph.xs.push(x);
349
+ graph.ys.push(y);
350
+ graph.outgoing.push([]);
351
+ }
352
+ return id;
353
+ }
354
+ function directedGraphWalk(graph, startEdge) {
355
+ let ring = [];
356
+ let edge = startEdge;
357
+ let guard = 0;
358
+ const limit = graph.edgeFrom.length + 2;
359
+ while (guard < limit) {
360
+ guard += 1;
361
+ graph.used[edge] = true;
362
+ const from = graph.edgeFrom[edge];
363
+ ring.push(graph.xs[from]);
364
+ ring.push(graph.ys[from]);
365
+ const to = graph.edgeTo[edge];
366
+ const next = directedGraphNextEdge(graph, to, from);
367
+ if (next < 0 || next >>> 0 === startEdge || graph.used[next >>> 0]) {
368
+ break;
369
+ }
370
+ edge = next >>> 0;
371
+ }
372
+ return ring;
373
+ }
374
+ function divideIfInterior(events, heap, vertexSnap, le, x, y) {
375
+ const other = getOtherEvent(events, le);
376
+ const leX = events[le].x;
377
+ const leY = events[le].y;
378
+ const otherX = events[other].x;
379
+ const otherY = events[other].y;
380
+ if (pointStrictlyInside(leX, leY, otherX, otherY, x, y, vertexSnap)) {
381
+ divideSegment(events, heap, le, x, y);
382
+ }
383
+ }
384
+ /**
385
+ * Splits `le` at interior point (x, y).
386
+ */
387
+ function divideSegment(events, heap, le, x, y) {
388
+ const isSubject = events[le].isSubject;
389
+ const windingDelta = events[le].windingDelta;
390
+ const right = getOtherEvent(events, le);
391
+ const newRight = createEvent(events, x, y, isSubject);
392
+ const newLeft = createEvent(events, x, y, isSubject);
393
+ events[newRight].left = false;
394
+ events[newRight].otherEvent = le;
395
+ events[newLeft].left = true;
396
+ events[newLeft].otherEvent = right;
397
+ events[newLeft].windingDelta = windingDelta;
398
+ events[le].otherEvent = newRight;
399
+ events[right].otherEvent = newLeft;
400
+ eventHeapPush(heap, events, newLeft);
401
+ eventHeapPush(heap, events, newRight);
402
+ }
403
+ function eventHeapIsEmpty(heap) {
404
+ return heap.length === 0;
405
+ }
406
+ function eventHeapPop(heap, events) {
407
+ const top = heap.data[0];
408
+ heap.length -= 1;
409
+ const last = heap.data[heap.length];
410
+ if (heap.length > 0) {
411
+ heap.data[0] = last;
412
+ let i = 0;
413
+ const n = heap.length;
414
+ for (;;) {
415
+ const left = i * 2 + 1;
416
+ const right = left + 1;
417
+ let smallest = i;
418
+ if (left < n && compareEvents(events, heap.data[left], heap.data[smallest]) < 0) {
419
+ smallest = left;
420
+ }
421
+ if (right < n && compareEvents(events, heap.data[right], heap.data[smallest]) < 0) {
422
+ smallest = right;
423
+ }
424
+ if (smallest === i) {
425
+ break;
426
+ }
427
+ [heap.data[i], heap.data[smallest]] = [heap.data[smallest], heap.data[i]];
428
+ i = smallest;
429
+ }
430
+ }
431
+ return top;
432
+ }
433
+ function eventHeapPush(heap, events, event) {
434
+ if (heap.length < heap.data.length) {
435
+ heap.data[heap.length] = event;
436
+ }
437
+ else {
438
+ heap.data.push(event);
439
+ }
440
+ heap.length += 1;
441
+ let i = heap.length - 1;
442
+ while (i > 0) {
443
+ const parent = (i - 1) >> 1;
444
+ if (compareEvents(events, heap.data[i], heap.data[parent]) < 0) {
445
+ [heap.data[i], heap.data[parent]] = [heap.data[parent], heap.data[i]];
446
+ i = parent;
447
+ }
448
+ else {
449
+ break;
450
+ }
451
+ }
452
+ }
453
+ /**
454
+ * Appends every edge of every contour as a left/right event pair.
455
+ */
456
+ function fillQueue(contours, isSubject, events, heap, vertexSnap) {
123
457
  for (const contour of contours) {
124
458
  let count = contour.length >> 1;
125
- if (count < 2)
459
+ if (count < 2) {
126
460
  continue;
127
- if (approxEqual(contour[0], contour[(count - 1) * 2]) && approxEqual(contour[1], contour[(count - 1) * 2 + 1])) {
461
+ }
462
+ if (approxEqual(contour[0], contour[(count - 1) * 2], vertexSnap) &&
463
+ approxEqual(contour[1], contour[(count - 1) * 2 + 1], vertexSnap)) {
128
464
  count -= 1;
129
465
  }
130
- if (count < 3)
466
+ if (count < 3) {
131
467
  continue;
132
- for (let i = 0; i < count; i++) {
468
+ }
469
+ for (let i = 0; i < count; i += 1) {
133
470
  const ax = contour[i * 2];
134
471
  const ay = contour[i * 2 + 1];
135
472
  const j = (i + 1) % count;
136
473
  const bx = contour[j * 2];
137
474
  const by = contour[j * 2 + 1];
138
- if (approxEqual(ax, bx) && approxEqual(ay, by))
475
+ if (approxEqual(ax, bx, vertexSnap) && approxEqual(ay, by, vertexSnap)) {
139
476
  continue;
140
- addEdge(ax, ay, bx, by, isSubject, heap);
477
+ }
478
+ addEdge(ax, ay, bx, by, isSubject, events, heap);
141
479
  }
142
480
  }
143
481
  }
144
- function addEdge(ax, ay, bx, by, isSubject, heap) {
145
- const a = createEvent(ax, ay, isSubject);
146
- const b = createEvent(bx, by, isSubject);
147
- a.otherEvent = b;
148
- b.otherEvent = a;
149
- if (pointOrder(ax, ay, bx, by) < 0) {
150
- a.left = true;
151
- a.windingDelta = 1;
482
+ function findStatus(status, event) {
483
+ for (let i = 0; i < status.length; i += 1) {
484
+ if (status[i] === event) {
485
+ return i | 0;
486
+ }
152
487
  }
153
- else {
154
- b.left = true;
155
- b.windingDelta = -1;
156
- }
157
- heap.push(a);
158
- heap.push(b);
159
- }
160
- function createEvent(x, y, isSubject) {
161
- return {
162
- x,
163
- y,
164
- left: false,
165
- otherEvent: null,
166
- isSubject,
167
- id: nextEventId++,
168
- windingDelta: 0,
169
- };
170
- }
171
- // Reshapes the arrangement so two status-neighbor segments only ever meet at shared endpoints.
172
- function possibleIntersection(le1, le2, heap) {
173
- const le1Other = getOtherEvent(le1);
174
- const le2Other = getOtherEvent(le2);
175
- const inter = segmentIntersection(le1.x, le1.y, le1Other.x, le1Other.y, le2.x, le2.y, le2Other.x, le2Other.y);
176
- if (inter === null)
177
- return;
178
- if (inter.length === 1) {
179
- const ix = inter[0][0];
180
- const iy = inter[0][1];
181
- if (!pointOnEndpoint(le1, ix, iy))
182
- divideSegment(le1, ix, iy, heap);
183
- if (!pointOnEndpoint(le2, ix, iy))
184
- divideSegment(le2, ix, iy, heap);
185
- return;
488
+ return -1;
489
+ }
490
+ function firstOccurrenceIds(keys, stride) {
491
+ const count = Math.trunc(keys.length / stride);
492
+ let order = [];
493
+ for (let position = 0; position < count; position += 1) {
494
+ order.push(position);
186
495
  }
187
- // Collinear overlap: split each segment at the other's strictly-interior endpoints. Remaining overlap
188
- // on the freshly-created halves is resolved when those halves become status neighbors and are tested.
189
- divideIfInterior(le1, le2.x, le2.y, heap);
190
- divideIfInterior(le1, le2Other.x, le2Other.y, heap);
191
- divideIfInterior(le2, le1.x, le1.y, heap);
192
- divideIfInterior(le2, le1Other.x, le1Other.y, heap);
193
- }
194
- function divideIfInterior(le, x, y, heap) {
195
- const other = getOtherEvent(le);
196
- if (pointStrictlyInside(le.x, le.y, other.x, other.y, x, y))
197
- divideSegment(le, x, y, heap);
198
- }
199
- // Splits `le` at interior point (x, y): the left half keeps `le` as its left endpoint; a fresh left
200
- // event opens the right half. Both halves inherit the ring orientation, so their winding deltas match.
201
- function divideSegment(le, x, y, heap) {
202
- const right = getOtherEvent(le);
203
- const newRight = createEvent(x, y, le.isSubject);
204
- const newLeft = createEvent(x, y, le.isSubject);
205
- newRight.left = false;
206
- newRight.otherEvent = le;
207
- newLeft.left = true;
208
- newLeft.otherEvent = right;
209
- newLeft.windingDelta = le.windingDelta;
210
- le.otherEvent = newRight;
211
- right.otherEvent = newLeft;
212
- heap.push(newLeft);
213
- heap.push(newRight);
214
- }
215
- function pointOnEndpoint(le, x, y) {
216
- const other = getOtherEvent(le);
217
- return (approxEqual(le.x, x) && approxEqual(le.y, y)) || (approxEqual(other.x, x) && approxEqual(other.y, y));
218
- }
219
- function pointStrictlyInside(lx, ly, rx, ry, x, y) {
220
- if ((approxEqual(lx, x) && approxEqual(ly, y)) || (approxEqual(rx, x) && approxEqual(ry, y)))
221
- return false;
222
- const cross = (rx - lx) * (y - ly) - (ry - ly) * (x - lx);
223
- const lenSq = (rx - lx) * (rx - lx) + (ry - ly) * (ry - ly);
224
- if (cross * cross > INTERSECTION_EPS * lenSq * lenSq)
225
- return false;
226
- const dot = (x - lx) * (rx - lx) + (y - ly) * (ry - ly);
227
- return dot > 0 && dot < lenSq;
496
+ order.sort((a, b) => compareSnappedKeys(keys, stride, a, b));
497
+ let first = new Array(count).fill(0);
498
+ let groupStart = 0;
499
+ for (let rank = 0; rank < count; rank += 1) {
500
+ if (!snappedKeysEqual(keys, stride, order[groupStart], order[rank])) {
501
+ groupStart = rank;
502
+ }
503
+ first[order[rank]] = order[groupStart];
504
+ }
505
+ let ids = new Array(count).fill(0);
506
+ let next = 0;
507
+ for (let position = 0; position < count; position += 1) {
508
+ if (first[position] === position) {
509
+ ids[position] = next;
510
+ next += 1;
511
+ }
512
+ else {
513
+ ids[position] = ids[first[position]];
514
+ }
515
+ }
516
+ return ids;
517
+ }
518
+ function getOtherEvent(events, event) {
519
+ return (() => {
520
+ const matchValue = events[event].otherEvent;
521
+ if (matchValue !== undefined) {
522
+ const other = matchValue;
523
+ return other;
524
+ }
525
+ else {
526
+ return (() => {
527
+ throw new Error('Sweep event entered the Martinez queue before its endpoint pair was linked');
528
+ })();
529
+ }
530
+ })();
531
+ }
532
+ export function getPathBooleanOperationString(value) {
533
+ return (() => {
534
+ const matchValue = value;
535
+ switch (matchValue) {
536
+ case 'difference':
537
+ return 'difference';
538
+ case 'intersection':
539
+ return 'intersection';
540
+ case 'union':
541
+ return 'union';
542
+ case 'xor':
543
+ return 'xor';
544
+ }
545
+ })();
546
+ }
547
+ /**
548
+ * The construction initializer of `createMartinezPathBooleanKernel`.
549
+ */
550
+ export function initializeMartinezPathBooleanKernel(out) {
551
+ out.computePathBoolean = computeMartinezBoolean;
228
552
  }
229
- // Sweep-line status: a sorted array. Because segments are subdivided at every crossing they never swap
230
- // order once inserted, so a plain insertion order stays valid.
231
- function insertStatus(status, event) {
553
+ function insertStatus(status, events, event) {
232
554
  let lo = 0;
233
555
  let hi = status.length;
234
556
  while (lo < hi) {
235
557
  const mid = (lo + hi) >> 1;
236
- if (compareSegments(status[mid], event) < 0)
558
+ if (compareSegments(events, status[mid], event) < 0) {
237
559
  lo = mid + 1;
238
- else
560
+ }
561
+ else {
239
562
  hi = mid;
563
+ }
564
+ }
565
+ status.push(event);
566
+ let shift = status.length - 1;
567
+ while (shift > lo) {
568
+ status[shift] = status[shift - 1];
569
+ shift -= 1;
240
570
  }
241
- status.splice(lo, 0, event);
571
+ status[lo] = event;
242
572
  return lo;
243
573
  }
244
- function findStatus(status, event) {
245
- for (let i = 0; i < status.length; i++)
246
- if (status[i] === event)
247
- return i;
248
- return -1;
574
+ /**
575
+ * True when segment `le` passes strictly below point (x, y).
576
+ */
577
+ function isBelow(events, le, x, y) {
578
+ const other = getOtherEvent(events, le);
579
+ return events[le].left
580
+ ? signedArea(events[le].x, events[le].y, events[other].x, events[other].y, x, y) > 0.0
581
+ : signedArea(events[other].x, events[other].y, events[le].x, events[le].y, x, y) > 0.0;
249
582
  }
250
- // Orders two active segments by vertical position at the sweep (Martinez segment comparator). Uses
251
- // exact comparisons throughout so the order is a consistent total order — a non-transitive tolerant
252
- // comparator would corrupt the sorted status.
253
- function compareSegments(le1, le2) {
254
- if (le1 === le2)
255
- return 0;
256
- const a1x = le1.x;
257
- const a1y = le1.y;
258
- const le2Other = getOtherEvent(le2);
259
- const le1Other = getOtherEvent(le1);
260
- const a2x = le1Other.x;
261
- const a2y = le1Other.y;
262
- const s1 = signedArea(a1x, a1y, a2x, a2y, le2.x, le2.y);
263
- const s2 = signedArea(a1x, a1y, a2x, a2y, le2Other.x, le2Other.y);
264
- if (s1 !== 0 || s2 !== 0) {
265
- if (a1x === le2.x && a1y === le2.y)
266
- return isBelow(le1, le2Other.x, le2Other.y) ? -1 : 1;
267
- if (a1x === le2.x)
268
- return a1y < le2.y ? -1 : 1;
269
- if (compareEvents(le1, le2) === 1)
270
- return isBelow(le2, a1x, a1y) ? 1 : -1;
271
- return isBelow(le1, le2.x, le2.y) ? -1 : 1;
272
- }
273
- if (le1.isSubject !== le2.isSubject)
274
- return le1.isSubject ? -1 : 1;
275
- if (a1x === le2.x && a1y === le2.y)
276
- return le1.id < le2.id ? -1 : 1;
277
- return compareEvents(le1, le2) === 1 ? 1 : -1;
278
- }
279
- // True when segment `le` passes strictly below point (x, y).
280
- function isBelow(le, x, y) {
281
- const other = getOtherEvent(le);
282
- return le.left
283
- ? signedArea(le.x, le.y, other.x, other.y, x, y) > 0
284
- : signedArea(other.x, other.y, le.x, le.y, x, y) > 0;
285
- }
286
- // Priority order of two events: by x, then y, then right-endpoint-before-left, then by slope, then a
287
- // stable operand/id tiebreak. Returns -1 when e1 is processed first. Exact comparisons only.
288
- function compareEvents(e1, e2) {
289
- if (e1.x !== e2.x)
290
- return e1.x < e2.x ? -1 : 1;
291
- if (e1.y !== e2.y)
292
- return e1.y < e2.y ? -1 : 1;
293
- if (e1 === e2)
294
- return 0;
295
- if (e1.left !== e2.left)
296
- return e1.left ? 1 : -1;
297
- const e1Other = getOtherEvent(e1);
298
- const e2Other = getOtherEvent(e2);
299
- const area = signedArea(e1.x, e1.y, e1Other.x, e1Other.y, e2Other.x, e2Other.y);
300
- if (area !== 0)
301
- return isBelow(e1, e2Other.x, e2Other.y) ? -1 : 1;
302
- if (e1.isSubject !== e2.isSubject)
303
- return e1.isSubject ? -1 : 1;
304
- return e1.id < e2.id ? -1 : 1;
305
- }
306
- function getOtherEvent(event) {
307
- if (event.otherEvent === null) {
308
- throw new Error('Sweep event entered the Martinez queue before its endpoint pair was linked');
309
- }
310
- return event.otherEvent;
583
+ function isInside(winding, fillRule) {
584
+ return (() => {
585
+ const matchValue = fillRule;
586
+ switch (matchValue) {
587
+ case 'evenOdd':
588
+ return (Math.abs(winding) & 1) === 1;
589
+ case 'positive':
590
+ return winding > 0;
591
+ case 'negative':
592
+ return winding < 0;
593
+ case 'nonZero':
594
+ return winding !== 0;
595
+ }
596
+ })();
597
+ }
598
+ function mergeCoincidentSegments(segments, vertexSnap) {
599
+ let oriented = [];
600
+ let keys = [];
601
+ for (const seg of segments) {
602
+ let ax = seg.ax;
603
+ let ay = seg.ay;
604
+ let bx = seg.bx;
605
+ let by = seg.by;
606
+ if (pointOrder(ax, ay, bx, by) > 0) {
607
+ ax = seg.bx;
608
+ ay = seg.by;
609
+ bx = seg.ax;
610
+ by = seg.ay;
611
+ }
612
+ oriented.push(ax);
613
+ oriented.push(ay);
614
+ oriented.push(bx);
615
+ oriented.push(by);
616
+ keys.push(snap(ax, vertexSnap));
617
+ keys.push(snap(ay, vertexSnap));
618
+ keys.push(snap(bx, vertexSnap));
619
+ keys.push(snap(by, vertexSnap));
620
+ }
621
+ const ids = firstOccurrenceIds(keys, 4);
622
+ let unique = [];
623
+ for (let index = 0; index < segments.length; index += 1) {
624
+ const seg = segments[index];
625
+ const idx = ids[index];
626
+ if (idx < unique.length) {
627
+ if (seg.isSubject) {
628
+ unique[idx].subjectDelta += seg.windingDelta;
629
+ }
630
+ else {
631
+ unique[idx].clipDelta += seg.windingDelta;
632
+ }
633
+ }
634
+ else {
635
+ const ax = oriented[index * 4];
636
+ const ay = oriented[index * 4 + 1];
637
+ const bx = oriented[index * 4 + 2];
638
+ const by = oriented[index * 4 + 3];
639
+ unique.push({
640
+ ax: ax,
641
+ ay: ay,
642
+ bx: bx,
643
+ by: by,
644
+ subjectDelta: seg.isSubject ? seg.windingDelta : 0,
645
+ clipDelta: seg.isSubject ? 0 : seg.windingDelta,
646
+ });
647
+ }
648
+ }
649
+ return unique;
650
+ }
651
+ export function parsePathBooleanFillRule(value) {
652
+ return (() => {
653
+ const matchValue = value;
654
+ switch (matchValue) {
655
+ case 'evenOdd':
656
+ return 'evenOdd';
657
+ case 'nonZero':
658
+ return 'nonZero';
659
+ case 'negative':
660
+ return 'negative';
661
+ case 'positive':
662
+ return 'positive';
663
+ default:
664
+ return undefined;
665
+ }
666
+ })();
667
+ }
668
+ export function parsePathBooleanOperation(value) {
669
+ return (() => {
670
+ const matchValue = value;
671
+ switch (matchValue) {
672
+ case 'difference':
673
+ return 'difference';
674
+ case 'intersection':
675
+ return 'intersection';
676
+ case 'union':
677
+ return 'union';
678
+ case 'xor':
679
+ return 'xor';
680
+ default:
681
+ return undefined;
682
+ }
683
+ })();
684
+ }
685
+ function pointOnEndpointCoords(leX, leY, otherX, otherY, x, y, vertexSnap) {
686
+ return ((approxEqual(leX, x, vertexSnap) && approxEqual(leY, y, vertexSnap)) ||
687
+ (approxEqual(otherX, x, vertexSnap) && approxEqual(otherY, y, vertexSnap)));
311
688
  }
312
689
  function pointOrder(ax, ay, bx, by) {
313
- if (ax < bx)
690
+ if (ax < bx) {
314
691
  return -1;
315
- if (ax > bx)
692
+ }
693
+ if (ax > bx) {
316
694
  return 1;
317
- if (ay < by)
695
+ }
696
+ if (ay < by) {
318
697
  return -1;
319
- if (ay > by)
698
+ }
699
+ if (ay > by) {
320
700
  return 1;
701
+ }
321
702
  return 0;
322
703
  }
323
- // > 0 when (p0, p1, p2) turn counterclockwise in math (y-up) orientation.
324
- function signedArea(p0x, p0y, p1x, p1y, p2x, p2y) {
325
- return (p0x - p2x) * (p1y - p2y) - (p1x - p2x) * (p0y - p2y);
704
+ function pointStrictlyInside(lx, ly, rx, ry, x, y, vertexSnap) {
705
+ if ((approxEqual(lx, x, vertexSnap) && approxEqual(ly, y, vertexSnap)) ||
706
+ (approxEqual(rx, x, vertexSnap) && approxEqual(ry, y, vertexSnap))) {
707
+ return false;
708
+ }
709
+ const cross = (rx - lx) * (y - ly) - (ry - ly) * (x - lx);
710
+ const lenSq = (rx - lx) * (rx - lx) + (ry - ly) * (ry - ly);
711
+ if (cross * cross > INTERSECTION_EPS * lenSq * lenSq) {
712
+ return false;
713
+ }
714
+ const dot = (x - lx) * (rx - lx) + (y - ly) * (ry - ly);
715
+ return dot > 0.0 && dot < lenSq;
716
+ }
717
+ /**
718
+ * Reshapes the arrangement so two status-neighbor segments only ever meet at
719
+ * shared endpoints.
720
+ */
721
+ function possibleIntersection(events, heap, vertexSnap, le1, le2) {
722
+ const le1Other = getOtherEvent(events, le1);
723
+ const le2Other = getOtherEvent(events, le2);
724
+ const le1X = events[le1].x;
725
+ const le1Y = events[le1].y;
726
+ const le1OtherX = events[le1Other].x;
727
+ const le1OtherY = events[le1Other].y;
728
+ const le2X = events[le2].x;
729
+ const le2Y = events[le2].y;
730
+ const le2OtherX = events[le2Other].x;
731
+ const le2OtherY = events[le2Other].y;
732
+ let inter = segmentIntersection(le1X, le1Y, le1OtherX, le1OtherY, le2X, le2Y, le2OtherX, le2OtherY);
733
+ if (inter === undefined) {
734
+ return;
735
+ }
736
+ inter = inter;
737
+ if (inter.length === 1) {
738
+ const ix = inter[0][0];
739
+ const iy = inter[0][1];
740
+ const on1 = pointOnEndpointCoords(le1X, le1Y, le1OtherX, le1OtherY, ix, iy, vertexSnap);
741
+ if (!on1) {
742
+ divideSegment(events, heap, le1, ix, iy);
743
+ }
744
+ const on2 = pointOnEndpointCoords(le2X, le2Y, le2OtherX, le2OtherY, ix, iy, vertexSnap);
745
+ if (!on2) {
746
+ divideSegment(events, heap, le2, ix, iy);
747
+ }
748
+ return;
749
+ }
750
+ divideIfInterior(events, heap, vertexSnap, le1, le2X, le2Y);
751
+ divideIfInterior(events, heap, vertexSnap, le1, le2OtherX, le2OtherY);
752
+ divideIfInterior(events, heap, vertexSnap, le2, le1X, le1Y);
753
+ divideIfInterior(events, heap, vertexSnap, le2, le1OtherX, le1OtherY);
754
+ }
755
+ function recordSegment(events, leftEvent, rightEvent, segments, vertexSnap) {
756
+ if (!events[leftEvent].left) {
757
+ return;
758
+ }
759
+ if (approxEqual(events[leftEvent].x, events[rightEvent].x, vertexSnap) &&
760
+ approxEqual(events[leftEvent].y, events[rightEvent].y, vertexSnap)) {
761
+ return;
762
+ }
763
+ segments.push({
764
+ ax: events[leftEvent].x,
765
+ ay: events[leftEvent].y,
766
+ bx: events[rightEvent].x,
767
+ by: events[rightEvent].y,
768
+ isSubject: events[leftEvent].isSubject,
769
+ windingDelta: events[leftEvent].windingDelta,
770
+ });
326
771
  }
327
- // Returns null, a single intersection point, or the two endpoints of a collinear overlap.
772
+ /**
773
+ * Returns None, a single intersection point, or the two endpoints of a collinear
774
+ * overlap.
775
+ */
328
776
  function segmentIntersection(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y) {
329
777
  const vax = a2x - a1x;
330
778
  const vay = a2y - a1y;
@@ -338,313 +786,83 @@ function segmentIntersection(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y) {
338
786
  const sqrLenB = vbx * vbx + vby * vby;
339
787
  if (sqrKross > INTERSECTION_EPS * sqrLenA * sqrLenB) {
340
788
  const s = (ex * vby - ey * vbx) / kross;
341
- if (s < 0 || s > 1)
342
- return null;
789
+ if (s < 0.0 || s > 1.0) {
790
+ return undefined;
791
+ }
343
792
  const t = (ex * vay - ey * vax) / kross;
344
- if (t < 0 || t > 1)
345
- return null;
793
+ if (t < 0.0 || t > 1.0) {
794
+ return undefined;
795
+ }
346
796
  return [[a1x + s * vax, a1y + s * vay]];
347
797
  }
348
798
  const krossE = ex * vay - ey * vax;
349
- if (krossE * krossE > INTERSECTION_EPS * sqrLenA * (ex * ex + ey * ey))
350
- return null;
799
+ if (krossE * krossE > INTERSECTION_EPS * sqrLenA * (ex * ex + ey * ey)) {
800
+ return undefined;
801
+ }
351
802
  const sa = (ex * vax + ey * vay) / sqrLenA;
352
803
  const sb = sa + (vbx * vax + vby * vay) / sqrLenA;
353
- const lo = Math.max(0, Math.min(sa, sb));
354
- const hi = Math.min(1, Math.max(sa, sb));
355
- if (lo > hi + 1e-12)
356
- return null;
357
- if (Math.abs(lo - hi) <= 1e-12)
804
+ const lo = Math.max(0.0, Math.min(sa, sb));
805
+ const hi = Math.min(1.0, Math.max(sa, sb));
806
+ if (lo > hi + 1e-12) {
807
+ return undefined;
808
+ }
809
+ if (Math.abs(lo - hi) <= 1e-12) {
358
810
  return [[a1x + lo * vax, a1y + lo * vay]];
811
+ }
359
812
  return [
360
813
  [a1x + lo * vax, a1y + lo * vay],
361
814
  [a1x + hi * vax, a1y + hi * vay],
362
815
  ];
363
816
  }
364
- // Scene2D 2a: fold coincident final segments into one, summing each operand's winding contribution. The
365
- // key normalizes endpoints to lexicographic order, matching the way `addEdge` measures windingDelta, so
366
- // deltas from opposite-direction coincident edges cancel.
367
- function mergeCoincidentSegments(segments) {
368
- const map = new Map();
369
- for (const seg of segments) {
370
- let ax = seg.ax;
371
- let ay = seg.ay;
372
- let bx = seg.bx;
373
- let by = seg.by;
374
- if (pointOrder(ax, ay, bx, by) > 0) {
375
- ax = seg.bx;
376
- ay = seg.by;
377
- bx = seg.ax;
378
- by = seg.ay;
817
+ /**
818
+ * > 0 when (p0, p1, p2) turn counterclockwise.
819
+ */
820
+ function signedArea(p0x, p0y, p1x, p1y, p2x, p2y) {
821
+ return (p0x - p2x) * (p1y - p2y) - (p1x - p2x) * (p0y - p2y);
822
+ }
823
+ function snap(v, vertexSnap) {
824
+ return Math.round(v / vertexSnap);
825
+ }
826
+ function snappedKeysEqual(keys, stride, a, b) {
827
+ for (let offset = 0; offset < stride; offset += 1) {
828
+ if (keys[a * stride + offset] !== keys[b * stride + offset]) {
829
+ return false;
379
830
  }
380
- const key = `${snap(ax)},${snap(ay)},${snap(bx)},${snap(by)}`;
381
- let unique = map.get(key);
382
- if (unique === undefined) {
383
- unique = { ax, ay, bx, by, subjectDelta: 0, clipDelta: 0 };
384
- map.set(key, unique);
385
- }
386
- if (seg.isSubject)
387
- unique.subjectDelta += seg.windingDelta;
388
- else
389
- unique.clipDelta += seg.windingDelta;
390
- }
391
- return [...map.values()];
392
- }
393
- // Scene2D 2b: classify each unique segment by the boundary test, orienting kept segments so the retained
394
- // region sits on the segment's left. Returns directed edges [fromX, fromY, toX, toY].
395
- //
396
- // Membership is sampled at two points a hair off each side of the segment's midpoint, perpendicular to
397
- // it, and the winding of each operand at those points is found by a downward ray. Sampling
398
- // perpendicular to the edge (rather than straight up/down) is what makes vertical and horizontal edges
399
- // classify correctly — a purely y-based below/above test is degenerate for vertical edges.
400
- function classifySegments(unique, operation, fillRule) {
401
- const kept = [];
402
- for (const seg of unique) {
403
- const mx = (seg.ax + seg.bx) / 2;
404
- const my = (seg.ay + seg.by) / 2;
405
- const dx = seg.bx - seg.ax;
406
- const dy = seg.by - seg.ay;
407
- const len = Math.hypot(dx, dy);
408
- if (len === 0)
409
- continue;
410
- // Left normal of the lex-ordered direction A→B (math y-up: rotate +90°). Offset well inside the
411
- // segment's own clear zone — other segments only meet it at its endpoints.
412
- const eps = len * 1e-4;
413
- const nlx = (-dy / len) * eps;
414
- const nly = (dx / len) * eps;
415
- const leftInSubject = isInside(windingAt(unique, mx + nlx, my + nly, true), fillRule);
416
- const leftInClip = isInside(windingAt(unique, mx + nlx, my + nly, false), fillRule);
417
- const rightInSubject = isInside(windingAt(unique, mx - nlx, my - nly, true), fillRule);
418
- const rightInClip = isInside(windingAt(unique, mx - nlx, my - nly, false), fillRule);
419
- const filledLeft = combine(operation, leftInSubject, leftInClip);
420
- const filledRight = combine(operation, rightInSubject, rightInClip);
421
- if (filledLeft === filledRight)
422
- continue;
423
- // Direct the edge so the kept region is on its left.
424
- if (filledLeft)
425
- kept.push([seg.ax, seg.ay, seg.bx, seg.by]);
426
- else
427
- kept.push([seg.bx, seg.by, seg.ax, seg.ay]);
428
831
  }
429
- return kept;
832
+ return true;
833
+ }
834
+ function traceRings(edges, vertexSnap) {
835
+ let keys = [];
836
+ for (const e of edges) {
837
+ keys.push(snap(e[0], vertexSnap));
838
+ keys.push(snap(e[1], vertexSnap));
839
+ keys.push(snap(e[2], vertexSnap));
840
+ keys.push(snap(e[3], vertexSnap));
841
+ }
842
+ const ids = firstOccurrenceIds(keys, 2);
843
+ let graph = directedGraphNew();
844
+ for (let index = 0; index < edges.length; index += 1) {
845
+ directedGraphAddEdge(graph, ids[index * 2], ids[index * 2 + 1], edges[index]);
846
+ }
847
+ return directedGraphTraceRings(graph);
430
848
  }
431
- // Winding number of one operand at (px, py): the signed sum of the winding deltas of every segment that
432
- // the downward vertical ray from the point crosses (a segment strictly spanning px whose y at px lies
433
- // below py).
849
+ /**
850
+ * Winding number of one operand at (px, py).
851
+ */
434
852
  function windingAt(unique, px, py, subject) {
435
853
  let winding = 0;
436
854
  for (const v of unique) {
437
855
  const lox = Math.min(v.ax, v.bx);
438
856
  const hix = Math.max(v.ax, v.bx);
439
- // Half-open x-interval so a ray passing exactly through a shared vertex counts exactly one of the
440
- // two edges meeting there (the one that starts at px, not the one that ends at it).
441
- if (px < lox || px >= hix)
857
+ if (px < lox || px >= hix) {
442
858
  continue;
859
+ }
443
860
  const t = (px - v.ax) / (v.bx - v.ax);
444
861
  const yAt = v.ay + t * (v.by - v.ay);
445
- if (yAt < py)
862
+ if (yAt < py) {
446
863
  winding += subject ? v.subjectDelta : v.clipDelta;
447
- }
448
- return winding;
449
- }
450
- function combine(operation, inSubject, inClip) {
451
- switch (operation) {
452
- case 'union':
453
- return inSubject || inClip;
454
- case 'intersection':
455
- return inSubject && inClip;
456
- case 'difference':
457
- return inSubject && !inClip;
458
- case 'xor':
459
- return inSubject !== inClip;
460
- }
461
- }
462
- function isInside(winding, fillRule) {
463
- switch (fillRule) {
464
- case 'evenOdd':
465
- return (Math.abs(winding) & 1) === 1;
466
- case 'positive':
467
- return winding > 0;
468
- case 'negative':
469
- return winding < 0;
470
- default:
471
- return winding !== 0;
472
- }
473
- }
474
- // Scene2D 2c: chain the directed, fill-on-left edges into closed rings. At a vertex shared by several
475
- // rings, the next edge is the one turning most clockwise from the reversed incoming direction, which
476
- // keeps the retained region continuously on the left.
477
- function traceRings(edges) {
478
- const graph = new DirectedGraph();
479
- for (const e of edges)
480
- graph.addEdge(e[0], e[1], e[2], e[3]);
481
- return graph.traceRings();
482
- }
483
- function approxEqual(a, b) {
484
- return Math.abs(a - b) <= vertexSnap;
485
- }
486
- // Derives the vertex-merge grid size from the largest coordinate span across both operands, so the
487
- // snap tolerance tracks the input's magnitude. Zero/degenerate extent falls back to a fixed absolute.
488
- function computeVertexSnap(subject, clip) {
489
- let minX = Infinity;
490
- let minY = Infinity;
491
- let maxX = -Infinity;
492
- let maxY = -Infinity;
493
- for (const contours of [subject, clip]) {
494
- for (const contour of contours) {
495
- for (let i = 0; i < contour.length; i += 2) {
496
- const x = contour[i];
497
- const y = contour[i + 1];
498
- if (x < minX)
499
- minX = x;
500
- if (x > maxX)
501
- maxX = x;
502
- if (y < minY)
503
- minY = y;
504
- if (y > maxY)
505
- maxY = y;
506
- }
507
- }
508
- }
509
- const extent = Math.max(maxX - minX, maxY - minY);
510
- return extent > 0 ? extent * VERTEX_SNAP_RELATIVE : VERTEX_SNAP_FALLBACK;
511
- }
512
- function snap(v) {
513
- return Math.round(v / vertexSnap);
514
- }
515
- // Directed graph of the kept boundary edges. Each directed edge is consumed once; ring closure and
516
- // vertex sharing are handled by angular next-edge selection.
517
- class DirectedGraph {
518
- keys = new Map();
519
- xs = [];
520
- ys = [];
521
- outgoing = [];
522
- edgeTo = [];
523
- edgeFrom = [];
524
- used = [];
525
- addEdge(fromX, fromY, toX, toY) {
526
- const from = this.vertex(fromX, fromY);
527
- const to = this.vertex(toX, toY);
528
- if (from === to)
529
- return;
530
- const edge = this.edgeFrom.length;
531
- this.edgeFrom.push(from);
532
- this.edgeTo.push(to);
533
- this.used.push(false);
534
- this.outgoing[from].push(edge);
535
- }
536
- traceRings() {
537
- const rings = [];
538
- for (let start = 0; start < this.edgeFrom.length; start++) {
539
- if (this.used[start])
540
- continue;
541
- const ring = this.walk(start);
542
- if (ring !== null && ring.length >= 6)
543
- rings.push(ring);
544
- }
545
- return rings;
546
- }
547
- walk(startEdge) {
548
- const ring = [];
549
- let edge = startEdge;
550
- let guard = 0;
551
- const limit = this.edgeFrom.length + 2;
552
- while (guard++ < limit) {
553
- this.used[edge] = true;
554
- const from = this.edgeFrom[edge];
555
- ring.push(this.xs[from], this.ys[from]);
556
- const to = this.edgeTo[edge];
557
- const next = this.nextEdge(to, from);
558
- if (next === -1 || next === startEdge || this.used[next])
559
- break;
560
- edge = next;
561
- }
562
- return ring;
563
- }
564
- // At vertex `at`, arriving from `cameFrom`, pick the unused outgoing edge that turns most clockwise
565
- // from the reversed incoming direction — the walk that keeps the retained region on the left.
566
- nextEdge(at, cameFrom) {
567
- const incomingAngle = Math.atan2(this.ys[cameFrom] - this.ys[at], this.xs[cameFrom] - this.xs[at]);
568
- let best = -1;
569
- let bestGap = Infinity;
570
- for (const edge of this.outgoing[at]) {
571
- if (this.used[edge])
572
- continue;
573
- const to = this.edgeTo[edge];
574
- const angle = Math.atan2(this.ys[to] - this.ys[at], this.xs[to] - this.xs[at]);
575
- let gap = incomingAngle - angle;
576
- while (gap <= 0)
577
- gap += Math.PI * 2;
578
- while (gap > Math.PI * 2)
579
- gap -= Math.PI * 2;
580
- if (gap < bestGap) {
581
- bestGap = gap;
582
- best = edge;
583
- }
584
- }
585
- return best;
586
- }
587
- vertex(x, y) {
588
- const key = `${snap(x)},${snap(y)}`;
589
- const existing = this.keys.get(key);
590
- if (existing !== undefined)
591
- return existing;
592
- const index = this.xs.length;
593
- this.keys.set(key, index);
594
- this.xs.push(x);
595
- this.ys.push(y);
596
- this.outgoing.push([]);
597
- return index;
598
- }
599
- }
600
- // Binary min-heap over sweep events ordered by compareEvents.
601
- class EventHeap {
602
- data = [];
603
- isEmpty() {
604
- return this.data.length === 0;
605
- }
606
- push(event) {
607
- const data = this.data;
608
- data.push(event);
609
- let i = data.length - 1;
610
- while (i > 0) {
611
- const parent = (i - 1) >> 1;
612
- if (compareEvents(data[i], data[parent]) < 0) {
613
- swap(data, i, parent);
614
- i = parent;
615
- }
616
- else
617
- break;
618
864
  }
619
865
  }
620
- pop() {
621
- const data = this.data;
622
- const top = data[0];
623
- const last = data.pop();
624
- if (data.length > 0) {
625
- data[0] = last;
626
- let i = 0;
627
- const n = data.length;
628
- for (;;) {
629
- const left = i * 2 + 1;
630
- const right = left + 1;
631
- let smallest = i;
632
- if (left < n && compareEvents(data[left], data[smallest]) < 0)
633
- smallest = left;
634
- if (right < n && compareEvents(data[right], data[smallest]) < 0)
635
- smallest = right;
636
- if (smallest === i)
637
- break;
638
- swap(data, i, smallest);
639
- i = smallest;
640
- }
641
- }
642
- return top;
643
- }
644
- }
645
- function swap(data, i, j) {
646
- const tmp = data[i];
647
- data[i] = data[j];
648
- data[j] = tmp;
866
+ return winding;
649
867
  }
650
868
  //# sourceMappingURL=martinezKernel.js.map