@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
|
@@ -1,398 +1,404 @@
|
|
|
1
|
-
|
|
1
|
+
// Generated from crates/flight-path-boolean/tests/martinez_kernel_test.rs by flight-compiler.
|
|
2
|
+
// Rust is authoritative; run `npm run generate` after changing it.
|
|
3
|
+
|
|
2
4
|
import { describe, expect, it } from 'vitest';
|
|
3
5
|
|
|
4
6
|
import {
|
|
5
7
|
createMartinezPathBooleanKernel,
|
|
8
|
+
getPathBooleanFillRuleString,
|
|
9
|
+
getPathBooleanOperationString,
|
|
6
10
|
initializeMartinezPathBooleanKernel,
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
parsePathBooleanFillRule,
|
|
12
|
+
parsePathBooleanOperation,
|
|
13
|
+
} from './contract.ts';
|
|
14
|
+
import type { PathBooleanContour, PathBooleanFillRule, PathBooleanOperation } from './contract.ts';
|
|
15
|
+
|
|
16
|
+
function computeEmptyTestPathBoolean(
|
|
17
|
+
_subject: Readonly<PathBooleanContour[]>,
|
|
18
|
+
_clip: Readonly<PathBooleanContour[]>,
|
|
19
|
+
_operation: PathBooleanOperation,
|
|
20
|
+
_fillRule: PathBooleanFillRule,
|
|
21
|
+
): PathBooleanContour[] {
|
|
22
|
+
return [] as never[];
|
|
23
|
+
}
|
|
9
24
|
|
|
10
|
-
// A closed square contour [x, y, x+s, y, x+s, y+s, x, y+s], wound consistently (screen y-down CW).
|
|
11
25
|
function square(x: number, y: number, s: number): number[] {
|
|
12
26
|
return [x, y, x + s, y, x + s, y + s, x, y + s];
|
|
13
27
|
}
|
|
14
28
|
|
|
15
|
-
// A donut: an outer square with a reverse-wound inner square that cuts a hole under non-zero fill.
|
|
16
29
|
function donut(x: number, y: number, outer: number, inset: number, inner: number): number[][] {
|
|
17
30
|
const ix = x + inset;
|
|
18
31
|
const iy = y + inset;
|
|
19
32
|
return [square(x, y, outer), [ix, iy, ix, iy + inner, ix + inner, iy + inner, ix + inner, iy]];
|
|
20
33
|
}
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
function netArea(rings: Readonly<number[][]>): number {
|
|
36
|
+
let total: number = 0.0;
|
|
37
|
+
for (const ring of rings) {
|
|
38
|
+
total += signedAreaRing(ring);
|
|
39
|
+
}
|
|
27
40
|
return Math.abs(total);
|
|
28
41
|
}
|
|
29
42
|
|
|
30
|
-
function
|
|
31
|
-
|
|
43
|
+
function overlap(s: number): number[][] {
|
|
44
|
+
return [square(0.0, 0.0, 10.0 * s), square(5.0 * s, 5.0 * s, 10.0 * s)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function signedAreaRing(ring: Readonly<number[]>): number {
|
|
48
|
+
let area: number = 0.0;
|
|
32
49
|
const n = ring.length >> 1;
|
|
33
|
-
|
|
50
|
+
let j = n - 1;
|
|
51
|
+
for (let i = 0; i < n; i += 1) {
|
|
34
52
|
area += ring[j * 2] * ring[i * 2 + 1] - ring[i * 2] * ring[j * 2 + 1];
|
|
53
|
+
j = i;
|
|
35
54
|
}
|
|
36
|
-
return area / 2;
|
|
55
|
+
return area / 2.0;
|
|
37
56
|
}
|
|
38
57
|
|
|
39
|
-
function boundsOf(rings:
|
|
40
|
-
minX
|
|
41
|
-
minY
|
|
42
|
-
maxX
|
|
43
|
-
maxY
|
|
44
|
-
} {
|
|
45
|
-
let minX = Infinity;
|
|
46
|
-
let minY = Infinity;
|
|
47
|
-
let maxX = -Infinity;
|
|
48
|
-
let maxY = -Infinity;
|
|
58
|
+
function boundsOf(rings: Readonly<number[][]>): readonly [number, number, number, number] {
|
|
59
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
60
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
61
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
62
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
49
63
|
for (const ring of rings) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
let i = 0;
|
|
65
|
+
while (i < ring.length) {
|
|
66
|
+
if (ring[i] < minX) {
|
|
67
|
+
minX = ring[i];
|
|
68
|
+
}
|
|
69
|
+
if (ring[i] > maxX) {
|
|
70
|
+
maxX = ring[i];
|
|
71
|
+
}
|
|
72
|
+
if (ring[i + 1] < minY) {
|
|
73
|
+
minY = ring[i + 1];
|
|
74
|
+
}
|
|
75
|
+
if (ring[i + 1] > maxY) {
|
|
76
|
+
maxY = ring[i + 1];
|
|
77
|
+
}
|
|
78
|
+
i += 2;
|
|
55
79
|
}
|
|
56
80
|
}
|
|
57
|
-
return
|
|
81
|
+
return [minX, minY, maxX, maxY];
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let winding = 0;
|
|
84
|
+
function fillContains(rings: Readonly<number[][]>, x: number, y: number): boolean {
|
|
85
|
+
let winding: number = 0;
|
|
63
86
|
for (const ring of rings) {
|
|
64
87
|
const n = ring.length >> 1;
|
|
65
|
-
|
|
88
|
+
let j = n - 1;
|
|
89
|
+
for (let i = 0; i < n; i += 1) {
|
|
66
90
|
const xi = ring[i * 2];
|
|
67
91
|
const yi = ring[i * 2 + 1];
|
|
68
92
|
const xj = ring[j * 2];
|
|
69
93
|
const yj = ring[j * 2 + 1];
|
|
70
94
|
if (yi <= y ? yj > y : yj <= y) {
|
|
71
95
|
const t = (y - yi) / (yj - yi);
|
|
72
|
-
if (x < xi + t * (xj - xi))
|
|
96
|
+
if (x < xi + t * (xj - xi)) {
|
|
97
|
+
winding += yj > yi ? 1 : -1;
|
|
98
|
+
}
|
|
73
99
|
}
|
|
100
|
+
j = i;
|
|
74
101
|
}
|
|
75
102
|
}
|
|
76
103
|
return winding !== 0;
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
function run(
|
|
80
|
-
subject:
|
|
81
|
-
clip:
|
|
107
|
+
subject: Readonly<number[][]>,
|
|
108
|
+
clip: Readonly<number[][]>,
|
|
82
109
|
operation: PathBooleanOperation,
|
|
83
|
-
fillRule: PathBooleanFillRule
|
|
84
|
-
):
|
|
85
|
-
|
|
110
|
+
fillRule: PathBooleanFillRule,
|
|
111
|
+
): number[][] {
|
|
112
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
113
|
+
return kernel.computePathBoolean(subject, clip, operation, fillRule);
|
|
86
114
|
}
|
|
87
115
|
|
|
88
116
|
describe('createMartinezPathBooleanKernel', () => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(boundsOf(result)).toMatchObject({ minX: 0, maxX: 20, minY: 0, maxY: 10 });
|
|
94
|
-
expect(fillContains(result, 10, 5)).toBe(true); // the former shared edge is now interior
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('intersects two squares sharing an edge to nothing (zero-width overlap)', () => {
|
|
98
|
-
const result = run([square(0, 0, 10)], [square(10, 0, 10)], 'intersection');
|
|
99
|
-
expect(netArea(result)).toBeCloseTo(0, 6);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('handles a partially overlapping shared edge (offset abutment)', () => {
|
|
103
|
-
// Clip's left edge overlaps only the top half of subject's right edge.
|
|
104
|
-
const result = run([square(0, 0, 10)], [[10, 5, 20, 5, 20, 15, 10, 15]], 'union');
|
|
105
|
-
expect(netArea(result)).toBeCloseTo(200, 6);
|
|
106
|
-
expect(fillContains(result, 10, 7)).toBe(true);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('collapses two identical squares under union to a single square', () => {
|
|
110
|
-
const result = run([square(0, 0, 10)], [square(0, 0, 10)], 'union');
|
|
111
|
-
expect(netArea(result)).toBeCloseTo(100, 6);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('collapses two identical squares under intersection to the same square', () => {
|
|
115
|
-
const result = run([square(0, 0, 10)], [square(0, 0, 10)], 'intersection');
|
|
116
|
-
expect(netArea(result)).toBeCloseTo(100, 6);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('empties two identical squares under xor', () => {
|
|
120
|
-
const result = run([square(0, 0, 10)], [square(0, 0, 10)], 'xor');
|
|
121
|
-
expect(netArea(result)).toBeCloseTo(0, 6);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('empties a square differenced against an identical square', () => {
|
|
125
|
-
const result = run([square(0, 0, 10)], [square(0, 0, 10)], 'difference');
|
|
126
|
-
expect(netArea(result)).toBeCloseTo(0, 6);
|
|
127
|
-
});
|
|
117
|
+
it('creates a kernel that computes boolean operations', () => {
|
|
118
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
119
|
+
const result = kernel.computePathBoolean([square(0.0, 0.0, 10.0)], [square(5.0, 5.0, 10.0)], 'union', 'nonZero');
|
|
120
|
+
expect(Math.abs(netArea(result) - 175.0) < 0.1).toBe(true);
|
|
128
121
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
it('subtracts a partial overlap', () => {
|
|
140
|
-
const result = run([square(0, 0, 10)], [square(5, 5, 10)], 'difference');
|
|
141
|
-
expect(netArea(result)).toBeCloseTo(75, 6);
|
|
142
|
-
expect(fillContains(result, 2, 2)).toBe(true);
|
|
143
|
-
expect(fillContains(result, 7, 7)).toBe(false);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('is empty when subtracting a superset', () => {
|
|
147
|
-
const result = run([square(10, 10, 10)], [square(0, 0, 30)], 'difference');
|
|
148
|
-
expect(netArea(result)).toBeCloseTo(0, 6);
|
|
149
|
-
});
|
|
122
|
+
it('unions two squares sharing a full edge into one rectangle', () => {
|
|
123
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(10.0, 0.0, 10.0)], 'union', 'nonZero');
|
|
124
|
+
expect(Math.abs(netArea(result) - 200.0) < 5e-7).toBe(true);
|
|
125
|
+
const [minX, minY, maxX, maxY] = boundsOf(result);
|
|
126
|
+
expect(Math.abs(minX - 0.0) < 5e-7).toBe(true);
|
|
127
|
+
expect(Math.abs(maxX - 20.0) < 5e-7).toBe(true);
|
|
128
|
+
expect(Math.abs(minY - 0.0) < 5e-7).toBe(true);
|
|
129
|
+
expect(Math.abs(maxY - 10.0) < 5e-7).toBe(true);
|
|
130
|
+
expect(fillContains(result, 10.0, 5.0)).toBe(true);
|
|
150
131
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const result = run([square(0, 0, 10)], [square(20, 20, 10)], 'union');
|
|
155
|
-
expect(netArea(result)).toBeCloseTo(200, 6);
|
|
156
|
-
expect(result.length).toBe(2);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('intersects disjoint squares to nothing', () => {
|
|
160
|
-
const result = run([square(0, 0, 10)], [square(20, 20, 10)], 'intersection');
|
|
161
|
-
expect(netArea(result)).toBeCloseTo(0, 6);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('xors disjoint squares to both squares', () => {
|
|
165
|
-
const result = run([square(0, 0, 10)], [square(20, 20, 10)], 'xor');
|
|
166
|
-
expect(netArea(result)).toBeCloseTo(200, 6);
|
|
167
|
-
});
|
|
132
|
+
it('intersects two squares sharing an edge to nothing', () => {
|
|
133
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(10.0, 0.0, 10.0)], 'intersection', 'nonZero');
|
|
134
|
+
expect(netArea(result) < 5e-7).toBe(true);
|
|
168
135
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('passes the subject through union with an empty clip', () => {
|
|
176
|
-
const result = run([square(0, 0, 10)], [], 'union');
|
|
177
|
-
expect(netArea(result)).toBeCloseTo(100, 6);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('empties intersection with an empty clip', () => {
|
|
181
|
-
expect(netArea(run([square(0, 0, 10)], [], 'intersection'))).toBeCloseTo(0, 6);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('ignores a zero-area (collinear) contour', () => {
|
|
185
|
-
const degenerate = [0, 0, 10, 0, 5, 0]; // three collinear points, no area
|
|
186
|
-
const result = run([square(0, 0, 10)], [degenerate], 'union');
|
|
187
|
-
expect(netArea(result)).toBeCloseTo(100, 6);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('ignores a single-point contour', () => {
|
|
191
|
-
const result = run([square(0, 0, 10)], [[5, 5, 5, 5, 5, 5]], 'union');
|
|
192
|
-
expect(netArea(result)).toBeCloseTo(100, 6);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('ignores a contour with too few points', () => {
|
|
196
|
-
const result = run([square(0, 0, 10)], [[1, 1, 2, 2]], 'union');
|
|
197
|
-
expect(netArea(result)).toBeCloseTo(100, 6);
|
|
198
|
-
});
|
|
136
|
+
it('handles a partially overlapping shared edge', () => {
|
|
137
|
+
const result = run([square(0.0, 0.0, 10.0)], [[10.0, 5.0, 20.0, 5.0, 20.0, 15.0, 10.0, 15.0]], 'union', 'nonZero');
|
|
138
|
+
expect(Math.abs(netArea(result) - 200.0) < 5e-7).toBe(true);
|
|
139
|
+
expect(fillContains(result, 10.0, 7.0)).toBe(true);
|
|
199
140
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const result = run(donut(0, 0, 30, 10, 10), [square(12, 12, 20)], 'union');
|
|
204
|
-
// Donut is 30x30 minus a 10x10 hole (800); the union square (12,12)-(32,32) plugs the hole and
|
|
205
|
-
// extends past the donut. Assert the former hole center is now filled and a hole corner sample
|
|
206
|
-
// that the plug does not cover stays empty.
|
|
207
|
-
expect(fillContains(result, 15, 15)).toBe(true); // plugged
|
|
208
|
-
expect(fillContains(result, 11, 11)).toBe(false); // still a hole corner outside the plug
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('intersects a donut with a square covering its hole to a frame', () => {
|
|
212
|
-
const result = run(donut(0, 0, 30, 10, 10), [square(5, 5, 20)], 'intersection');
|
|
213
|
-
// Intersection keeps donut material within (5,5)-(25,25), which is a frame around the hole.
|
|
214
|
-
expect(fillContains(result, 15, 15)).toBe(false); // the hole survives
|
|
215
|
-
expect(fillContains(result, 8, 8)).toBe(true); // donut material inside the clip
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('differences a square from a donut, deepening the hole', () => {
|
|
219
|
-
const result = run(donut(0, 0, 30, 5, 20), [square(10, 10, 10)], 'difference');
|
|
220
|
-
// Donut hole is (5,5)-(25,25); subtracting (10,10)-(20,20) removes material only where it overlaps
|
|
221
|
-
// the donut ring. Hole center stays empty; ring material outside the clip stays filled.
|
|
222
|
-
expect(fillContains(result, 15, 15)).toBe(false);
|
|
223
|
-
expect(fillContains(result, 2, 2)).toBe(true);
|
|
224
|
-
});
|
|
141
|
+
it('collapses two identical squares under union to a single square', () => {
|
|
142
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(0.0, 0.0, 10.0)], 'union', 'nonZero');
|
|
143
|
+
expect(Math.abs(netArea(result) - 100.0) < 5e-7).toBe(true);
|
|
225
144
|
});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const result = run([square(0, 0, 10), square(5, 5, 10)], [], 'union', 'nonZero');
|
|
230
|
-
expect(netArea(result)).toBeCloseTo(175, 6);
|
|
231
|
-
expect(fillContains(result, 7, 7)).toBe(true); // overlap filled
|
|
232
|
-
expect(result.length).toBe(1); // one solid outline, no hole
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('holes out the overlap of two same-wound squares under even-odd', () => {
|
|
236
|
-
const result = run([square(0, 0, 10), square(5, 5, 10)], [], 'union', 'evenOdd');
|
|
237
|
-
expect(netArea(result)).toBeCloseTo(150, 6);
|
|
238
|
-
expect(fillContains(result, 7, 7)).toBe(false); // overlap is a hole
|
|
239
|
-
expect(fillContains(result, 2, 2)).toBe(true); // non-overlap stays filled
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('treats a bowtie contour under non-zero as its two filled lobes', () => {
|
|
243
|
-
// A self-crossing quad (0,0)-(10,0)-(0,10)-(10,10): the diagonals cross at (5,5) forming two
|
|
244
|
-
// triangles. Under non-zero the opposite-wound lobes each fill.
|
|
245
|
-
const bowtie = [0, 0, 10, 0, 0, 10, 10, 10];
|
|
246
|
-
const result = run([bowtie], [], 'union', 'nonZero');
|
|
247
|
-
expect(fillContains(result, 5, 1)).toBe(true); // lower lobe
|
|
248
|
-
expect(fillContains(result, 5, 9)).toBe(true); // upper lobe
|
|
249
|
-
expect(fillContains(result, 1, 5)).toBe(false); // between lobes, outside
|
|
250
|
-
});
|
|
145
|
+
it('collapses two identical squares under intersection to the same square', () => {
|
|
146
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(0.0, 0.0, 10.0)], 'intersection', 'nonZero');
|
|
147
|
+
expect(Math.abs(netArea(result) - 100.0) < 5e-7).toBe(true);
|
|
251
148
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const ccw = square(0, 0, 10); // shoelace-positive winding
|
|
256
|
-
expect(netArea(run([ccw], [], 'union', 'positive'))).toBeCloseTo(100, 6);
|
|
257
|
-
expect(netArea(run([ccw], [], 'union', 'negative'))).toBeCloseTo(0, 6);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('mirrors that for a clockwise (negatively-wound) region', () => {
|
|
261
|
-
const cw = [0, 0, 0, 10, 10, 10, 10, 0]; // reversed traversal, shoelace-negative winding
|
|
262
|
-
expect(netArea(run([cw], [], 'union', 'negative'))).toBeCloseTo(100, 6);
|
|
263
|
-
expect(netArea(run([cw], [], 'union', 'positive'))).toBeCloseTo(0, 6);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('dissolves a same-wound self-overlap under positive fill (winding 2 stays filled)', () => {
|
|
267
|
-
// The offset-cleanup fill: two overlapping same-wound squares fill solid (area 175, one ring), the
|
|
268
|
-
// doubly-wound overlap kept rather than holed — this is why offsetPath resolves under positive.
|
|
269
|
-
const result = run([square(0, 0, 10), square(5, 5, 10)], [], 'union', 'positive');
|
|
270
|
-
expect(netArea(result)).toBeCloseTo(175, 6);
|
|
271
|
-
expect(result.length).toBe(1);
|
|
272
|
-
expect(fillContains(result, 7, 7)).toBe(true);
|
|
273
|
-
});
|
|
149
|
+
it('empties two identical squares under xor', () => {
|
|
150
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(0.0, 0.0, 10.0)], 'xor', 'nonZero');
|
|
151
|
+
expect(netArea(result) < 5e-7).toBe(true);
|
|
274
152
|
});
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// Two overlapping squares — a real interior crossing, the regime where the vertex-merge snap
|
|
279
|
-
// matters. Scaling the coordinates must not change the ring count nor the area-relative-to-scale²;
|
|
280
|
-
// the magnitude-relative snap is what makes the resolved topology invariant to coordinate scale.
|
|
281
|
-
const overlap = (s: number): number[][] => [square(0, 0, 10 * s), square(5 * s, 5 * s, 10 * s)];
|
|
282
|
-
const small = run(overlap(1e-3), [], 'union', 'nonZero');
|
|
283
|
-
const mid = run(overlap(1), [], 'union', 'nonZero');
|
|
284
|
-
const large = run(overlap(1e6), [], 'union', 'nonZero');
|
|
285
|
-
expect(small.length).toBe(mid.length);
|
|
286
|
-
expect(large.length).toBe(mid.length);
|
|
287
|
-
// Union of the two squares is one solid ring of area 175 at unit scale; area scales with s².
|
|
288
|
-
expect(netArea(mid)).toBeCloseTo(175, 6);
|
|
289
|
-
expect(netArea(small) / 1e-3 ** 2).toBeCloseTo(175, 4);
|
|
290
|
-
expect(netArea(large) / (1e6 * 1e6)).toBeCloseTo(175, 4);
|
|
291
|
-
});
|
|
153
|
+
it('empties a square differenced against an identical square', () => {
|
|
154
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(0.0, 0.0, 10.0)], 'difference', 'nonZero');
|
|
155
|
+
expect(netArea(result) < 5e-7).toBe(true);
|
|
292
156
|
});
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
expect(fillContains(result, 15, 15)).toBe(true);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('intersects two corner-touching squares to nothing', () => {
|
|
303
|
-
const result = run([square(0, 0, 10)], [square(10, 10, 10)], 'intersection');
|
|
304
|
-
expect(netArea(result)).toBeCloseTo(0, 6);
|
|
305
|
-
});
|
|
157
|
+
it('cuts a hole when the clip is fully contained', () => {
|
|
158
|
+
const result = run([square(0.0, 0.0, 30.0)], [square(10.0, 10.0, 10.0)], 'difference', 'nonZero');
|
|
159
|
+
expect(Math.abs(netArea(result) - 800.0) < 5e-7).toBe(true);
|
|
160
|
+
expect(result.length).toEqual(2);
|
|
161
|
+
expect(fillContains(result, 2.0, 2.0)).toBe(true);
|
|
162
|
+
expect(!fillContains(result, 15.0, 15.0)).toBe(true);
|
|
306
163
|
});
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
expect(netArea(run([a], [b], 'union'))).toBeCloseTo(netArea(run([b], [a], 'union')), 6);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('intersections commute', () => {
|
|
316
|
-
const a = square(0, 0, 10);
|
|
317
|
-
const b = square(3, 4, 10);
|
|
318
|
-
expect(netArea(run([a], [b], 'intersection'))).toBeCloseTo(netArea(run([b], [a], 'intersection')), 6);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('xor is symmetric', () => {
|
|
322
|
-
const a = square(0, 0, 10);
|
|
323
|
-
const b = square(4, 4, 10);
|
|
324
|
-
expect(netArea(run([a], [b], 'xor'))).toBeCloseTo(netArea(run([b], [a], 'xor')), 6);
|
|
325
|
-
});
|
|
164
|
+
it('subtracts a partial overlap', () => {
|
|
165
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(5.0, 5.0, 10.0)], 'difference', 'nonZero');
|
|
166
|
+
expect(Math.abs(netArea(result) - 75.0) < 5e-7).toBe(true);
|
|
167
|
+
expect(fillContains(result, 2.0, 2.0)).toBe(true);
|
|
168
|
+
expect(!fillContains(result, 7.0, 7.0)).toBe(true);
|
|
326
169
|
});
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const result = run([square(0, 0, 30)], [square(10, 10, 10)], 'union');
|
|
331
|
-
expect(netArea(result)).toBeCloseTo(900, 6);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('intersects to the inner square', () => {
|
|
335
|
-
const result = run([square(0, 0, 30)], [square(10, 10, 10)], 'intersection');
|
|
336
|
-
expect(netArea(result)).toBeCloseTo(100, 6);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('xors to the outer-with-hole frame', () => {
|
|
340
|
-
const result = run([square(0, 0, 30)], [square(10, 10, 10)], 'xor');
|
|
341
|
-
expect(netArea(result)).toBeCloseTo(800, 6);
|
|
342
|
-
expect(fillContains(result, 15, 15)).toBe(false);
|
|
343
|
-
expect(fillContains(result, 2, 2)).toBe(true);
|
|
344
|
-
});
|
|
170
|
+
it('is empty when subtracting a superset', () => {
|
|
171
|
+
const result = run([square(10.0, 10.0, 10.0)], [square(0.0, 0.0, 30.0)], 'difference', 'nonZero');
|
|
172
|
+
expect(netArea(result) < 5e-7).toBe(true);
|
|
345
173
|
});
|
|
174
|
+
it('unions disjoint squares into two rings', () => {
|
|
175
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(20.0, 20.0, 10.0)], 'union', 'nonZero');
|
|
176
|
+
expect(Math.abs(netArea(result) - 200.0) < 5e-7).toBe(true);
|
|
177
|
+
expect(result.length).toEqual(2);
|
|
178
|
+
});
|
|
179
|
+
it('intersects disjoint squares to nothing', () => {
|
|
180
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(20.0, 20.0, 10.0)], 'intersection', 'nonZero');
|
|
181
|
+
expect(netArea(result) < 5e-7).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
it('xors disjoint squares to both squares', () => {
|
|
184
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(20.0, 20.0, 10.0)], 'xor', 'nonZero');
|
|
185
|
+
expect(Math.abs(netArea(result) - 200.0) < 5e-7).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
it('returns empty for two empty operands', () => {
|
|
188
|
+
const result = run([], [], 'union', 'nonZero');
|
|
189
|
+
expect(result.length === 0).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
it('passes the subject through union with an empty clip', () => {
|
|
192
|
+
const result = run([square(0.0, 0.0, 10.0)], [], 'union', 'nonZero');
|
|
193
|
+
expect(Math.abs(netArea(result) - 100.0) < 5e-7).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
it('empties intersection with an empty clip', () => {
|
|
196
|
+
const result = run([square(0.0, 0.0, 10.0)], [], 'intersection', 'nonZero');
|
|
197
|
+
expect(netArea(result) < 5e-7).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
it('ignores a zero area collinear contour', () => {
|
|
200
|
+
const degenerate = [0.0, 0.0, 10.0, 0.0, 5.0, 0.0];
|
|
201
|
+
const result = run([square(0.0, 0.0, 10.0)], [degenerate], 'union', 'nonZero');
|
|
202
|
+
expect(Math.abs(netArea(result) - 100.0) < 5e-7).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
it('ignores a single point contour', () => {
|
|
205
|
+
const result = run([square(0.0, 0.0, 10.0)], [[5.0, 5.0, 5.0, 5.0, 5.0, 5.0]], 'union', 'nonZero');
|
|
206
|
+
expect(Math.abs(netArea(result) - 100.0) < 5e-7).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
it('ignores a contour with too few points', () => {
|
|
209
|
+
const result = run([square(0.0, 0.0, 10.0)], [[1.0, 1.0, 2.0, 2.0]], 'union', 'nonZero');
|
|
210
|
+
expect(Math.abs(netArea(result) - 100.0) < 5e-7).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
it('unions a donut with an overlapping square filling part of the hole', () => {
|
|
213
|
+
const result = run(donut(0.0, 0.0, 30.0, 10.0, 10.0), [square(12.0, 12.0, 20.0)], 'union', 'nonZero');
|
|
214
|
+
expect(fillContains(result, 15.0, 15.0)).toBe(true);
|
|
215
|
+
expect(!fillContains(result, 11.0, 11.0)).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
it('intersects a donut with a square covering its hole to a frame', () => {
|
|
218
|
+
const result = run(donut(0.0, 0.0, 30.0, 10.0, 10.0), [square(5.0, 5.0, 20.0)], 'intersection', 'nonZero');
|
|
219
|
+
expect(!fillContains(result, 15.0, 15.0)).toBe(true);
|
|
220
|
+
expect(fillContains(result, 8.0, 8.0)).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
it('differences a square from a donut deepening the hole', () => {
|
|
223
|
+
const result = run(donut(0.0, 0.0, 30.0, 5.0, 20.0), [square(10.0, 10.0, 10.0)], 'difference', 'nonZero');
|
|
224
|
+
expect(!fillContains(result, 15.0, 15.0)).toBe(true);
|
|
225
|
+
expect(fillContains(result, 2.0, 2.0)).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
it('fills the overlap of two same wound squares under non zero', () => {
|
|
228
|
+
const result = run([square(0.0, 0.0, 10.0), square(5.0, 5.0, 10.0)], [], 'union', 'nonZero');
|
|
229
|
+
expect(Math.abs(netArea(result) - 175.0) < 5e-7).toBe(true);
|
|
230
|
+
expect(fillContains(result, 7.0, 7.0)).toBe(true);
|
|
231
|
+
expect(result.length).toEqual(1);
|
|
232
|
+
});
|
|
233
|
+
it('holes out the overlap of two same wound squares under even odd', () => {
|
|
234
|
+
const result = run([square(0.0, 0.0, 10.0), square(5.0, 5.0, 10.0)], [], 'union', 'evenOdd');
|
|
235
|
+
expect(Math.abs(netArea(result) - 150.0) < 5e-7).toBe(true);
|
|
236
|
+
expect(!fillContains(result, 7.0, 7.0)).toBe(true);
|
|
237
|
+
expect(fillContains(result, 2.0, 2.0)).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
it('treats a bowtie contour under non zero as its two filled lobes', () => {
|
|
240
|
+
const bowtie = [0.0, 0.0, 10.0, 0.0, 0.0, 10.0, 10.0, 10.0];
|
|
241
|
+
const result = run([bowtie], [], 'union', 'nonZero');
|
|
242
|
+
expect(fillContains(result, 5.0, 1.0)).toBe(true);
|
|
243
|
+
expect(fillContains(result, 5.0, 9.0)).toBe(true);
|
|
244
|
+
expect(!fillContains(result, 1.0, 5.0)).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
it('keeps a counter clockwise region under positive fill drops it under negative', () => {
|
|
247
|
+
const ccw = square(0.0, 0.0, 10.0);
|
|
248
|
+
expect(Math.abs(netArea(run([ccw.slice()], [], 'union', 'positive')) - 100.0) < 5e-7).toBe(true);
|
|
249
|
+
expect(netArea(run([ccw], [], 'union', 'negative')) < 5e-7).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
it('mirrors that for a clockwise region', () => {
|
|
252
|
+
const cw = [0.0, 0.0, 0.0, 10.0, 10.0, 10.0, 10.0, 0.0];
|
|
253
|
+
expect(Math.abs(netArea(run([cw.slice()], [], 'union', 'negative')) - 100.0) < 5e-7).toBe(true);
|
|
254
|
+
expect(netArea(run([cw], [], 'union', 'positive')) < 5e-7).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
it('dissolves a same wound self overlap under positive fill', () => {
|
|
257
|
+
const result = run([square(0.0, 0.0, 10.0), square(5.0, 5.0, 10.0)], [], 'union', 'positive');
|
|
258
|
+
expect(Math.abs(netArea(result) - 175.0) < 5e-7).toBe(true);
|
|
259
|
+
expect(result.length).toEqual(1);
|
|
260
|
+
expect(fillContains(result, 7.0, 7.0)).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
it('resolves the same topology across coordinate scales', () => {
|
|
263
|
+
const small = run(overlap(1e-3), [], 'union', 'nonZero');
|
|
264
|
+
const mid = run(overlap(1.0), [], 'union', 'nonZero');
|
|
265
|
+
const large = run(overlap(1e6), [], 'union', 'nonZero');
|
|
266
|
+
expect(small.length).toEqual(mid.length);
|
|
267
|
+
expect(large.length).toEqual(mid.length);
|
|
268
|
+
expect(Math.abs(netArea(mid) - 175.0) < 5e-7).toBe(true);
|
|
269
|
+
expect(Math.abs(netArea(small) / 1e-6 - 175.0) < 5e-5).toBe(true);
|
|
270
|
+
expect(Math.abs(netArea(large) / 1e12 - 175.0) < 5e-5).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
it('unions two corner touching squares to their combined area', () => {
|
|
273
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(10.0, 10.0, 10.0)], 'union', 'nonZero');
|
|
274
|
+
expect(Math.abs(netArea(result) - 200.0) < 5e-7).toBe(true);
|
|
275
|
+
expect(fillContains(result, 5.0, 5.0)).toBe(true);
|
|
276
|
+
expect(fillContains(result, 15.0, 15.0)).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
it('intersects two corner touching squares to nothing', () => {
|
|
279
|
+
const result = run([square(0.0, 0.0, 10.0)], [square(10.0, 10.0, 10.0)], 'intersection', 'nonZero');
|
|
280
|
+
expect(netArea(result) < 5e-7).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
it('unions commute', () => {
|
|
283
|
+
const a = square(0.0, 0.0, 10.0);
|
|
284
|
+
const b = square(5.0, 5.0, 10.0);
|
|
285
|
+
const ab = netArea(run([a.slice()], [b.slice()], 'union', 'nonZero'));
|
|
286
|
+
const ba = netArea(run([b], [a], 'union', 'nonZero'));
|
|
287
|
+
expect(Math.abs(ab - ba) < 5e-7).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
it('intersections commute', () => {
|
|
290
|
+
const a = square(0.0, 0.0, 10.0);
|
|
291
|
+
const b = square(3.0, 4.0, 10.0);
|
|
292
|
+
const ab = netArea(run([a.slice()], [b.slice()], 'intersection', 'nonZero'));
|
|
293
|
+
const ba = netArea(run([b], [a], 'intersection', 'nonZero'));
|
|
294
|
+
expect(Math.abs(ab - ba) < 5e-7).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
it('xor is symmetric', () => {
|
|
297
|
+
const a = square(0.0, 0.0, 10.0);
|
|
298
|
+
const b = square(4.0, 4.0, 10.0);
|
|
299
|
+
const ab = netArea(run([a.slice()], [b.slice()], 'xor', 'nonZero'));
|
|
300
|
+
const ba = netArea(run([b], [a], 'xor', 'nonZero'));
|
|
301
|
+
expect(Math.abs(ab - ba) < 5e-7).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
it('unions contained to the outer square', () => {
|
|
304
|
+
const result = run([square(0.0, 0.0, 30.0)], [square(10.0, 10.0, 10.0)], 'union', 'nonZero');
|
|
305
|
+
expect(Math.abs(netArea(result) - 900.0) < 5e-7).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
it('intersects contained to the inner square', () => {
|
|
308
|
+
const result = run([square(0.0, 0.0, 30.0)], [square(10.0, 10.0, 10.0)], 'intersection', 'nonZero');
|
|
309
|
+
expect(Math.abs(netArea(result) - 100.0) < 5e-7).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
it('xors contained to the outer with hole frame', () => {
|
|
312
|
+
const result = run([square(0.0, 0.0, 30.0)], [square(10.0, 10.0, 10.0)], 'xor', 'nonZero');
|
|
313
|
+
expect(Math.abs(netArea(result) - 800.0) < 5e-7).toBe(true);
|
|
314
|
+
expect(!fillContains(result, 15.0, 15.0)).toBe(true);
|
|
315
|
+
expect(fillContains(result, 2.0, 2.0)).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
it('unions two triangles sharing a diagonal into a square', () => {
|
|
318
|
+
const lower = [0.0, 0.0, 10.0, 0.0, 10.0, 10.0];
|
|
319
|
+
const upper = [0.0, 0.0, 10.0, 10.0, 0.0, 10.0];
|
|
320
|
+
const result = run([lower], [upper], 'union', 'nonZero');
|
|
321
|
+
expect(Math.abs(netArea(result) - 100.0) < 5e-7).toBe(true);
|
|
322
|
+
expect(fillContains(result, 5.0, 5.0)).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
it('intersects two diagonal sharing triangles to nothing', () => {
|
|
325
|
+
const lower = [0.0, 0.0, 10.0, 0.0, 10.0, 10.0];
|
|
326
|
+
const upper = [0.0, 0.0, 10.0, 10.0, 0.0, 10.0];
|
|
327
|
+
const result = run([lower], [upper], 'intersection', 'nonZero');
|
|
328
|
+
expect(netArea(result) < 5e-7).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
it('intersects a diamond overlapping a square', () => {
|
|
331
|
+
const diamond = [5.0, 5.0, 10.0, 0.0, 15.0, 5.0, 10.0, 10.0];
|
|
332
|
+
const result = run([square(0.0, 0.0, 10.0)], [diamond], 'intersection', 'nonZero');
|
|
333
|
+
expect(Math.abs(netArea(result) - 25.0) < 5e-7).toBe(true);
|
|
334
|
+
expect(fillContains(result, 8.0, 5.0)).toBe(true);
|
|
335
|
+
expect(!fillContains(result, 12.0, 5.0)).toBe(true);
|
|
336
|
+
});
|
|
337
|
+
it('unions a rotated square with an overlapping axis square', () => {
|
|
338
|
+
const diamond = [0.0, 10.0, 10.0, 0.0, 20.0, 10.0, 10.0, 20.0];
|
|
339
|
+
const result = run([square(0.0, 0.0, 12.0)], [diamond], 'union', 'nonZero');
|
|
340
|
+
expect(fillContains(result, 2.0, 2.0)).toBe(true);
|
|
341
|
+
expect(fillContains(result, 18.0, 10.0)).toBe(true);
|
|
342
|
+
expect(!fillContains(result, 19.0, 19.0)).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
346
345
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
expect(fillContains(result, 5, 5)).toBe(true); // the former shared diagonal is interior
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it('intersects two diagonal-sharing triangles to nothing', () => {
|
|
357
|
-
const lower = [0, 0, 10, 0, 10, 10];
|
|
358
|
-
const upper = [0, 0, 10, 10, 0, 10];
|
|
359
|
-
expect(netArea(run([lower], [upper], 'intersection'))).toBeCloseTo(0, 6);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it('intersects a diamond overlapping a square (real crossing points)', () => {
|
|
363
|
-
// Diamond centered on the square's right edge; only its left triangle (area 25) is inside.
|
|
364
|
-
const diamond = [5, 5, 10, 0, 15, 5, 10, 10];
|
|
365
|
-
const result = run([square(0, 0, 10)], [diamond], 'intersection');
|
|
366
|
-
expect(netArea(result)).toBeCloseTo(25, 6);
|
|
367
|
-
expect(fillContains(result, 8, 5)).toBe(true);
|
|
368
|
-
expect(fillContains(result, 12, 5)).toBe(false);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('unions a rotated square (diamond) with an overlapping axis square', () => {
|
|
372
|
-
const diamond = [0, 10, 10, 0, 20, 10, 10, 20]; // area 200, centered (10,10)
|
|
373
|
-
const result = run([square(0, 0, 12)], [diamond], 'union');
|
|
374
|
-
// Both regions are inside the union; a point deep in each stays filled and the exterior stays out.
|
|
375
|
-
expect(fillContains(result, 2, 2)).toBe(true); // square-only corner
|
|
376
|
-
expect(fillContains(result, 18, 10)).toBe(true); // diamond-only tip region
|
|
377
|
-
expect(fillContains(result, 19, 19)).toBe(false); // outside both
|
|
378
|
-
});
|
|
346
|
+
describe('getPathBooleanFillRuleString', () => {
|
|
347
|
+
it('returns the exact ts strings', () => {
|
|
348
|
+
expect(getPathBooleanFillRuleString('evenOdd')).toEqual('evenOdd');
|
|
349
|
+
expect(getPathBooleanFillRuleString('nonZero')).toEqual('nonZero');
|
|
350
|
+
expect(getPathBooleanFillRuleString('negative')).toEqual('negative');
|
|
351
|
+
expect(getPathBooleanFillRuleString('positive')).toEqual('positive');
|
|
379
352
|
});
|
|
380
353
|
});
|
|
381
354
|
|
|
382
|
-
describe('
|
|
383
|
-
it('
|
|
384
|
-
expect(
|
|
355
|
+
describe('getPathBooleanOperationString', () => {
|
|
356
|
+
it('returns the exact ts strings', () => {
|
|
357
|
+
expect(getPathBooleanOperationString('difference')).toEqual('difference');
|
|
358
|
+
expect(getPathBooleanOperationString('intersection')).toEqual('intersection');
|
|
359
|
+
expect(getPathBooleanOperationString('union')).toEqual('union');
|
|
360
|
+
expect(getPathBooleanOperationString('xor')).toEqual('xor');
|
|
385
361
|
});
|
|
386
362
|
});
|
|
387
363
|
|
|
388
|
-
describe('
|
|
389
|
-
it('is the
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
364
|
+
describe('initializeMartinezPathBooleanKernel', () => {
|
|
365
|
+
it('is the construction initializer of create martinez path boolean kernel', () => {
|
|
366
|
+
let kernel = createMartinezPathBooleanKernel();
|
|
367
|
+
kernel.computePathBoolean = computeEmptyTestPathBoolean;
|
|
368
|
+
initializeMartinezPathBooleanKernel(kernel);
|
|
369
|
+
expect(kernel).toEqual(createMartinezPathBooleanKernel());
|
|
370
|
+
const result = kernel.computePathBoolean(
|
|
371
|
+
[square(0.0, 0.0, 10.0)],
|
|
372
|
+
[square(5.0, 5.0, 10.0)],
|
|
373
|
+
'intersection',
|
|
394
374
|
'nonZero',
|
|
395
375
|
);
|
|
396
|
-
expect(netArea(result)).
|
|
376
|
+
expect(Math.abs(netArea(result) - 25.0) < 0.1).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe('parsePathBooleanFillRule', () => {
|
|
381
|
+
it('parses the exact ts strings', () => {
|
|
382
|
+
expect(parsePathBooleanFillRule('evenOdd')).toEqual('evenOdd');
|
|
383
|
+
expect(parsePathBooleanFillRule('nonZero')).toEqual('nonZero');
|
|
384
|
+
expect(parsePathBooleanFillRule('negative')).toEqual('negative');
|
|
385
|
+
expect(parsePathBooleanFillRule('positive')).toEqual('positive');
|
|
386
|
+
});
|
|
387
|
+
it('rejects other text and wrong casing', () => {
|
|
388
|
+
expect(parsePathBooleanFillRule('EVENODD')).toEqual(undefined);
|
|
389
|
+
expect(parsePathBooleanFillRule('')).toEqual(undefined);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('parsePathBooleanOperation', () => {
|
|
394
|
+
it('parses the exact ts strings', () => {
|
|
395
|
+
expect(parsePathBooleanOperation('difference')).toEqual('difference');
|
|
396
|
+
expect(parsePathBooleanOperation('intersection')).toEqual('intersection');
|
|
397
|
+
expect(parsePathBooleanOperation('union')).toEqual('union');
|
|
398
|
+
expect(parsePathBooleanOperation('xor')).toEqual('xor');
|
|
399
|
+
});
|
|
400
|
+
it('rejects other text and wrong casing', () => {
|
|
401
|
+
expect(parsePathBooleanOperation('DIFFERENCE')).toEqual(undefined);
|
|
402
|
+
expect(parsePathBooleanOperation('')).toEqual(undefined);
|
|
397
403
|
});
|
|
398
404
|
});
|