@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/src/booleanPaths.test.ts
CHANGED
|
@@ -1,45 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// Generated from crates/flight-path-boolean/tests/boolean_paths_test.rs by flight-compiler.
|
|
2
|
+
// Rust is authoritative; run `npm run generate` after changing it.
|
|
3
|
+
|
|
4
|
+
import { appendPathRectangle, clonePath, createPath, flattenPath } from '@flighthq/path/contract';
|
|
5
|
+
import type { Path } from '@flighthq/path/contract';
|
|
4
6
|
import { describe, expect, it } from 'vitest';
|
|
5
7
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
+
import {
|
|
9
|
+
booleanPaths,
|
|
10
|
+
createMartinezPathBooleanKernel,
|
|
11
|
+
differencePaths,
|
|
12
|
+
intersectPaths,
|
|
13
|
+
unionPaths,
|
|
14
|
+
xorPaths,
|
|
15
|
+
} from './contract.ts';
|
|
16
|
+
import type { PathBooleanContour, PathBooleanFillRule, PathBooleanOperation, PathBooleanOptions } from './contract.ts';
|
|
8
17
|
|
|
9
|
-
|
|
18
|
+
function flattenedPath(path: Readonly<Path>, tolerance: number | undefined): number[][] {
|
|
19
|
+
const out = flattenPath(path, tolerance);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
10
22
|
|
|
11
|
-
function rectanglePath(x: number, y: number, w: number, h: number
|
|
12
|
-
|
|
23
|
+
function rectanglePath(x: number, y: number, w: number, h: number): Path {
|
|
24
|
+
let path = createPath('nonZero');
|
|
13
25
|
appendPathRectangle(path, x, y, w, h);
|
|
14
26
|
return path;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
function pathFilledArea(path: Readonly<Path>): number {
|
|
18
|
-
let total = 0;
|
|
19
|
-
for (const ring of
|
|
20
|
-
let area = 0;
|
|
30
|
+
let total: number = 0.0;
|
|
31
|
+
for (const ring of flattenedPath(path, 0.25)) {
|
|
32
|
+
let area: number = 0.0;
|
|
21
33
|
const n = ring.length >> 1;
|
|
22
|
-
|
|
34
|
+
let j = n - 1;
|
|
35
|
+
for (let i = 0; i < n; i += 1) {
|
|
23
36
|
area += ring[j * 2] * ring[i * 2 + 1] - ring[i * 2] * ring[j * 2 + 1];
|
|
37
|
+
j = i;
|
|
24
38
|
}
|
|
25
|
-
total += area / 2;
|
|
39
|
+
total += area / 2.0;
|
|
26
40
|
}
|
|
27
41
|
return Math.abs(total);
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
function pathFillContains(path: Readonly<Path>, x: number, y: number): boolean {
|
|
31
|
-
let winding = 0;
|
|
32
|
-
for (const ring of
|
|
45
|
+
let winding: number = 0;
|
|
46
|
+
for (const ring of flattenedPath(path, 0.25)) {
|
|
33
47
|
const n = ring.length >> 1;
|
|
34
|
-
|
|
48
|
+
let j = n - 1;
|
|
49
|
+
for (let i = 0; i < n; i += 1) {
|
|
35
50
|
const xi = ring[i * 2];
|
|
36
51
|
const yi = ring[i * 2 + 1];
|
|
37
52
|
const xj = ring[j * 2];
|
|
38
53
|
const yj = ring[j * 2 + 1];
|
|
39
54
|
if (yi <= y ? yj > y : yj <= y) {
|
|
40
55
|
const t = (y - yi) / (yj - yi);
|
|
41
|
-
if (x < xi + t * (xj - xi))
|
|
56
|
+
if (x < xi + t * (xj - xi)) {
|
|
57
|
+
winding += yj > yi ? 1 : -1;
|
|
58
|
+
}
|
|
42
59
|
}
|
|
60
|
+
j = i;
|
|
43
61
|
}
|
|
44
62
|
}
|
|
45
63
|
return winding !== 0;
|
|
@@ -47,135 +65,240 @@ function pathFillContains(path: Readonly<Path>, x: number, y: number): boolean {
|
|
|
47
65
|
|
|
48
66
|
describe('booleanPaths', () => {
|
|
49
67
|
it('dispatches to the operation named in the argument', () => {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
booleanPaths(
|
|
57
|
-
|
|
58
|
-
booleanPaths(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
expect(pathFilledArea(
|
|
62
|
-
expect(pathFilledArea(difference)).toBeCloseTo(75, 4);
|
|
63
|
-
expect(pathFilledArea(xor)).toBeCloseTo(150, 4);
|
|
68
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
69
|
+
const a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
70
|
+
const b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
71
|
+
let out = createPath('nonZero');
|
|
72
|
+
booleanPaths(out, kernel, a, b, 'union', undefined);
|
|
73
|
+
expect(Math.abs(pathFilledArea(out) - 175.0) < 5e-5).toBe(true);
|
|
74
|
+
booleanPaths(out, kernel, a, b, 'intersection', undefined);
|
|
75
|
+
expect(Math.abs(pathFilledArea(out) - 25.0) < 5e-5).toBe(true);
|
|
76
|
+
booleanPaths(out, kernel, a, b, 'difference', undefined);
|
|
77
|
+
expect(Math.abs(pathFilledArea(out) - 75.0) < 5e-5).toBe(true);
|
|
78
|
+
booleanPaths(out, kernel, a, b, 'xor', undefined);
|
|
79
|
+
expect(Math.abs(pathFilledArea(out) - 150.0) < 5e-5).toBe(true);
|
|
64
80
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
booleanPaths(
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
it('returns a non zero winding path', () => {
|
|
82
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
83
|
+
let out = createPath('nonZero');
|
|
84
|
+
booleanPaths(
|
|
85
|
+
out,
|
|
86
|
+
kernel,
|
|
87
|
+
rectanglePath(0.0, 0.0, 10.0, 10.0),
|
|
88
|
+
rectanglePath(20.0, 20.0, 5.0, 5.0),
|
|
89
|
+
'union',
|
|
90
|
+
undefined,
|
|
91
|
+
);
|
|
92
|
+
expect(out.winding).toEqual('nonZero');
|
|
93
|
+
expect(out.commands.length > 0).toBe(true);
|
|
71
94
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
it('writes into the provided out path', () => {
|
|
96
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
97
|
+
let out = createPath('nonZero');
|
|
98
|
+
booleanPaths(
|
|
99
|
+
out,
|
|
100
|
+
kernel,
|
|
101
|
+
rectanglePath(0.0, 0.0, 10.0, 10.0),
|
|
102
|
+
rectanglePath(5.0, 5.0, 10.0, 10.0),
|
|
103
|
+
'union',
|
|
104
|
+
undefined,
|
|
105
|
+
);
|
|
106
|
+
expect(Math.abs(pathFilledArea(out) - 175.0) < 5e-5).toBe(true);
|
|
78
107
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const b = rectanglePath(5, 5, 10, 10);
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
it('reuses subject destination with a distinct source snapshot', () => {
|
|
109
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
110
|
+
let a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
111
|
+
const b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
112
|
+
const aClone = a;
|
|
113
|
+
booleanPaths(a, kernel, aClone, b, 'union', undefined);
|
|
114
|
+
expect(Math.abs(pathFilledArea(a) - 175.0) < 0.1).toBe(true);
|
|
85
115
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const a =
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
it('reuses clip destination with a distinct source snapshot', () => {
|
|
117
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
118
|
+
const a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
119
|
+
let b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
120
|
+
const bClone = b;
|
|
121
|
+
booleanPaths(b, kernel, a, bClone, 'intersection', undefined);
|
|
122
|
+
expect(Math.abs(pathFilledArea(b) - 25.0) < 0.1).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
it('honors the even odd fill rule option', () => {
|
|
125
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
126
|
+
let a = createPath('nonZero');
|
|
127
|
+
appendPathRectangle(a, 0.0, 0.0, 10.0, 10.0);
|
|
128
|
+
appendPathRectangle(a, 5.0, 5.0, 10.0, 10.0);
|
|
91
129
|
const empty = createPath('nonZero');
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
booleanPaths(
|
|
96
|
-
expect(pathFilledArea(
|
|
97
|
-
|
|
130
|
+
const nzOpts: PathBooleanOptions = { fillRule: 'nonZero', tolerance: undefined } as const;
|
|
131
|
+
const eoOpts: PathBooleanOptions = { fillRule: 'evenOdd', tolerance: undefined } as const;
|
|
132
|
+
let out = createPath('nonZero');
|
|
133
|
+
booleanPaths(out, kernel, a, empty, 'union', nzOpts);
|
|
134
|
+
expect(Math.abs(pathFilledArea(out) - 175.0) < 5e-5).toBe(true);
|
|
135
|
+
booleanPaths(out, kernel, a, empty, 'union', eoOpts);
|
|
136
|
+
expect(Math.abs(pathFilledArea(out) - 150.0) < 5e-5).toBe(true);
|
|
98
137
|
});
|
|
99
|
-
|
|
100
138
|
it('accepts a coarser tolerance option without error', () => {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
139
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
140
|
+
const a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
141
|
+
const b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
142
|
+
const opts: PathBooleanOptions = { fillRule: undefined, tolerance: 2.0 };
|
|
143
|
+
let out = createPath('nonZero');
|
|
144
|
+
booleanPaths(out, kernel, a, b, 'union', opts);
|
|
145
|
+
expect(Math.abs(pathFilledArea(out) - 175.0) < 5e-5).toBe(true);
|
|
106
146
|
});
|
|
107
147
|
});
|
|
108
148
|
|
|
109
149
|
describe('differencePaths', () => {
|
|
150
|
+
it('supports geometry clone form for first input', () => {
|
|
151
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
152
|
+
let a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
153
|
+
const b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
154
|
+
const snapshot = clonePath(a);
|
|
155
|
+
differencePaths(a, kernel, snapshot, b, undefined);
|
|
156
|
+
expect(Math.abs(pathFilledArea(a) - 75.0) < 0.1).toBe(true);
|
|
157
|
+
expect(!pathFillContains(a, 7.0, 7.0)).toBe(true);
|
|
158
|
+
expect(pathFillContains(a, 2.0, 2.0)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
it('supports geometry clone form for second input', () => {
|
|
161
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
162
|
+
const a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
163
|
+
let b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
164
|
+
const snapshot = clonePath(b);
|
|
165
|
+
differencePaths(b, kernel, a, snapshot, undefined);
|
|
166
|
+
expect(Math.abs(pathFilledArea(b) - 75.0) < 0.1).toBe(true);
|
|
167
|
+
expect(!pathFillContains(b, 7.0, 7.0)).toBe(true);
|
|
168
|
+
expect(pathFillContains(b, 2.0, 2.0)).toBe(true);
|
|
169
|
+
});
|
|
110
170
|
it('cuts a contained hole', () => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
expect(
|
|
115
|
-
expect(pathFillContains(
|
|
171
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
172
|
+
let out = createPath('nonZero');
|
|
173
|
+
differencePaths(out, kernel, rectanglePath(0.0, 0.0, 30.0, 30.0), rectanglePath(10.0, 10.0, 10.0, 10.0), undefined);
|
|
174
|
+
expect(Math.abs(pathFilledArea(out) - 800.0) < 5e-5).toBe(true);
|
|
175
|
+
expect(!pathFillContains(out, 15.0, 15.0)).toBe(true);
|
|
176
|
+
expect(pathFillContains(out, 2.0, 2.0)).toBe(true);
|
|
116
177
|
});
|
|
117
|
-
|
|
118
178
|
it('is empty for a path differenced against itself', () => {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
179
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
180
|
+
const a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
181
|
+
const b = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
182
|
+
let out = createPath('nonZero');
|
|
183
|
+
differencePaths(out, kernel, a, b, undefined);
|
|
184
|
+
expect(pathFilledArea(out) < 0.1).toBe(true);
|
|
124
185
|
});
|
|
125
186
|
});
|
|
126
187
|
|
|
127
188
|
describe('intersectPaths', () => {
|
|
189
|
+
it('supports geometry clone form for first input', () => {
|
|
190
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
191
|
+
let a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
192
|
+
const b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
193
|
+
const snapshot = clonePath(a);
|
|
194
|
+
intersectPaths(a, kernel, snapshot, b, undefined);
|
|
195
|
+
expect(Math.abs(pathFilledArea(a) - 25.0) < 0.1).toBe(true);
|
|
196
|
+
expect(pathFillContains(a, 7.0, 7.0)).toBe(true);
|
|
197
|
+
expect(!pathFillContains(a, 2.0, 2.0)).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
it('supports geometry clone form for second input', () => {
|
|
200
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
201
|
+
const a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
202
|
+
let b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
203
|
+
const snapshot = clonePath(b);
|
|
204
|
+
intersectPaths(b, kernel, a, snapshot, undefined);
|
|
205
|
+
expect(Math.abs(pathFilledArea(b) - 25.0) < 0.1).toBe(true);
|
|
206
|
+
expect(pathFillContains(b, 7.0, 7.0)).toBe(true);
|
|
207
|
+
expect(!pathFillContains(b, 2.0, 2.0)).toBe(true);
|
|
208
|
+
});
|
|
128
209
|
it('keeps the overlap', () => {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
210
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
211
|
+
let out = createPath('nonZero');
|
|
212
|
+
intersectPaths(out, kernel, rectanglePath(0.0, 0.0, 10.0, 10.0), rectanglePath(5.0, 5.0, 10.0, 10.0), undefined);
|
|
213
|
+
expect(Math.abs(pathFilledArea(out) - 25.0) < 5e-5).toBe(true);
|
|
132
214
|
});
|
|
133
|
-
|
|
134
215
|
it('is empty for disjoint inputs', () => {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
216
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
217
|
+
let out = createPath('nonZero');
|
|
218
|
+
intersectPaths(out, kernel, rectanglePath(0.0, 0.0, 10.0, 10.0), rectanglePath(20.0, 20.0, 10.0, 10.0), undefined);
|
|
219
|
+
expect(pathFilledArea(out) < 0.1).toBe(true);
|
|
138
220
|
});
|
|
139
221
|
});
|
|
140
222
|
|
|
141
223
|
describe('unionPaths', () => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
224
|
+
function computeSentinelPathBoolean(
|
|
225
|
+
_subject: Readonly<PathBooleanContour[]>,
|
|
226
|
+
_clip: Readonly<PathBooleanContour[]>,
|
|
227
|
+
_operation: PathBooleanOperation,
|
|
228
|
+
_fillRule: PathBooleanFillRule,
|
|
229
|
+
): PathBooleanContour[] {
|
|
230
|
+
return [[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]];
|
|
231
|
+
}
|
|
232
|
+
it('supports geometry clone form for first input', () => {
|
|
233
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
234
|
+
let a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
235
|
+
const b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
236
|
+
const snapshot = clonePath(a);
|
|
237
|
+
unionPaths(a, kernel, snapshot, b, undefined);
|
|
238
|
+
expect(Math.abs(pathFilledArea(a) - 175.0) < 0.1).toBe(true);
|
|
239
|
+
expect(pathFillContains(a, 2.0, 2.0)).toBe(true);
|
|
240
|
+
expect(pathFillContains(a, 12.0, 12.0)).toBe(true);
|
|
146
241
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const a = rectanglePath(0, 0, 10, 10);
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
expect(
|
|
242
|
+
it('supports geometry clone form for second input', () => {
|
|
243
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
244
|
+
const a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
245
|
+
let b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
246
|
+
const snapshot = clonePath(b);
|
|
247
|
+
unionPaths(b, kernel, a, snapshot, undefined);
|
|
248
|
+
expect(Math.abs(pathFilledArea(b) - 175.0) < 0.1).toBe(true);
|
|
249
|
+
expect(pathFillContains(b, 2.0, 2.0)).toBe(true);
|
|
250
|
+
expect(pathFillContains(b, 12.0, 12.0)).toBe(true);
|
|
156
251
|
});
|
|
157
|
-
|
|
158
252
|
it('routes through a provided kernel', () => {
|
|
159
|
-
let
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
253
|
+
let custom = createMartinezPathBooleanKernel();
|
|
254
|
+
custom.computePathBoolean = computeSentinelPathBoolean;
|
|
255
|
+
let out = createPath('nonZero');
|
|
256
|
+
unionPaths(out, custom, rectanglePath(0.0, 0.0, 10.0, 10.0), rectanglePath(5.0, 5.0, 10.0, 10.0), undefined);
|
|
257
|
+
expect(out.data).toEqual([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
|
|
258
|
+
});
|
|
259
|
+
it('merges overlapping rectangles', () => {
|
|
260
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
261
|
+
let out = createPath('nonZero');
|
|
262
|
+
unionPaths(out, kernel, rectanglePath(0.0, 0.0, 10.0, 10.0), rectanglePath(5.0, 5.0, 10.0, 10.0), undefined);
|
|
263
|
+
expect(Math.abs(pathFilledArea(out) - 175.0) < 5e-5).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
it('commutes', () => {
|
|
266
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
267
|
+
const a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
268
|
+
const b = rectanglePath(4.0, 4.0, 10.0, 10.0);
|
|
269
|
+
let out1 = createPath('nonZero');
|
|
270
|
+
let out2 = createPath('nonZero');
|
|
271
|
+
unionPaths(out1, kernel, a, b, undefined);
|
|
272
|
+
unionPaths(out2, kernel, b, a, undefined);
|
|
273
|
+
expect(Math.abs(pathFilledArea(out1) - pathFilledArea(out2)) < 5e-5).toBe(true);
|
|
172
274
|
});
|
|
173
275
|
});
|
|
174
276
|
|
|
175
277
|
describe('xorPaths', () => {
|
|
278
|
+
it('supports geometry clone form for first input', () => {
|
|
279
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
280
|
+
let a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
281
|
+
const b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
282
|
+
const snapshot = clonePath(a);
|
|
283
|
+
xorPaths(a, kernel, snapshot, b, undefined);
|
|
284
|
+
expect(Math.abs(pathFilledArea(a) - 150.0) < 0.1).toBe(true);
|
|
285
|
+
expect(!pathFillContains(a, 7.0, 7.0)).toBe(true);
|
|
286
|
+
expect(pathFillContains(a, 2.0, 2.0)).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
it('supports geometry clone form for second input', () => {
|
|
289
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
290
|
+
const a = rectanglePath(0.0, 0.0, 10.0, 10.0);
|
|
291
|
+
let b = rectanglePath(5.0, 5.0, 10.0, 10.0);
|
|
292
|
+
const snapshot = clonePath(b);
|
|
293
|
+
xorPaths(b, kernel, a, snapshot, undefined);
|
|
294
|
+
expect(Math.abs(pathFilledArea(b) - 150.0) < 0.1).toBe(true);
|
|
295
|
+
expect(!pathFillContains(b, 7.0, 7.0)).toBe(true);
|
|
296
|
+
expect(pathFillContains(b, 2.0, 2.0)).toBe(true);
|
|
297
|
+
});
|
|
176
298
|
it('keeps the symmetric difference', () => {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
299
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
300
|
+
let out = createPath('nonZero');
|
|
301
|
+
xorPaths(out, kernel, rectanglePath(0.0, 0.0, 10.0, 10.0), rectanglePath(5.0, 5.0, 10.0, 10.0), undefined);
|
|
302
|
+
expect(Math.abs(pathFilledArea(out) - 150.0) < 5e-5).toBe(true);
|
|
180
303
|
});
|
|
181
304
|
});
|
|
@@ -1,34 +1,48 @@
|
|
|
1
|
+
// Generated from crates/flight-path-boolean/tests/explain_path_boolean_test.rs by flight-compiler.
|
|
2
|
+
// Rust is authoritative; run `npm run generate` after changing it.
|
|
3
|
+
|
|
1
4
|
import { appendPathClose, appendPathLineTo, appendPathMoveTo, createPath } from '@flighthq/path/contract';
|
|
2
5
|
import { describe, expect, it } from 'vitest';
|
|
3
6
|
|
|
4
|
-
import { explainOffsetPath, explainSimplifyPath } from './
|
|
5
|
-
import { martinezPathBooleanKernel } from './martinezKernel.ts';
|
|
6
|
-
|
|
7
|
-
const kernel = martinezPathBooleanKernel;
|
|
7
|
+
import { createMartinezPathBooleanKernel, explainOffsetPath, explainSimplifyPath } from './contract.ts';
|
|
8
8
|
|
|
9
9
|
describe('explainOffsetPath', () => {
|
|
10
|
-
it('distinguishes empty
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
it('distinguishes empty invalid collapsed and viable results', () => {
|
|
11
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
12
|
+
const empty = createPath('nonZero');
|
|
13
|
+
const result = explainOffsetPath(kernel, empty, 1.0, undefined);
|
|
14
|
+
expect(result !== undefined).toBe(true);
|
|
15
|
+
expect(result!.reason).toEqual('empty-input');
|
|
16
|
+
let sq = createPath('nonZero');
|
|
17
|
+
appendPathMoveTo(sq, 0.0, 0.0);
|
|
18
|
+
appendPathLineTo(sq, 1.0, 0.0);
|
|
19
|
+
appendPathLineTo(sq, 1.0, 1.0);
|
|
20
|
+
appendPathLineTo(sq, 0.0, 1.0);
|
|
21
|
+
appendPathClose(sq);
|
|
22
|
+
const nanResult = explainOffsetPath(kernel, sq, Number.NaN, undefined);
|
|
23
|
+
expect(nanResult !== undefined).toBe(true);
|
|
24
|
+
expect(nanResult!.reason).toEqual('non-finite-delta');
|
|
25
|
+
const collapsed = explainOffsetPath(kernel, sq, -1.0, undefined);
|
|
26
|
+
expect(collapsed !== undefined).toBe(true);
|
|
27
|
+
expect(collapsed!.reason).toEqual('collapsed-or-degenerate');
|
|
28
|
+
const viable = explainOffsetPath(kernel, sq, 1.0, undefined);
|
|
29
|
+
expect(viable === undefined).toBe(true);
|
|
21
30
|
});
|
|
22
31
|
});
|
|
23
32
|
|
|
24
33
|
describe('explainSimplifyPath', () => {
|
|
25
34
|
it('distinguishes empty input from a viable region', () => {
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
36
|
+
const empty = createPath('nonZero');
|
|
37
|
+
const result = explainSimplifyPath(kernel, empty, undefined);
|
|
38
|
+
expect(result !== undefined).toBe(true);
|
|
39
|
+
expect(result!.reason).toEqual('empty-input');
|
|
40
|
+
let triangle = createPath('nonZero');
|
|
41
|
+
appendPathMoveTo(triangle, 0.0, 0.0);
|
|
42
|
+
appendPathLineTo(triangle, 1.0, 0.0);
|
|
43
|
+
appendPathLineTo(triangle, 0.0, 1.0);
|
|
31
44
|
appendPathClose(triangle);
|
|
32
|
-
|
|
45
|
+
const viable = explainSimplifyPath(kernel, triangle, undefined);
|
|
46
|
+
expect(viable === undefined).toBe(true);
|
|
33
47
|
});
|
|
34
48
|
});
|