@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/offsetPath.test.ts
CHANGED
|
@@ -1,217 +1,347 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Generated from crates/flight-path-boolean/tests/offset_path_test.rs by flight-compiler.
|
|
2
|
+
// Rust is authoritative; run `npm run generate` after changing it.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
PathCommand,
|
|
6
|
+
appendPathClose,
|
|
7
|
+
appendPathLineTo,
|
|
8
|
+
appendPathMoveTo,
|
|
9
|
+
createPath,
|
|
10
|
+
flattenPath,
|
|
11
|
+
} from '@flighthq/path/contract';
|
|
12
|
+
import type { Path } from '@flighthq/path/contract';
|
|
3
13
|
import { describe, expect, it } from 'vitest';
|
|
4
14
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { simplifyPath } from './simplifyPath.ts';
|
|
15
|
+
import { createMartinezPathBooleanKernel, offsetPath, simplifyPath } from './contract.ts';
|
|
16
|
+
import type { PathBooleanOptions, PathOffsetOptions } from './contract.ts';
|
|
8
17
|
|
|
9
|
-
const
|
|
18
|
+
const UNIT_SQUARE: Float64Array = new Float64Array([0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0]);
|
|
10
19
|
|
|
11
|
-
function
|
|
12
|
-
const
|
|
20
|
+
function flattenedPath(path: Readonly<Path>, tolerance: number | undefined): number[][] {
|
|
21
|
+
const out = flattenPath(path, tolerance);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function polygonPath(vertices: Readonly<number[]>, closed: boolean): Path {
|
|
26
|
+
let path = createPath('nonZero');
|
|
13
27
|
appendPathMoveTo(path, vertices[0], vertices[1]);
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
let i = 2;
|
|
29
|
+
while (i < vertices.length) {
|
|
30
|
+
appendPathLineTo(path, vertices[i], vertices[i + 1]);
|
|
31
|
+
i += 2;
|
|
32
|
+
}
|
|
33
|
+
if (closed) {
|
|
34
|
+
appendPathClose(path);
|
|
35
|
+
}
|
|
16
36
|
return path;
|
|
17
37
|
}
|
|
18
38
|
|
|
19
|
-
function pathBounds(path: Readonly<Path>):
|
|
20
|
-
let minX =
|
|
21
|
-
let minY =
|
|
22
|
-
let maxX =
|
|
23
|
-
let maxY =
|
|
24
|
-
for (const ring of
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
function pathBounds(path: Readonly<Path>): readonly [number, number, number, number] {
|
|
40
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
41
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
42
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
43
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
44
|
+
for (const ring of flattenedPath(path, 0.25)) {
|
|
45
|
+
let i = 0;
|
|
46
|
+
while (i < ring.length) {
|
|
47
|
+
if (ring[i] < minX) {
|
|
48
|
+
minX = ring[i];
|
|
49
|
+
}
|
|
50
|
+
if (ring[i + 1] < minY) {
|
|
51
|
+
minY = ring[i + 1];
|
|
52
|
+
}
|
|
53
|
+
if (ring[i] > maxX) {
|
|
54
|
+
maxX = ring[i];
|
|
55
|
+
}
|
|
56
|
+
if (ring[i + 1] > maxY) {
|
|
57
|
+
maxY = ring[i + 1];
|
|
58
|
+
}
|
|
59
|
+
i += 2;
|
|
30
60
|
}
|
|
31
61
|
}
|
|
32
|
-
return
|
|
62
|
+
return [minX, minY, maxX, maxY];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function lShape(s: number): number[] {
|
|
66
|
+
return [0.0, 0.0, 2.0 * s, 0.0, 2.0 * s, 2.0 * s, s, 2.0 * s, s, s, 0.0, s];
|
|
33
67
|
}
|
|
34
68
|
|
|
35
69
|
function pathArea(path: Readonly<Path>): number {
|
|
36
|
-
let total = 0;
|
|
37
|
-
for (const ring of
|
|
38
|
-
let area = 0;
|
|
70
|
+
let total: number = 0.0;
|
|
71
|
+
for (const ring of flattenedPath(path, 0.25)) {
|
|
72
|
+
let area: number = 0.0;
|
|
39
73
|
const n = ring.length >> 1;
|
|
40
|
-
|
|
74
|
+
let j = n - 1;
|
|
75
|
+
for (let i = 0; i < n; i += 1) {
|
|
41
76
|
area += ring[j * 2] * ring[i * 2 + 1] - ring[i * 2] * ring[j * 2 + 1];
|
|
77
|
+
j = i;
|
|
42
78
|
}
|
|
43
|
-
total += area / 2;
|
|
79
|
+
total += area / 2.0;
|
|
44
80
|
}
|
|
45
81
|
return Math.abs(total);
|
|
46
82
|
}
|
|
47
83
|
|
|
48
|
-
function
|
|
49
|
-
let count = 0;
|
|
50
|
-
for (const ring of
|
|
84
|
+
function _pathVertexCount(path: Readonly<Path>): number {
|
|
85
|
+
let count: number = 0;
|
|
86
|
+
for (const ring of flattenedPath(path, 0.25)) {
|
|
87
|
+
count += Math.trunc(ring.length / 2);
|
|
88
|
+
}
|
|
51
89
|
return count;
|
|
52
90
|
}
|
|
53
91
|
|
|
54
92
|
function ringCount(path: Readonly<Path>): number {
|
|
55
|
-
return
|
|
93
|
+
return flattenedPath(path, 0.25).length;
|
|
56
94
|
}
|
|
57
95
|
|
|
58
|
-
function runOffsetPath(path: Readonly<Path>, delta: number, options?: Readonly<PathOffsetOptions>): Path {
|
|
59
|
-
const out = createPath('nonZero');
|
|
60
|
-
offsetPath(out, kernel, path, delta, options);
|
|
61
|
-
return out;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const UNIT_SQUARE = [0, 0, 1, 0, 1, 1, 0, 1];
|
|
65
|
-
|
|
66
96
|
describe('offsetPath', () => {
|
|
67
|
-
it('writes into an existing output
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
it('writes into an existing output using a distinct source snapshot', () => {
|
|
98
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
99
|
+
let path = polygonPath([0.0, 0.0, 4.0, 0.0, 4.0, 4.0, 0.0, 4.0], true);
|
|
100
|
+
const pathClone = path;
|
|
101
|
+
offsetPath(path, kernel, pathClone, 1.0, undefined);
|
|
102
|
+
const [minX, minY, maxX, maxY] = pathBounds(path);
|
|
103
|
+
expect(Math.abs(minX - -1.0) < 5e-7).toBe(true);
|
|
104
|
+
expect(Math.abs(minY - -1.0) < 5e-7).toBe(true);
|
|
105
|
+
expect(Math.abs(maxX - 5.0) < 5e-7).toBe(true);
|
|
106
|
+
expect(Math.abs(maxY - 5.0) < 5e-7).toBe(true);
|
|
72
107
|
});
|
|
73
|
-
|
|
74
108
|
it('inflates a closed square by delta on every side with a miter join', () => {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
expect(
|
|
80
|
-
expect(
|
|
81
|
-
expect(
|
|
109
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
110
|
+
let out = createPath('nonZero');
|
|
111
|
+
offsetPath(out, kernel, polygonPath(Array.from(UNIT_SQUARE), true), 1.0, undefined);
|
|
112
|
+
const [minX, minY, maxX, maxY] = pathBounds(out);
|
|
113
|
+
expect(Math.abs(minX - -1.0) < 5e-7).toBe(true);
|
|
114
|
+
expect(Math.abs(minY - -1.0) < 5e-7).toBe(true);
|
|
115
|
+
expect(Math.abs(maxX - 2.0) < 5e-7).toBe(true);
|
|
116
|
+
expect(Math.abs(maxY - 2.0) < 5e-7).toBe(true);
|
|
117
|
+
expect(Math.abs(pathArea(out) - 9.0) < 5e-5).toBe(true);
|
|
82
118
|
});
|
|
83
|
-
|
|
84
119
|
it('deflates a closed square on a negative delta', () => {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
expect(
|
|
90
|
-
expect(
|
|
91
|
-
expect(
|
|
120
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
121
|
+
let out = createPath('nonZero');
|
|
122
|
+
offsetPath(out, kernel, polygonPath([0.0, 0.0, 4.0, 0.0, 4.0, 4.0, 0.0, 4.0], true), -1.0, undefined);
|
|
123
|
+
const [minX, minY, maxX, maxY] = pathBounds(out);
|
|
124
|
+
expect(Math.abs(minX - 1.0) < 5e-7).toBe(true);
|
|
125
|
+
expect(Math.abs(minY - 1.0) < 5e-7).toBe(true);
|
|
126
|
+
expect(Math.abs(maxX - 3.0) < 5e-7).toBe(true);
|
|
127
|
+
expect(Math.abs(maxY - 3.0) < 5e-7).toBe(true);
|
|
128
|
+
expect(Math.abs(pathArea(out) - 4.0) < 5e-5).toBe(true);
|
|
92
129
|
});
|
|
93
|
-
|
|
94
130
|
it('emits an empty path when deflation collapses the region', () => {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
132
|
+
let out = createPath('nonZero');
|
|
133
|
+
offsetPath(out, kernel, polygonPath(Array.from(UNIT_SQUARE), true), -1.0, undefined);
|
|
134
|
+
expect(out.commands.length).toEqual(0);
|
|
135
|
+
expect(Math.abs(pathArea(out) - 0.0) < 5e-7).toBe(true);
|
|
98
136
|
});
|
|
99
|
-
|
|
100
137
|
it('chamfers corners with a bevel join', () => {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
138
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
139
|
+
const opts: PathOffsetOptions = {
|
|
140
|
+
join: 'bevel',
|
|
141
|
+
end: undefined,
|
|
142
|
+
miterLimit: undefined,
|
|
143
|
+
tolerance: undefined,
|
|
144
|
+
arcTolerance: undefined,
|
|
145
|
+
} as const;
|
|
146
|
+
let out = createPath('nonZero');
|
|
147
|
+
offsetPath(out, kernel, polygonPath(Array.from(UNIT_SQUARE), true), 1.0, opts);
|
|
148
|
+
const [minX, , maxX] = pathBounds(out);
|
|
149
|
+
expect(Math.abs(minX - -1.0) < 5e-7).toBe(true);
|
|
150
|
+
expect(Math.abs(maxX - 2.0) < 5e-7).toBe(true);
|
|
151
|
+
expect(Math.abs(pathArea(out) - 7.0) < 5e-5).toBe(true);
|
|
106
152
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
153
|
+
it('rounds corners with a round join', () => {
|
|
154
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
155
|
+
const opts: PathOffsetOptions = {
|
|
156
|
+
join: 'round',
|
|
157
|
+
end: undefined,
|
|
158
|
+
miterLimit: undefined,
|
|
159
|
+
tolerance: undefined,
|
|
160
|
+
arcTolerance: undefined,
|
|
161
|
+
} as const;
|
|
162
|
+
let out = createPath('nonZero');
|
|
163
|
+
offsetPath(out, kernel, polygonPath(Array.from(UNIT_SQUARE), true), 1.0, opts);
|
|
164
|
+
const [minX, , maxX] = pathBounds(out);
|
|
165
|
+
expect(Math.abs(minX - -1.0) < 5e-7).toBe(true);
|
|
166
|
+
expect(Math.abs(maxX - 2.0) < 5e-7).toBe(true);
|
|
167
|
+
const fineOpts: PathOffsetOptions = {
|
|
120
168
|
join: 'round',
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
169
|
+
end: undefined,
|
|
170
|
+
miterLimit: undefined,
|
|
171
|
+
tolerance: undefined,
|
|
172
|
+
arcTolerance: 0.001,
|
|
173
|
+
} as const;
|
|
174
|
+
let fine = createPath('nonZero');
|
|
175
|
+
offsetPath(fine, kernel, polygonPath(Array.from(UNIT_SQUARE), true), 1.0, fineOpts);
|
|
176
|
+
const fineArea = pathArea(fine);
|
|
177
|
+
expect(Math.abs(fineArea - (5.0 + Math.PI)) < 0.2).toBe(true);
|
|
124
178
|
});
|
|
125
|
-
|
|
126
179
|
it('falls back to a bevel when a sharp miter exceeds the miter limit', () => {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
180
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
181
|
+
const wedge = [0.0, 0.0, 4.0, 0.5, 4.0, -0.5];
|
|
182
|
+
const clippedOpts: PathOffsetOptions = {
|
|
183
|
+
join: undefined,
|
|
184
|
+
end: undefined,
|
|
185
|
+
miterLimit: 2.0,
|
|
186
|
+
tolerance: undefined,
|
|
187
|
+
arcTolerance: undefined,
|
|
188
|
+
};
|
|
189
|
+
const sharpOpts: PathOffsetOptions = {
|
|
190
|
+
join: undefined,
|
|
191
|
+
end: undefined,
|
|
192
|
+
miterLimit: 50.0,
|
|
193
|
+
tolerance: undefined,
|
|
194
|
+
arcTolerance: undefined,
|
|
195
|
+
};
|
|
196
|
+
let clipped = createPath('nonZero');
|
|
197
|
+
let sharp = createPath('nonZero');
|
|
198
|
+
offsetPath(clipped, kernel, polygonPath(wedge, true), 1.0, clippedOpts);
|
|
199
|
+
offsetPath(sharp, kernel, polygonPath(wedge, true), 1.0, sharpOpts);
|
|
200
|
+
const [clippedMinX, , ,] = pathBounds(clipped);
|
|
201
|
+
const [sharpMinX, , ,] = pathBounds(sharp);
|
|
202
|
+
expect(clippedMinX > sharpMinX + 3.0).toBe(true);
|
|
203
|
+
expect(sharpMinX < -5.0).toBe(true);
|
|
132
204
|
});
|
|
133
|
-
|
|
134
205
|
it('squares corners with a square join', () => {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
206
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
207
|
+
const opts: PathOffsetOptions = {
|
|
208
|
+
join: 'square',
|
|
209
|
+
end: undefined,
|
|
210
|
+
miterLimit: undefined,
|
|
211
|
+
tolerance: undefined,
|
|
212
|
+
arcTolerance: undefined,
|
|
213
|
+
} as const;
|
|
214
|
+
let out = createPath('nonZero');
|
|
215
|
+
offsetPath(out, kernel, polygonPath(Array.from(UNIT_SQUARE), true), 1.0, opts);
|
|
216
|
+
const [minX, , maxX] = pathBounds(out);
|
|
217
|
+
expect(Math.abs(minX - -1.0) < 5e-7).toBe(true);
|
|
218
|
+
expect(Math.abs(maxX - 2.0) < 5e-7).toBe(true);
|
|
219
|
+
expect(Math.abs(pathArea(out) - 9.0) < 5e-5).toBe(true);
|
|
220
|
+
const bevelOpts: PathOffsetOptions = {
|
|
221
|
+
join: 'bevel',
|
|
222
|
+
end: undefined,
|
|
223
|
+
miterLimit: undefined,
|
|
224
|
+
tolerance: undefined,
|
|
225
|
+
arcTolerance: undefined,
|
|
226
|
+
} as const;
|
|
227
|
+
let bevel = createPath('nonZero');
|
|
228
|
+
offsetPath(bevel, kernel, polygonPath(Array.from(UNIT_SQUARE), true), 1.0, bevelOpts);
|
|
229
|
+
expect(pathArea(out) > pathArea(bevel)).toBe(true);
|
|
143
230
|
});
|
|
144
|
-
|
|
145
231
|
it('cleans a concave corner into a single valid outline', () => {
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
expect(
|
|
152
|
-
expect(
|
|
153
|
-
expect(
|
|
154
|
-
expect(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
expect(
|
|
163
|
-
expect(bounds.maxX).toBeCloseTo(11, 6);
|
|
164
|
-
expect(bounds.minY).toBeCloseTo(-1, 6);
|
|
165
|
-
expect(bounds.maxY).toBeCloseTo(11, 6);
|
|
166
|
-
const simplified = createPath();
|
|
167
|
-
simplifyPath(simplified, kernel, result, { fillRule: 'nonZero' });
|
|
168
|
-
expect(ringCount(simplified)).toBe(ringCount(result));
|
|
169
|
-
expect(pathArea(simplified)).toBeCloseTo(pathArea(result), 4);
|
|
232
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
233
|
+
const lShape = [0.0, 0.0, 2.0, 0.0, 2.0, 2.0, 1.0, 2.0, 1.0, 1.0, 0.0, 1.0];
|
|
234
|
+
let out = createPath('nonZero');
|
|
235
|
+
offsetPath(out, kernel, polygonPath(lShape, true), 0.25, undefined);
|
|
236
|
+
const [minX, minY, maxX, maxY] = pathBounds(out);
|
|
237
|
+
expect(Math.abs(minX - -0.25) < 5e-7).toBe(true);
|
|
238
|
+
expect(Math.abs(minY - -0.25) < 5e-7).toBe(true);
|
|
239
|
+
expect(Math.abs(maxX - 2.25) < 5e-7).toBe(true);
|
|
240
|
+
expect(Math.abs(maxY - 2.25) < 5e-7).toBe(true);
|
|
241
|
+
let moveCount = 0;
|
|
242
|
+
for (const command of out.commands) {
|
|
243
|
+
if (command === (PathCommand.MOVE_TO & 0xff)) {
|
|
244
|
+
moveCount += 1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
expect(moveCount).toEqual(1);
|
|
248
|
+
expect(pathArea(out) > 3.0).toBe(true);
|
|
170
249
|
});
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
expect(
|
|
179
|
-
expect(
|
|
180
|
-
expect(
|
|
181
|
-
expect(
|
|
250
|
+
it('closes a concave slot narrower than two delta into a valid outline', () => {
|
|
251
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
252
|
+
const uShape = [0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 5.5, 10.0, 5.5, 3.0, 4.5, 3.0, 4.5, 10.0, 0.0, 10.0];
|
|
253
|
+
let out = createPath('nonZero');
|
|
254
|
+
offsetPath(out, kernel, polygonPath(uShape, true), 1.0, undefined);
|
|
255
|
+
expect(ringCount(out)).toEqual(1);
|
|
256
|
+
const [minX, minY, maxX, maxY] = pathBounds(out);
|
|
257
|
+
expect(Math.abs(minX - -1.0) < 5e-7).toBe(true);
|
|
258
|
+
expect(Math.abs(maxX - 11.0) < 5e-7).toBe(true);
|
|
259
|
+
expect(Math.abs(minY - -1.0) < 5e-7).toBe(true);
|
|
260
|
+
expect(Math.abs(maxY - 11.0) < 5e-7).toBe(true);
|
|
261
|
+
let simplified = createPath('nonZero');
|
|
262
|
+
const simplifyOpts: PathBooleanOptions = { fillRule: 'nonZero', tolerance: undefined } as const;
|
|
263
|
+
simplifyPath(simplified, kernel, out, simplifyOpts);
|
|
264
|
+
expect(ringCount(simplified)).toEqual(ringCount(out));
|
|
265
|
+
expect(Math.abs(pathArea(simplified) - pathArea(out)) < 5e-5).toBe(true);
|
|
182
266
|
});
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
267
|
+
it('strokes an open path into a butt capped rectangle', () => {
|
|
268
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
269
|
+
const opts: PathOffsetOptions = {
|
|
270
|
+
join: undefined,
|
|
271
|
+
end: 'butt',
|
|
272
|
+
miterLimit: undefined,
|
|
273
|
+
tolerance: undefined,
|
|
274
|
+
arcTolerance: undefined,
|
|
275
|
+
} as const;
|
|
276
|
+
let out = createPath('nonZero');
|
|
277
|
+
offsetPath(out, kernel, polygonPath([0.0, 0.0, 2.0, 0.0], false), 0.5, opts);
|
|
278
|
+
const [minX, minY, maxX, maxY] = pathBounds(out);
|
|
279
|
+
expect(Math.abs(minX - 0.0) < 5e-7).toBe(true);
|
|
280
|
+
expect(Math.abs(maxX - 2.0) < 5e-7).toBe(true);
|
|
281
|
+
expect(Math.abs(minY - -0.5) < 5e-7).toBe(true);
|
|
282
|
+
expect(Math.abs(maxY - 0.5) < 5e-7).toBe(true);
|
|
283
|
+
expect(Math.abs(pathArea(out) - 2.0) < 5e-5).toBe(true);
|
|
192
284
|
});
|
|
193
|
-
|
|
194
285
|
it('extends an open path past its ends with a square cap', () => {
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
286
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
287
|
+
const opts: PathOffsetOptions = {
|
|
288
|
+
join: undefined,
|
|
289
|
+
end: 'square',
|
|
290
|
+
miterLimit: undefined,
|
|
291
|
+
tolerance: undefined,
|
|
292
|
+
arcTolerance: undefined,
|
|
293
|
+
} as const;
|
|
294
|
+
let out = createPath('nonZero');
|
|
295
|
+
offsetPath(out, kernel, polygonPath([0.0, 0.0, 2.0, 0.0], false), 0.5, opts);
|
|
296
|
+
const [minX, , maxX] = pathBounds(out);
|
|
297
|
+
expect(Math.abs(minX - -0.5) < 5e-7).toBe(true);
|
|
298
|
+
expect(Math.abs(maxX - 2.5) < 5e-7).toBe(true);
|
|
299
|
+
expect(Math.abs(pathArea(out) - 3.0) < 5e-5).toBe(true);
|
|
200
300
|
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
301
|
+
it('caps an open path with half circles on a round end', () => {
|
|
302
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
303
|
+
const opts: PathOffsetOptions = {
|
|
304
|
+
join: undefined,
|
|
305
|
+
end: 'round',
|
|
306
|
+
miterLimit: undefined,
|
|
307
|
+
tolerance: undefined,
|
|
308
|
+
arcTolerance: 0.001,
|
|
309
|
+
} as const;
|
|
310
|
+
let out = createPath('nonZero');
|
|
311
|
+
offsetPath(out, kernel, polygonPath([0.0, 0.0, 2.0, 0.0], false), 0.5, opts);
|
|
312
|
+
const [minX, , maxX] = pathBounds(out);
|
|
313
|
+
expect(Math.abs(minX - -0.5) < 0.005).toBe(true);
|
|
314
|
+
expect(Math.abs(maxX - 2.5) < 0.005).toBe(true);
|
|
315
|
+
expect(Math.abs(pathArea(out) - (2.0 + Math.PI * 0.25)) < 0.005).toBe(true);
|
|
208
316
|
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const vertices = [0, 0, 2, 0, 2, 2, 0, 2];
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
317
|
+
it('offsets the same vertices differently as open vs closed', () => {
|
|
318
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
319
|
+
const vertices = [0.0, 0.0, 2.0, 0.0, 2.0, 2.0, 0.0, 2.0];
|
|
320
|
+
let closedOut = createPath('nonZero');
|
|
321
|
+
offsetPath(closedOut, kernel, polygonPath(vertices, true), 0.5, undefined);
|
|
322
|
+
const buttOpts: PathOffsetOptions = {
|
|
323
|
+
join: undefined,
|
|
324
|
+
end: 'butt',
|
|
325
|
+
miterLimit: undefined,
|
|
326
|
+
tolerance: undefined,
|
|
327
|
+
arcTolerance: undefined,
|
|
328
|
+
} as const;
|
|
329
|
+
let openOut = createPath('nonZero');
|
|
330
|
+
offsetPath(openOut, kernel, polygonPath(vertices, false), 0.5, buttOpts);
|
|
331
|
+
expect(Math.abs(pathArea(closedOut) - 9.0) < 5e-5).toBe(true);
|
|
332
|
+
expect(pathArea(openOut) < pathArea(closedOut)).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
it('offsets consistently across coordinate scales', () => {
|
|
335
|
+
const kernel = createMartinezPathBooleanKernel();
|
|
336
|
+
let small = createPath('nonZero');
|
|
337
|
+
let mid = createPath('nonZero');
|
|
338
|
+
let large = createPath('nonZero');
|
|
339
|
+
offsetPath(small, kernel, polygonPath(lShape(1e-3), true), 0.25e-3, undefined);
|
|
340
|
+
offsetPath(mid, kernel, polygonPath(lShape(1.0), true), 0.25, undefined);
|
|
341
|
+
offsetPath(large, kernel, polygonPath(lShape(1e6), true), 0.25e6, undefined);
|
|
342
|
+
expect(ringCount(small)).toEqual(ringCount(mid));
|
|
343
|
+
expect(ringCount(large)).toEqual(ringCount(mid));
|
|
344
|
+
expect(Math.abs(pathArea(small) / 1e-6 - pathArea(mid)) < 0.1).toBe(true);
|
|
345
|
+
expect(Math.abs(pathArea(large) / 1e12 - pathArea(mid)) < 0.1).toBe(true);
|
|
216
346
|
});
|
|
217
347
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Generated from crates/flight-path-boolean/tests/path_boolean_test.rs by flight-compiler.
|
|
2
|
+
// Rust is authoritative; run `npm run generate` after changing it.
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getPathBooleanExplanationOperationString,
|
|
8
|
+
getPathBooleanExplanationReasonString,
|
|
9
|
+
getPathOffsetEndString,
|
|
10
|
+
getPathOffsetJoinString,
|
|
11
|
+
parsePathBooleanExplanationOperation,
|
|
12
|
+
parsePathBooleanExplanationReason,
|
|
13
|
+
parsePathOffsetEnd,
|
|
14
|
+
parsePathOffsetJoin,
|
|
15
|
+
} from './contract.ts';
|
|
16
|
+
|
|
17
|
+
describe('getPathBooleanExplanationOperationString', () => {
|
|
18
|
+
it('returns the exact ts strings', () => {
|
|
19
|
+
expect(getPathBooleanExplanationOperationString('offset')).toEqual('offset');
|
|
20
|
+
expect(getPathBooleanExplanationOperationString('simplify')).toEqual('simplify');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('getPathBooleanExplanationReasonString', () => {
|
|
25
|
+
it('returns the exact ts strings', () => {
|
|
26
|
+
expect(getPathBooleanExplanationReasonString('collapsed-or-degenerate')).toEqual('collapsed-or-degenerate');
|
|
27
|
+
expect(getPathBooleanExplanationReasonString('empty-input')).toEqual('empty-input');
|
|
28
|
+
expect(getPathBooleanExplanationReasonString('non-finite-delta')).toEqual('non-finite-delta');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('getPathOffsetEndString', () => {
|
|
33
|
+
it('returns the exact ts strings', () => {
|
|
34
|
+
expect(getPathOffsetEndString('butt')).toEqual('butt');
|
|
35
|
+
expect(getPathOffsetEndString('round')).toEqual('round');
|
|
36
|
+
expect(getPathOffsetEndString('square')).toEqual('square');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('getPathOffsetJoinString', () => {
|
|
41
|
+
it('returns the exact ts strings', () => {
|
|
42
|
+
expect(getPathOffsetJoinString('bevel')).toEqual('bevel');
|
|
43
|
+
expect(getPathOffsetJoinString('miter')).toEqual('miter');
|
|
44
|
+
expect(getPathOffsetJoinString('round')).toEqual('round');
|
|
45
|
+
expect(getPathOffsetJoinString('square')).toEqual('square');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('parsePathBooleanExplanationOperation', () => {
|
|
50
|
+
it('parses the exact ts strings', () => {
|
|
51
|
+
expect(parsePathBooleanExplanationOperation('offset')).toEqual('offset');
|
|
52
|
+
expect(parsePathBooleanExplanationOperation('simplify')).toEqual('simplify');
|
|
53
|
+
});
|
|
54
|
+
it('rejects other text and wrong casing', () => {
|
|
55
|
+
expect(parsePathBooleanExplanationOperation('OFFSET')).toEqual(undefined);
|
|
56
|
+
expect(parsePathBooleanExplanationOperation('')).toEqual(undefined);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('parsePathBooleanExplanationReason', () => {
|
|
61
|
+
it('parses the exact ts strings', () => {
|
|
62
|
+
expect(parsePathBooleanExplanationReason('collapsed-or-degenerate')).toEqual('collapsed-or-degenerate');
|
|
63
|
+
expect(parsePathBooleanExplanationReason('empty-input')).toEqual('empty-input');
|
|
64
|
+
expect(parsePathBooleanExplanationReason('non-finite-delta')).toEqual('non-finite-delta');
|
|
65
|
+
});
|
|
66
|
+
it('rejects other text and wrong casing', () => {
|
|
67
|
+
expect(parsePathBooleanExplanationReason('COLLAPSED-OR-DEGENERATE')).toEqual(undefined);
|
|
68
|
+
expect(parsePathBooleanExplanationReason('')).toEqual(undefined);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('parsePathOffsetEnd', () => {
|
|
73
|
+
it('parses the exact ts strings', () => {
|
|
74
|
+
expect(parsePathOffsetEnd('butt')).toEqual('butt');
|
|
75
|
+
expect(parsePathOffsetEnd('round')).toEqual('round');
|
|
76
|
+
expect(parsePathOffsetEnd('square')).toEqual('square');
|
|
77
|
+
});
|
|
78
|
+
it('rejects other text and wrong casing', () => {
|
|
79
|
+
expect(parsePathOffsetEnd('BUTT')).toEqual(undefined);
|
|
80
|
+
expect(parsePathOffsetEnd('')).toEqual(undefined);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('parsePathOffsetJoin', () => {
|
|
85
|
+
it('parses the exact ts strings', () => {
|
|
86
|
+
expect(parsePathOffsetJoin('bevel')).toEqual('bevel');
|
|
87
|
+
expect(parsePathOffsetJoin('miter')).toEqual('miter');
|
|
88
|
+
expect(parsePathOffsetJoin('round')).toEqual('round');
|
|
89
|
+
expect(parsePathOffsetJoin('square')).toEqual('square');
|
|
90
|
+
});
|
|
91
|
+
it('rejects other text and wrong casing', () => {
|
|
92
|
+
expect(parsePathOffsetJoin('BEVEL')).toEqual(undefined);
|
|
93
|
+
expect(parsePathOffsetJoin('')).toEqual(undefined);
|
|
94
|
+
});
|
|
95
|
+
});
|