@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,50 +1,61 @@
1
+ // Generated from crates/flight-path-boolean/tests/resolve_path_regions_test.rs by flight-compiler.
2
+ // Rust is authoritative; run `npm run generate` after changing it.
3
+
1
4
  import { createPath, flattenPath } from '@flighthq/path/contract';
2
- import type { Path } from '@flighthq/types/contract';
5
+ import type { Path } from '@flighthq/path/contract';
3
6
  import { describe, expect, it } from 'vitest';
4
7
 
5
- import { martinezPathBooleanKernel } from './martinezKernel.ts';
6
- import { resolvePathRegions } from './resolvePathRegions.ts';
8
+ import { createMartinezPathBooleanKernel, resolvePathRegions } from './contract.ts';
7
9
 
8
- const kernel = martinezPathBooleanKernel;
10
+ function flattenedPath(path: Readonly<Path>, tolerance: number | undefined): number[][] {
11
+ const out = flattenPath(path, tolerance);
12
+ return out;
13
+ }
9
14
 
10
15
  function pathArea(path: Readonly<Path>): number {
11
- let total = 0;
12
- for (const ring of flattenPath(path)) {
13
- let area = 0;
16
+ let total: number = 0.0;
17
+ for (const ring of flattenedPath(path, 0.25)) {
18
+ let area: number = 0.0;
14
19
  const n = ring.length >> 1;
15
- for (let i = 0, j = n - 1; i < n; j = i++) area += ring[j * 2] * ring[i * 2 + 1] - ring[i * 2] * ring[j * 2 + 1];
16
- total += area / 2;
20
+ let j = n - 1;
21
+ for (let i = 0; i < n; i += 1) {
22
+ area += ring[j * 2] * ring[i * 2 + 1] - ring[i * 2] * ring[j * 2 + 1];
23
+ j = i;
24
+ }
25
+ total += area / 2.0;
17
26
  }
18
27
  return Math.abs(total);
19
28
  }
20
29
 
21
30
  function ringCount(path: Readonly<Path>): number {
22
- return flattenPath(path).length;
31
+ return flattenedPath(path, 0.25).length;
23
32
  }
24
33
 
25
- const SQUARE_A = [0, 0, 4, 0, 4, 4, 0, 4];
26
- const SQUARE_B = [2, 2, 6, 2, 6, 6, 2, 6];
27
-
28
34
  describe('resolvePathRegions', () => {
29
35
  it('returns an empty path for no rings', () => {
30
- const result = createPath('evenOdd');
31
- resolvePathRegions(result, kernel, [], 'nonZero');
32
- expect(result.commands).toHaveLength(0);
33
- expect(result.data).toHaveLength(0);
34
- expect(result.winding).toBe('nonZero');
36
+ const kernel = createMartinezPathBooleanKernel();
37
+ let out = createPath('nonZero');
38
+ resolvePathRegions(out, kernel, [], 'nonZero');
39
+ expect(out.commands.length).toEqual(0);
40
+ expect(out.data.length).toEqual(0);
41
+ expect(out.winding).toEqual('nonZero');
35
42
  });
36
-
37
- it('self-unions overlapping rings into one outline under nonZero', () => {
38
- const result = createPath('nonZero');
39
- resolvePathRegions(result, kernel, [SQUARE_A, SQUARE_B], 'nonZero');
40
- expect(ringCount(result)).toBe(1);
41
- expect(pathArea(result)).toBeCloseTo(28, 6);
43
+ it('self unions overlapping rings into one outline under non zero', () => {
44
+ const kernel = createMartinezPathBooleanKernel();
45
+ const squareA = [0.0, 0.0, 4.0, 0.0, 4.0, 4.0, 0.0, 4.0];
46
+ const squareB = [2.0, 2.0, 6.0, 2.0, 6.0, 6.0, 2.0, 6.0];
47
+ let out = createPath('nonZero');
48
+ resolvePathRegions(out, kernel, [squareA, squareB], 'nonZero');
49
+ expect(ringCount(out)).toEqual(1);
50
+ expect(Math.abs(pathArea(out) - 28.0) < 5e-7).toBe(true);
42
51
  });
43
-
44
- it('applies the fill rule to self-overlap under evenOdd', () => {
45
- const result = createPath('nonZero');
46
- resolvePathRegions(result, kernel, [SQUARE_A, SQUARE_B], 'evenOdd');
47
- expect(pathArea(result)).toBeCloseTo(24, 6);
48
- expect(ringCount(result)).toBeGreaterThan(1);
52
+ it('applies the fill rule to self overlap under even odd', () => {
53
+ const kernel = createMartinezPathBooleanKernel();
54
+ const squareA = [0.0, 0.0, 4.0, 0.0, 4.0, 4.0, 0.0, 4.0];
55
+ const squareB = [2.0, 2.0, 6.0, 2.0, 6.0, 6.0, 2.0, 6.0];
56
+ let out = createPath('nonZero');
57
+ resolvePathRegions(out, kernel, [squareA, squareB], 'evenOdd');
58
+ expect(Math.abs(pathArea(out) - 24.0) < 5e-7).toBe(true);
59
+ expect(ringCount(out) > 1).toBe(true);
49
60
  });
50
61
  });
@@ -1,152 +1,183 @@
1
+ // Generated from crates/flight-path-boolean/tests/simplify_path_test.rs by flight-compiler.
2
+ // Rust is authoritative; run `npm run generate` after changing it.
3
+
1
4
  import { appendPathClose, appendPathLineTo, appendPathMoveTo, createPath, flattenPath } from '@flighthq/path/contract';
2
- import type { Path, PathWinding } from '@flighthq/types/contract';
5
+ import type { Path } from '@flighthq/path/contract';
3
6
  import { describe, expect, it } from 'vitest';
4
7
 
5
- import { martinezPathBooleanKernel } from './martinezKernel.ts';
6
- import { simplifyPath } from './simplifyPath.ts';
8
+ import { createMartinezPathBooleanKernel, simplifyPath } from './contract.ts';
9
+ import type { PathBooleanOptions } from './contract.ts';
10
+
11
+ const BOWTIE: Float64Array = new Float64Array([0.0, 0.0, 2.0, 2.0, 2.0, 0.0, 0.0, 2.0]);
12
+
13
+ const UNIT_SQUARE: Float64Array = new Float64Array([0.0, 0.0, 3.0, 0.0, 3.0, 3.0, 0.0, 3.0]);
7
14
 
8
- const kernel = martinezPathBooleanKernel;
15
+ function flattenedPath(path: Readonly<Path>, tolerance: number | undefined): number[][] {
16
+ const out = flattenPath(path, tolerance);
17
+ return out;
18
+ }
9
19
 
10
- function polygonPath(vertices: readonly number[], winding: PathWinding = 'nonZero'): Path {
11
- const path = createPath(winding);
20
+ function polygonPath(vertices: Readonly<number[]>): Path {
21
+ let path = createPath('nonZero');
12
22
  appendPathMoveTo(path, vertices[0], vertices[1]);
13
- for (let i = 2; i < vertices.length; i += 2) appendPathLineTo(path, vertices[i], vertices[i + 1]);
23
+ let i = 2;
24
+ while (i < vertices.length) {
25
+ appendPathLineTo(path, vertices[i], vertices[i + 1]);
26
+ i += 2;
27
+ }
14
28
  appendPathClose(path);
15
29
  return path;
16
30
  }
17
31
 
18
- function pathBounds(path: Readonly<Path>): { minX: number; minY: number; maxX: number; maxY: number } {
19
- let minX = Infinity;
20
- let minY = Infinity;
21
- let maxX = -Infinity;
22
- let maxY = -Infinity;
23
- for (const ring of flattenPath(path)) {
24
- for (let i = 0; i < ring.length; i += 2) {
25
- minX = Math.min(minX, ring[i]);
26
- minY = Math.min(minY, ring[i + 1]);
27
- maxX = Math.max(maxX, ring[i]);
28
- maxY = Math.max(maxY, ring[i + 1]);
32
+ function pathArea(path: Readonly<Path>): number {
33
+ let total: number = 0.0;
34
+ for (const ring of flattenedPath(path, 0.25)) {
35
+ let area: number = 0.0;
36
+ const n = ring.length >> 1;
37
+ let j = n - 1;
38
+ for (let i = 0; i < n; i += 1) {
39
+ area += ring[j * 2] * ring[i * 2 + 1] - ring[i * 2] * ring[j * 2 + 1];
40
+ j = i;
29
41
  }
42
+ total += area / 2.0;
30
43
  }
31
- return { minX, minY, maxX, maxY };
44
+ return Math.abs(total);
32
45
  }
33
46
 
34
- function pathArea(path: Readonly<Path>): number {
35
- let total = 0;
36
- for (const ring of flattenPath(path)) {
37
- let area = 0;
38
- const n = ring.length >> 1;
39
- for (let i = 0, j = n - 1; i < n; j = i++) area += ring[j * 2] * ring[i * 2 + 1] - ring[i * 2] * ring[j * 2 + 1];
40
- total += area / 2;
47
+ function pathBounds(path: Readonly<Path>): readonly [number, number, number, number] {
48
+ let minX = Number.POSITIVE_INFINITY;
49
+ let minY = Number.POSITIVE_INFINITY;
50
+ let maxX = Number.NEGATIVE_INFINITY;
51
+ let maxY = Number.NEGATIVE_INFINITY;
52
+ for (const ring of flattenedPath(path, 0.25)) {
53
+ let i = 0;
54
+ while (i < ring.length) {
55
+ if (ring[i] < minX) {
56
+ minX = ring[i];
57
+ }
58
+ if (ring[i + 1] < minY) {
59
+ minY = ring[i + 1];
60
+ }
61
+ if (ring[i] > maxX) {
62
+ maxX = ring[i];
63
+ }
64
+ if (ring[i + 1] > maxY) {
65
+ maxY = ring[i + 1];
66
+ }
67
+ i += 2;
68
+ }
41
69
  }
42
- return Math.abs(total);
70
+ return [minX, minY, maxX, maxY];
43
71
  }
44
72
 
45
73
  function ringCount(path: Readonly<Path>): number {
46
- return flattenPath(path).length;
74
+ return flattenedPath(path, 0.25).length;
47
75
  }
48
76
 
49
- function pentagramPath(radius: number, winding: PathWinding): Path {
77
+ function pentagramPath(radius: number): Path {
50
78
  const order = [0, 2, 4, 1, 3];
51
- const vertices: number[] = [];
79
+ let vertices: number[] = [] as never[];
52
80
  for (const k of order) {
53
- const angle = -Math.PI / 2 + (k * 2 * Math.PI) / 5;
54
- vertices.push(radius * Math.cos(angle), radius * Math.sin(angle));
81
+ const angle = -(Math.PI / 2.0) + (k * 2.0 * Math.PI) / 5.0;
82
+ vertices.push(radius * Math.cos(angle));
83
+ vertices.push(radius * Math.sin(angle));
55
84
  }
56
- return polygonPath(vertices, winding);
85
+ return polygonPath(vertices);
57
86
  }
58
87
 
59
- const BOWTIE = [0, 0, 2, 2, 2, 0, 0, 2];
60
-
61
- const UNIT_SQUARE = [0, 0, 3, 0, 3, 3, 0, 3];
62
-
63
88
  describe('simplifyPath', () => {
64
- it('writes into an existing output and is alias-safe', () => {
65
- const path = polygonPath(BOWTIE);
66
- simplifyPath(path, kernel, path);
67
-
68
- expect(ringCount(path)).toBe(2);
69
- expect(pathArea(path)).toBeCloseTo(2, 6);
89
+ it('writes into an existing output using a distinct source snapshot', () => {
90
+ const kernel = createMartinezPathBooleanKernel();
91
+ let path = polygonPath(Array.from(BOWTIE));
92
+ const pathClone = path;
93
+ simplifyPath(path, kernel, pathClone, undefined);
94
+ expect(ringCount(path)).toEqual(2);
95
+ expect(Math.abs(pathArea(path) - 2.0) < 5e-7).toBe(true);
70
96
  });
71
-
72
- it('resolves a self-intersecting bowtie into two triangles', () => {
73
- const result = createPath();
74
- simplifyPath(result, kernel, polygonPath(BOWTIE));
75
- expect(ringCount(result)).toBe(2);
76
- expect(pathArea(result)).toBeCloseTo(2, 6);
97
+ it('resolves a self intersecting bowtie into two triangles', () => {
98
+ const kernel = createMartinezPathBooleanKernel();
99
+ let out = createPath('nonZero');
100
+ simplifyPath(out, kernel, polygonPath(Array.from(BOWTIE)), undefined);
101
+ expect(ringCount(out)).toEqual(2);
102
+ expect(Math.abs(pathArea(out) - 2.0) < 5e-7).toBe(true);
77
103
  });
78
-
79
- it('fills a self-overlapping region solid under nonZero', () => {
80
- const path = createPath('nonZero');
81
- appendPathMoveTo(path, 0, 0);
82
- appendPathLineTo(path, 4, 0);
83
- appendPathLineTo(path, 4, 4);
84
- appendPathLineTo(path, 0, 4);
104
+ it('fills a self overlapping region solid under non zero', () => {
105
+ const kernel = createMartinezPathBooleanKernel();
106
+ let path = createPath('nonZero');
107
+ appendPathMoveTo(path, 0.0, 0.0);
108
+ appendPathLineTo(path, 4.0, 0.0);
109
+ appendPathLineTo(path, 4.0, 4.0);
110
+ appendPathLineTo(path, 0.0, 4.0);
85
111
  appendPathClose(path);
86
- appendPathMoveTo(path, 2, 2);
87
- appendPathLineTo(path, 6, 2);
88
- appendPathLineTo(path, 6, 6);
89
- appendPathLineTo(path, 2, 6);
112
+ appendPathMoveTo(path, 2.0, 2.0);
113
+ appendPathLineTo(path, 6.0, 2.0);
114
+ appendPathLineTo(path, 6.0, 6.0);
115
+ appendPathLineTo(path, 2.0, 6.0);
90
116
  appendPathClose(path);
91
- const result = createPath();
92
- simplifyPath(result, kernel, path, { fillRule: 'nonZero' });
93
- expect(ringCount(result)).toBe(1);
94
- expect(pathArea(result)).toBeCloseTo(28, 6);
117
+ const opts: PathBooleanOptions = { fillRule: 'nonZero', tolerance: undefined } as const;
118
+ let out = createPath('nonZero');
119
+ simplifyPath(out, kernel, path, opts);
120
+ expect(ringCount(out)).toEqual(1);
121
+ expect(Math.abs(pathArea(out) - 28.0) < 5e-7).toBe(true);
95
122
  });
96
-
97
- it('punches an even-odd hole through a self-overlapping region', () => {
98
- const path = createPath('evenOdd');
99
- appendPathMoveTo(path, 0, 0);
100
- appendPathLineTo(path, 4, 0);
101
- appendPathLineTo(path, 4, 4);
102
- appendPathLineTo(path, 0, 4);
123
+ it('punches an even odd hole through a self overlapping region', () => {
124
+ const kernel = createMartinezPathBooleanKernel();
125
+ let path = createPath('nonZero');
126
+ path.winding = 'evenOdd';
127
+ appendPathMoveTo(path, 0.0, 0.0);
128
+ appendPathLineTo(path, 4.0, 0.0);
129
+ appendPathLineTo(path, 4.0, 4.0);
130
+ appendPathLineTo(path, 0.0, 4.0);
103
131
  appendPathClose(path);
104
- appendPathMoveTo(path, 2, 2);
105
- appendPathLineTo(path, 6, 2);
106
- appendPathLineTo(path, 6, 6);
107
- appendPathLineTo(path, 2, 6);
132
+ appendPathMoveTo(path, 2.0, 2.0);
133
+ appendPathLineTo(path, 6.0, 2.0);
134
+ appendPathLineTo(path, 6.0, 6.0);
135
+ appendPathLineTo(path, 2.0, 6.0);
108
136
  appendPathClose(path);
109
- const result = createPath();
110
- simplifyPath(result, kernel, path, { fillRule: 'evenOdd' });
111
- expect(pathArea(result)).toBeCloseTo(24, 6);
112
- expect(ringCount(result)).toBeGreaterThan(1);
137
+ const opts: PathBooleanOptions = { fillRule: 'evenOdd', tolerance: undefined } as const;
138
+ let out = createPath('nonZero');
139
+ simplifyPath(out, kernel, path, opts);
140
+ expect(Math.abs(pathArea(out) - 24.0) < 5e-7).toBe(true);
141
+ expect(ringCount(out) > 1).toBe(true);
113
142
  });
114
-
115
- it('fills a self-overlapping star solid under nonZero but hollow under evenOdd', () => {
116
- const solid = createPath();
117
- const hollow = createPath();
118
- simplifyPath(solid, kernel, pentagramPath(10, 'nonZero'), { fillRule: 'nonZero' });
119
- simplifyPath(hollow, kernel, pentagramPath(10, 'evenOdd'), { fillRule: 'evenOdd' });
120
- expect(ringCount(solid)).toBe(1);
121
- expect(pathArea(solid)).toBeCloseTo(112.257, 2);
122
- expect(ringCount(hollow)).toBeGreaterThan(1);
123
- expect(pathArea(hollow)).toBeCloseTo(77.568, 2);
124
- expect(pathArea(hollow)).toBeLessThan(pathArea(solid));
143
+ it('fills a pentagram solid under non zero but hollow under even odd', () => {
144
+ const kernel = createMartinezPathBooleanKernel();
145
+ const nzOpts: PathBooleanOptions = { fillRule: 'nonZero', tolerance: undefined } as const;
146
+ const eoOpts: PathBooleanOptions = { fillRule: 'evenOdd', tolerance: undefined } as const;
147
+ let solid = createPath('nonZero');
148
+ let hollow = createPath('nonZero');
149
+ simplifyPath(solid, kernel, pentagramPath(10.0), nzOpts);
150
+ simplifyPath(hollow, kernel, pentagramPath(10.0), eoOpts);
151
+ expect(ringCount(solid)).toEqual(1);
152
+ expect(Math.abs(pathArea(solid) - 112.257) < 0.005).toBe(true);
153
+ expect(ringCount(hollow) > 1).toBe(true);
154
+ expect(Math.abs(pathArea(hollow) - 77.568) < 0.005).toBe(true);
155
+ expect(pathArea(hollow) < pathArea(solid)).toBe(true);
125
156
  });
126
-
127
- it('passes an already-simple convex path through unchanged', () => {
128
- const result = createPath();
129
- simplifyPath(result, kernel, polygonPath(UNIT_SQUARE));
130
- expect(ringCount(result)).toBe(1);
131
- expect(pathArea(result)).toBeCloseTo(9, 6);
132
- const bounds = pathBounds(result);
133
- expect(bounds.minX).toBeCloseTo(0, 6);
134
- expect(bounds.minY).toBeCloseTo(0, 6);
135
- expect(bounds.maxX).toBeCloseTo(3, 6);
136
- expect(bounds.maxY).toBeCloseTo(3, 6);
157
+ it('passes an already simple convex path through unchanged', () => {
158
+ const kernel = createMartinezPathBooleanKernel();
159
+ let out = createPath('nonZero');
160
+ simplifyPath(out, kernel, polygonPath(Array.from(UNIT_SQUARE)), undefined);
161
+ expect(ringCount(out)).toEqual(1);
162
+ expect(Math.abs(pathArea(out) - 9.0) < 5e-7).toBe(true);
163
+ const [minX, minY, maxX, maxY] = pathBounds(out);
164
+ expect(Math.abs(minX - 0.0) < 5e-7).toBe(true);
165
+ expect(Math.abs(minY - 0.0) < 5e-7).toBe(true);
166
+ expect(Math.abs(maxX - 3.0) < 5e-7).toBe(true);
167
+ expect(Math.abs(maxY - 3.0) < 5e-7).toBe(true);
137
168
  });
138
-
139
169
  it('returns an empty path for empty input', () => {
140
- const result = createPath();
141
- simplifyPath(result, kernel, createPath('nonZero'));
142
- expect(result.commands).toHaveLength(0);
143
- expect(result.data).toHaveLength(0);
170
+ const kernel = createMartinezPathBooleanKernel();
171
+ let out = createPath('nonZero');
172
+ simplifyPath(out, kernel, createPath('nonZero'), undefined);
173
+ expect(out.commands.length).toEqual(0);
174
+ expect(out.data.length).toEqual(0);
144
175
  });
145
-
146
- it('returns an empty path for a degenerate zero-area contour', () => {
147
- const result = createPath();
148
- simplifyPath(result, kernel, polygonPath([0, 0, 5, 0, 10, 0]));
149
- expect(result.commands).toHaveLength(0);
150
- expect(result.data).toHaveLength(0);
176
+ it('returns an empty path for a degenerate zero area contour', () => {
177
+ const kernel = createMartinezPathBooleanKernel();
178
+ let out = createPath('nonZero');
179
+ simplifyPath(out, kernel, polygonPath([0.0, 0.0, 5.0, 0.0, 10.0, 0.0]), undefined);
180
+ expect(out.commands.length).toEqual(0);
181
+ expect(out.data.length).toEqual(0);
151
182
  });
152
183
  });
@@ -1,14 +1,26 @@
1
- import { appendPathClose, appendPathLineTo, appendPathMoveTo, createPath, flattenPath } from '@flighthq/path/contract';
2
- import type { Path } from '@flighthq/types/contract';
1
+ // Generated from crates/flight-path-boolean/tests/union_all_paths_test.rs by flight-compiler.
2
+ // Rust is authoritative; run `npm run generate` after changing it.
3
+
4
+ import {
5
+ appendPathClose,
6
+ appendPathLineTo,
7
+ appendPathMoveTo,
8
+ clonePath,
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 { martinezPathBooleanKernel } from './martinezKernel.ts';
6
- import { unionAllPaths } from './unionAllPaths.ts';
15
+ import { createMartinezPathBooleanKernel, unionAllPaths } from './contract.ts';
7
16
 
8
- const kernel = martinezPathBooleanKernel;
17
+ function flattenedPath(path: Readonly<Path>, tolerance: number | undefined): number[][] {
18
+ const out = flattenPath(path, tolerance);
19
+ return out;
20
+ }
9
21
 
10
22
  function squarePath(x: number, y: number, s: number): Path {
11
- const path = createPath('nonZero');
23
+ let path = createPath('nonZero');
12
24
  appendPathMoveTo(path, x, y);
13
25
  appendPathLineTo(path, x + s, y);
14
26
  appendPathLineTo(path, x + s, y + s);
@@ -18,78 +30,98 @@ function squarePath(x: number, y: number, s: number): Path {
18
30
  }
19
31
 
20
32
  function pathArea(path: Readonly<Path>): number {
21
- let total = 0;
22
- for (const ring of flattenPath(path)) {
23
- let area = 0;
33
+ let total: number = 0.0;
34
+ for (const ring of flattenedPath(path, 0.25)) {
35
+ let area: number = 0.0;
24
36
  const n = ring.length >> 1;
25
- for (let i = 0, j = n - 1; i < n; j = i++) area += ring[j * 2] * ring[i * 2 + 1] - ring[i * 2] * ring[j * 2 + 1];
26
- total += area / 2;
37
+ let j = n - 1;
38
+ for (let i = 0; i < n; i += 1) {
39
+ area += ring[j * 2] * ring[i * 2 + 1] - ring[i * 2] * ring[j * 2 + 1];
40
+ j = i;
41
+ }
42
+ total += area / 2.0;
27
43
  }
28
44
  return Math.abs(total);
29
45
  }
30
46
 
31
47
  function ringCount(path: Readonly<Path>): number {
32
- return flattenPath(path).length;
48
+ return flattenedPath(path, 0.25).length;
33
49
  }
34
50
 
35
51
  describe('unionAllPaths', () => {
52
+ it('supports geometry clone form for a later list input', () => {
53
+ const kernel = createMartinezPathBooleanKernel();
54
+ const a = squarePath(0.0, 0.0, 10.0);
55
+ let b = squarePath(5.0, 5.0, 10.0);
56
+ const snapshot = clonePath(b);
57
+ unionAllPaths(b, kernel, [a, snapshot], undefined);
58
+ expect(Math.abs(pathArea(b) - 175.0) < 0.1).toBe(true);
59
+ });
36
60
  it('returns an empty path for an empty list', () => {
37
- const result = createPath('nonZero');
38
- unionAllPaths(result, kernel, []);
39
- expect(result.commands).toHaveLength(0);
40
- expect(result.data).toHaveLength(0);
61
+ const kernel = createMartinezPathBooleanKernel();
62
+ let out = createPath('nonZero');
63
+ unionAllPaths(out, kernel, [], undefined);
64
+ expect(out.commands.length).toEqual(0);
65
+ expect(out.data.length).toEqual(0);
41
66
  });
42
-
43
- it('simplifies a single self-overlapping path', () => {
44
- const overlap = createPath('nonZero');
45
- appendPathMoveTo(overlap, 0, 0);
46
- appendPathLineTo(overlap, 4, 0);
47
- appendPathLineTo(overlap, 4, 4);
48
- appendPathLineTo(overlap, 0, 4);
67
+ it('simplifies a single self overlapping path', () => {
68
+ const kernel = createMartinezPathBooleanKernel();
69
+ let overlap = createPath('nonZero');
70
+ appendPathMoveTo(overlap, 0.0, 0.0);
71
+ appendPathLineTo(overlap, 4.0, 0.0);
72
+ appendPathLineTo(overlap, 4.0, 4.0);
73
+ appendPathLineTo(overlap, 0.0, 4.0);
49
74
  appendPathClose(overlap);
50
- appendPathMoveTo(overlap, 2, 2);
51
- appendPathLineTo(overlap, 6, 2);
52
- appendPathLineTo(overlap, 6, 6);
53
- appendPathLineTo(overlap, 2, 6);
75
+ appendPathMoveTo(overlap, 2.0, 2.0);
76
+ appendPathLineTo(overlap, 6.0, 2.0);
77
+ appendPathLineTo(overlap, 6.0, 6.0);
78
+ appendPathLineTo(overlap, 2.0, 6.0);
54
79
  appendPathClose(overlap);
55
- const result = createPath('nonZero');
56
- unionAllPaths(result, kernel, [overlap]);
57
- expect(ringCount(result)).toBe(1);
58
- expect(pathArea(result)).toBeCloseTo(28, 6);
80
+ let out = createPath('nonZero');
81
+ unionAllPaths(out, kernel, [overlap], undefined);
82
+ expect(ringCount(out)).toEqual(1);
83
+ expect(Math.abs(pathArea(out) - 28.0) < 5e-7).toBe(true);
59
84
  });
60
-
61
85
  it('merges a list of overlapping squares into one outline', () => {
62
- const result = createPath('nonZero');
63
- unionAllPaths(result, kernel, [squarePath(0, 0, 10), squarePath(5, 5, 10), squarePath(8, 8, 10)]);
64
- expect(ringCount(result)).toBe(1);
65
- expect(pathArea(result)).toBeGreaterThan(100);
66
- expect(pathArea(result)).toBeLessThan(300);
86
+ const kernel = createMartinezPathBooleanKernel();
87
+ const a = squarePath(0.0, 0.0, 10.0);
88
+ const b = squarePath(5.0, 5.0, 10.0);
89
+ const c = squarePath(8.0, 8.0, 10.0);
90
+ let out = createPath('nonZero');
91
+ unionAllPaths(out, kernel, [a, b, c], undefined);
92
+ expect(ringCount(out)).toEqual(1);
93
+ expect(pathArea(out) > 100.0).toBe(true);
94
+ expect(pathArea(out) < 300.0).toBe(true);
67
95
  });
68
-
69
- it('keeps disjoint squares as separate rings summing their areas', () => {
70
- const result = createPath('nonZero');
71
- unionAllPaths(result, kernel, [squarePath(0, 0, 10), squarePath(20, 20, 10), squarePath(40, 0, 10)]);
72
- expect(ringCount(result)).toBe(3);
73
- expect(pathArea(result)).toBeCloseTo(300, 6);
96
+ it('keeps disjoint squares as separate rings', () => {
97
+ const kernel = createMartinezPathBooleanKernel();
98
+ const a = squarePath(0.0, 0.0, 10.0);
99
+ const b = squarePath(20.0, 20.0, 10.0);
100
+ const c = squarePath(40.0, 0.0, 10.0);
101
+ let out = createPath('nonZero');
102
+ unionAllPaths(out, kernel, [a, b, c], undefined);
103
+ expect(ringCount(out)).toEqual(3);
104
+ expect(Math.abs(pathArea(out) - 300.0) < 5e-7).toBe(true);
74
105
  });
75
-
76
- it('folds order-independently (union is commutative across the list)', () => {
77
- const a = squarePath(0, 0, 10);
78
- const b = squarePath(5, 5, 10);
79
- const c = squarePath(2, 8, 10);
80
- const forward = createPath('nonZero');
81
- const reversed = createPath('nonZero');
82
- unionAllPaths(forward, kernel, [a, b, c]);
83
- unionAllPaths(reversed, kernel, [c, b, a]);
84
- expect(pathArea(forward)).toBeCloseTo(pathArea(reversed), 6);
85
- expect(ringCount(forward)).toBe(ringCount(reversed));
106
+ it('folds order independently', () => {
107
+ const kernel = createMartinezPathBooleanKernel();
108
+ const a = squarePath(0.0, 0.0, 10.0);
109
+ const b = squarePath(5.0, 5.0, 10.0);
110
+ const c = squarePath(2.0, 8.0, 10.0);
111
+ let forward = createPath('nonZero');
112
+ let reversed = createPath('nonZero');
113
+ unionAllPaths(forward, kernel, [a, b, c], undefined);
114
+ unionAllPaths(reversed, kernel, [c, b, a], undefined);
115
+ expect(Math.abs(pathArea(forward) - pathArea(reversed)) < 5e-7).toBe(true);
116
+ expect(ringCount(forward)).toEqual(ringCount(reversed));
86
117
  });
87
-
88
- it('writes into a provided out path that aliases an input', () => {
89
- const a = squarePath(0, 0, 10);
90
- const b = squarePath(5, 5, 10);
91
- unionAllPaths(a, kernel, [a, b]);
92
- expect(ringCount(a)).toBe(1);
93
- expect(pathArea(a)).toBeCloseTo(175, 6);
118
+ it('writes into an existing path using a distinct input snapshot', () => {
119
+ const kernel = createMartinezPathBooleanKernel();
120
+ let a = squarePath(0.0, 0.0, 10.0);
121
+ const b = squarePath(5.0, 5.0, 10.0);
122
+ const aClone = a;
123
+ unionAllPaths(a, kernel, [aClone, b], undefined);
124
+ expect(ringCount(a)).toEqual(1);
125
+ expect(Math.abs(pathArea(a) - 175.0) < 5e-7).toBe(true);
94
126
  });
95
127
  });
@@ -1,13 +1,51 @@
1
- import { createPath } from '@flighthq/path/contract';
1
+ // Generated from crates/flight-path-boolean/tests/write_path_boolean_contours_test.rs by flight-compiler.
2
+ // Rust is authoritative; run `npm run generate` after changing it.
3
+
4
+ import { PathCommand, appendPathLineTo, appendPathMoveTo, createPath } from '@flighthq/path/contract';
2
5
  import { describe, expect, it } from 'vitest';
3
6
 
4
- import { writePathBooleanContours } from './writePathBooleanContours.ts';
7
+ import { writePathBooleanContours } from './contract.ts';
5
8
 
6
9
  describe('writePathBooleanContours', () => {
7
10
  it('rebuilds into a supplied path without allocation', () => {
8
- const out = createPath('evenOdd');
9
- writePathBooleanContours(out, [[0, 0, 1, 0, 0, 1]]);
10
- expect(out.winding).toBe('nonZero');
11
- expect(out.commands.length).toBe(4);
11
+ let out = createPath('nonZero');
12
+ out.winding = 'evenOdd';
13
+ writePathBooleanContours(out, [[0.0, 0.0, 1.0, 0.0, 0.0, 1.0]]);
14
+ expect(out.winding).toEqual('nonZero');
15
+ expect(out.commands.length).toEqual(4);
16
+ });
17
+ it('skips rings with fewer than three vertices', () => {
18
+ let out = createPath('nonZero');
19
+ writePathBooleanContours(out, [[0.0, 0.0, 1.0, 1.0]]);
20
+ expect(out.commands.length).toEqual(0);
21
+ expect(out.data.length).toEqual(0);
22
+ });
23
+ it('converts multiple rings to separate sub paths', () => {
24
+ let out = createPath('nonZero');
25
+ writePathBooleanContours(out, [
26
+ [0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0, 10.0],
27
+ [20.0, 20.0, 30.0, 20.0, 30.0, 30.0],
28
+ ]);
29
+ let moveCount = 0;
30
+ for (const command of out.commands) {
31
+ if (command === (PathCommand.MOVE_TO & 0xff)) {
32
+ moveCount += 1;
33
+ }
34
+ }
35
+ let closeCount = 0;
36
+ for (const command of out.commands) {
37
+ if (command === (PathCommand.CLOSE & 0xff)) {
38
+ closeCount += 1;
39
+ }
40
+ }
41
+ expect(moveCount).toEqual(2);
42
+ expect(closeCount).toEqual(2);
43
+ });
44
+ it('clears existing content before writing', () => {
45
+ let out = createPath('nonZero');
46
+ appendPathMoveTo(out, 99.0, 99.0);
47
+ appendPathLineTo(out, 100.0, 100.0);
48
+ writePathBooleanContours(out, [[0.0, 0.0, 1.0, 0.0, 0.0, 1.0]]);
49
+ expect(out.commands.length).toEqual(4);
12
50
  });
13
51
  });