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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +2 -2
  2. package/dist/.tsbuildinfo.contract +1 -0
  3. package/dist/.tsbuildinfo.runtime +1 -0
  4. package/dist/booleanPaths.d.ts +33 -6
  5. package/dist/booleanPaths.d.ts.map +1 -1
  6. package/dist/booleanPaths.js +42 -3
  7. package/dist/booleanPaths.js.map +1 -1
  8. package/dist/contract.d.ts +12 -8
  9. package/dist/contract.d.ts.map +1 -1
  10. package/dist/contract.js +11 -8
  11. package/dist/contract.js.map +1 -1
  12. package/dist/contract.path-boolean-types.d.ts +4 -0
  13. package/dist/contract.path-boolean-types.d.ts.map +1 -0
  14. package/dist/contract.path-boolean-types.js +2 -0
  15. package/dist/contract.path-boolean-types.js.map +1 -0
  16. package/dist/explainPathBoolean.d.ts +15 -3
  17. package/dist/explainPathBoolean.d.ts.map +1 -1
  18. package/dist/explainPathBoolean.js +35 -9
  19. package/dist/explainPathBoolean.js.map +1 -1
  20. package/dist/index.d.ts +3 -8
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -8
  23. package/dist/index.js.map +1 -1
  24. package/dist/martinezKernel.d.ts +44 -4
  25. package/dist/martinezKernel.d.ts.map +1 -1
  26. package/dist/martinezKernel.js +777 -559
  27. package/dist/martinezKernel.js.map +1 -1
  28. package/dist/offsetPath.d.ts +15 -2
  29. package/dist/offsetPath.d.ts.map +1 -1
  30. package/dist/offsetPath.js +205 -215
  31. package/dist/offsetPath.js.map +1 -1
  32. package/dist/pathBoolean.d.ts +53 -0
  33. package/dist/pathBoolean.d.ts.map +1 -0
  34. package/dist/pathBoolean.js +115 -0
  35. package/dist/pathBoolean.js.map +1 -0
  36. package/dist/pathBooleanCompat.d.ts +3 -0
  37. package/dist/pathBooleanCompat.d.ts.map +1 -0
  38. package/dist/pathBooleanCompat.js +2 -0
  39. package/dist/pathBooleanCompat.js.map +1 -0
  40. package/dist/resolvePathRegions.d.ts +8 -2
  41. package/dist/resolvePathRegions.d.ts.map +1 -1
  42. package/dist/resolvePathRegions.js +11 -3
  43. package/dist/resolvePathRegions.js.map +1 -1
  44. package/dist/simplifyPath.d.ts +9 -2
  45. package/dist/simplifyPath.d.ts.map +1 -1
  46. package/dist/simplifyPath.js +12 -2
  47. package/dist/simplifyPath.js.map +1 -1
  48. package/dist/unionAllPaths.d.ts +9 -2
  49. package/dist/unionAllPaths.d.ts.map +1 -1
  50. package/dist/unionAllPaths.js +23 -8
  51. package/dist/unionAllPaths.js.map +1 -1
  52. package/dist/writePathBooleanContours.d.ts +8 -2
  53. package/dist/writePathBooleanContours.d.ts.map +1 -1
  54. package/dist/writePathBooleanContours.js +16 -4
  55. package/dist/writePathBooleanContours.js.map +1 -1
  56. package/package.json +5 -4
  57. package/src/booleanPaths.test.ts +237 -114
  58. package/src/explainPathBoolean.test.ts +35 -21
  59. package/src/fuzzInvariants.test.ts +241 -154
  60. package/src/martinezKernel.test.ts +334 -328
  61. package/src/offsetPath.test.ts +296 -166
  62. package/src/pathBoolean.test.ts +95 -0
  63. package/src/resolvePathRegions.test.ts +41 -30
  64. package/src/simplifyPath.test.ts +143 -112
  65. package/src/unionAllPaths.test.ts +93 -61
  66. package/src/writePathBooleanContours.test.ts +44 -6
