@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.
- package/README.md +2 -2
- package/dist/.tsbuildinfo.contract +1 -0
- package/dist/.tsbuildinfo.runtime +1 -0
- package/dist/booleanPaths.d.ts +33 -6
- package/dist/booleanPaths.d.ts.map +1 -1
- package/dist/booleanPaths.js +42 -3
- package/dist/booleanPaths.js.map +1 -1
- package/dist/contract.d.ts +12 -8
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +11 -8
- package/dist/contract.js.map +1 -1
- package/dist/contract.path-boolean-types.d.ts +4 -0
- package/dist/contract.path-boolean-types.d.ts.map +1 -0
- package/dist/contract.path-boolean-types.js +2 -0
- package/dist/contract.path-boolean-types.js.map +1 -0
- package/dist/explainPathBoolean.d.ts +15 -3
- package/dist/explainPathBoolean.d.ts.map +1 -1
- package/dist/explainPathBoolean.js +35 -9
- package/dist/explainPathBoolean.js.map +1 -1
- package/dist/index.d.ts +3 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -8
- package/dist/index.js.map +1 -1
- package/dist/martinezKernel.d.ts +44 -4
- package/dist/martinezKernel.d.ts.map +1 -1
- package/dist/martinezKernel.js +777 -559
- package/dist/martinezKernel.js.map +1 -1
- package/dist/offsetPath.d.ts +15 -2
- package/dist/offsetPath.d.ts.map +1 -1
- package/dist/offsetPath.js +205 -215
- package/dist/offsetPath.js.map +1 -1
- package/dist/pathBoolean.d.ts +53 -0
- package/dist/pathBoolean.d.ts.map +1 -0
- package/dist/pathBoolean.js +115 -0
- package/dist/pathBoolean.js.map +1 -0
- package/dist/pathBooleanCompat.d.ts +3 -0
- package/dist/pathBooleanCompat.d.ts.map +1 -0
- package/dist/pathBooleanCompat.js +2 -0
- package/dist/pathBooleanCompat.js.map +1 -0
- package/dist/resolvePathRegions.d.ts +8 -2
- package/dist/resolvePathRegions.d.ts.map +1 -1
- package/dist/resolvePathRegions.js +11 -3
- package/dist/resolvePathRegions.js.map +1 -1
- package/dist/simplifyPath.d.ts +9 -2
- package/dist/simplifyPath.d.ts.map +1 -1
- package/dist/simplifyPath.js +12 -2
- package/dist/simplifyPath.js.map +1 -1
- package/dist/unionAllPaths.d.ts +9 -2
- package/dist/unionAllPaths.d.ts.map +1 -1
- package/dist/unionAllPaths.js +23 -8
- package/dist/unionAllPaths.js.map +1 -1
- package/dist/writePathBooleanContours.d.ts +8 -2
- package/dist/writePathBooleanContours.d.ts.map +1 -1
- package/dist/writePathBooleanContours.js +16 -4
- package/dist/writePathBooleanContours.js.map +1 -1
- package/package.json +5 -4
- package/src/booleanPaths.test.ts +237 -114
- package/src/explainPathBoolean.test.ts +35 -21
- package/src/fuzzInvariants.test.ts +241 -154
- package/src/martinezKernel.test.ts +334 -328
- package/src/offsetPath.test.ts +296 -166
- package/src/pathBoolean.test.ts +95 -0
- package/src/resolvePathRegions.test.ts +41 -30
- package/src/simplifyPath.test.ts +143 -112
- package/src/unionAllPaths.test.ts +93 -61
- package/src/writePathBooleanContours.test.ts +44 -6
package/dist/martinezKernel.js
CHANGED
|
@@ -1,330 +1,778 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
status
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
477
|
+
}
|
|
478
|
+
addEdge(ax, ay, bx, by, isSubject, events, heap);
|
|
141
479
|
}
|
|
142
480
|
}
|
|
143
481
|
}
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
571
|
+
status[lo] = event;
|
|
242
572
|
return lo;
|
|
243
573
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
692
|
+
}
|
|
693
|
+
if (ax > bx) {
|
|
316
694
|
return 1;
|
|
317
|
-
|
|
695
|
+
}
|
|
696
|
+
if (ay < by) {
|
|
318
697
|
return -1;
|
|
319
|
-
|
|
698
|
+
}
|
|
699
|
+
if (ay > by) {
|
|
320
700
|
return 1;
|
|
701
|
+
}
|
|
321
702
|
return 0;
|
|
322
703
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
357
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
function
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|