@@ -1,45 +1,63 @@
1
- import { allocateEntity, finishEntity } from '@flighthq/entity/contract';
2
- import { createPath, appendPathRectangle, flattenPath } from '@flighthq/path/contract';
3
- import type { PathBooleanKernel, Path, PathWinding } from '@flighthq/types/contract';
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 { booleanPaths, differencePaths, intersectPaths, unionPaths, xorPaths } from './booleanPaths.ts';
7
- import { martinezPathBooleanKernel } from './martinezKernel.ts';
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
- const kernel = martinezPathBooleanKernel;
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, winding: PathWinding = 'nonZero'): Path {
12
- const path = createPath(winding);
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 flattenPath(path)) {
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
- for (let i = 0, j = n - 1; i < n; j = i++) {
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 flattenPath(path)) {
45
+ let winding: number = 0;
46
+ for (const ring of flattenedPath(path, 0.25)) {
33
47
  const n = ring.length >> 1;
34
- for (let i = 0, j = n - 1; i < n; j = i++) {
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)) winding += yj > yi ? 1 : -1;
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 a = rectanglePath(0, 0, 10, 10);
51
- const b = rectanglePath(5, 5, 10, 10);
52
- const union = createPath();
53
- const intersection = createPath();
54
- const difference = createPath();
55
- const xor = createPath();
56
- booleanPaths(union, kernel, a, b, 'union');
57
- booleanPaths(intersection, kernel, a, b, 'intersection');
58
- booleanPaths(difference, kernel, a, b, 'difference');
59
- booleanPaths(xor, kernel, a, b, 'xor');
60
- expect(pathFilledArea(union)).toBeCloseTo(175, 4);
61
- expect(pathFilledArea(intersection)).toBeCloseTo(25, 4);
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
- it('writes into the required out path', () => {
67
- const out = createPath('evenOdd');
68
- booleanPaths(out, kernel, rectanglePath(0, 0, 10, 10), rectanglePath(5, 5, 10, 10), 'union');
69
- expect(pathFilledArea(out)).toBeCloseTo(175, 4);
70
- expect(out.winding).toBe('nonZero');
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
- it('is safe when out aliases the subject input', () => {
74
- const a = rectanglePath(0, 0, 10, 10);
75
- const b = rectanglePath(5, 5, 10, 10);
76
- booleanPaths(a, kernel, a, b, 'union');
77
- expect(pathFilledArea(a)).toBeCloseTo(175, 4);
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
- it('is safe when out aliases the clip input', () => {
81
- const a = rectanglePath(0, 0, 10, 10);
82
- const b = rectanglePath(5, 5, 10, 10);
83
- booleanPaths(b, kernel, a, b, 'intersection');
84
- expect(pathFilledArea(b)).toBeCloseTo(25, 4);
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
- it('honors the even-odd fill rule option on self-overlapping input', () => {
88
- const a = createPath('nonZero');
89
- appendPathRectangle(a, 0, 0, 10, 10);
90
- appendPathRectangle(a, 5, 5, 10, 10);
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 nonZero = createPath();
93
- const evenOdd = createPath();
94
- booleanPaths(nonZero, kernel, a, empty, 'union', { fillRule: 'nonZero' });
95
- booleanPaths(evenOdd, kernel, a, empty, 'union', { fillRule: 'evenOdd' });
96
- expect(pathFilledArea(nonZero)).toBeCloseTo(175, 4);
97
- expect(pathFilledArea(evenOdd)).toBeCloseTo(150, 4);
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 a = rectanglePath(0, 0, 10, 10);
102
- const b = rectanglePath(5, 5, 10, 10);
103
- const out = createPath();
104
- booleanPaths(out, kernel, a, b, 'union', { tolerance: 2 });
105
- expect(pathFilledArea(out)).toBeCloseTo(175, 4);
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 result = createPath();
112
- differencePaths(result, kernel, rectanglePath(0, 0, 30, 30), rectanglePath(10, 10, 10, 10));
113
- expect(pathFilledArea(result)).toBeCloseTo(800, 4);
114
- expect(pathFillContains(result, 15, 15)).toBe(false);
115
- expect(pathFillContains(result, 2, 2)).toBe(true);
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 a = rectanglePath(0, 0, 10, 10);
120
- const b = rectanglePath(0, 0, 10, 10);
121
- const result = createPath();
122
- differencePaths(result, kernel, a, b);
123
- expect(pathFilledArea(result)).toBeCloseTo(0, 4);
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 result = createPath();
130
- intersectPaths(result, kernel, rectanglePath(0, 0, 10, 10), rectanglePath(5, 5, 10, 10));
131
- expect(pathFilledArea(result)).toBeCloseTo(25, 4);
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 result = createPath();
136
- intersectPaths(result, kernel, rectanglePath(0, 0, 10, 10), rectanglePath(20, 20, 10, 10));
137
- expect(pathFilledArea(result)).toBeCloseTo(0, 4);
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
- it('merges overlapping rectangles', () => {
143
- const result = createPath();
144
- unionPaths(result, kernel, rectanglePath(0, 0, 10, 10), rectanglePath(5, 5, 10, 10));
145
- expect(pathFilledArea(result)).toBeCloseTo(175, 4);
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
- it('commutes', () => {
149
- const a = rectanglePath(0, 0, 10, 10);
150
- const b = rectanglePath(4, 4, 10, 10);
151
- const ab = createPath();
152
- const ba = createPath();
153
- unionPaths(ab, kernel, a, b);
154
- unionPaths(ba, kernel, b, a);
155
- expect(pathFilledArea(ab)).toBeCloseTo(pathFilledArea(ba), 4);
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 seen = false;
160
- const custom = (() => {
161
- const out = allocateEntity<PathBooleanKernel>();
162
- out.computePathBoolean = () => {
163
- seen = true;
164
- return [];
165
- };
166
- return finishEntity(out);
167
- })();
168
- const result = createPath();
169
- unionPaths(result, custom, rectanglePath(0, 0, 10, 10), rectanglePath(5, 5, 10, 10));
170
- expect(seen).toBe(true);
171
- expect(result.commands.length).toBe(0);
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 result = createPath();
178
- xorPaths(result, kernel, rectanglePath(0, 0, 10, 10), rectanglePath(5, 5, 10, 10));
179
- expect(pathFilledArea(result)).toBeCloseTo(150, 4);
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 './explainPathBoolean.ts';
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, invalid, collapsed, and viable results', () => {
11
- expect(explainOffsetPath(kernel, createPath(), 1)?.reason).toBe('empty-input');
12
- const square = createPath();
13
- appendPathMoveTo(square, 0, 0);
14
- appendPathLineTo(square, 1, 0);
15
- appendPathLineTo(square, 1, 1);
16
- appendPathLineTo(square, 0, 1);
17
- appendPathClose(square);
18
- expect(explainOffsetPath(kernel, square, Number.NaN)?.reason).toBe('non-finite-delta');
19
- expect(explainOffsetPath(kernel, square, -1)?.reason).toBe('collapsed-or-degenerate');
20
- expect(explainOffsetPath(kernel, square, 1)).toBeNull();
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
- expect(explainSimplifyPath(kernel, createPath())?.reason).toBe('empty-input');
27
- const triangle = createPath();
28
- appendPathMoveTo(triangle, 0, 0);
29
- appendPathLineTo(triangle, 1, 0);
30
- appendPathLineTo(triangle, 0, 1);
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
- expect(explainSimplifyPath(kernel, triangle)).toBeNull();
45
+ const viable = explainSimplifyPath(kernel, triangle, undefined);
46
+ expect(viable === undefined).toBe(true);
33
47
  });
34
48
  });