@flighthq/path-boolean 0.5.1-next.3302.76ece6b → 0.5.1-next.4304.322dc96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/.tsbuildinfo.contract +1 -0
- package/dist/.tsbuildinfo.runtime +1 -0
- package/dist/booleanPaths.d.ts +33 -6
- package/dist/booleanPaths.d.ts.map +1 -1
- package/dist/booleanPaths.js +42 -3
- package/dist/booleanPaths.js.map +1 -1
- package/dist/contract.d.ts +12 -8
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +11 -8
- package/dist/contract.js.map +1 -1
- package/dist/contract.path-boolean-types.d.ts +4 -0
- package/dist/contract.path-boolean-types.d.ts.map +1 -0
- package/dist/contract.path-boolean-types.js +2 -0
- package/dist/contract.path-boolean-types.js.map +1 -0
- package/dist/explainPathBoolean.d.ts +15 -3
- package/dist/explainPathBoolean.d.ts.map +1 -1
- package/dist/explainPathBoolean.js +35 -9
- package/dist/explainPathBoolean.js.map +1 -1
- package/dist/index.d.ts +3 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -8
- package/dist/index.js.map +1 -1
- package/dist/martinezKernel.d.ts +44 -4
- package/dist/martinezKernel.d.ts.map +1 -1
- package/dist/martinezKernel.js +777 -559
- package/dist/martinezKernel.js.map +1 -1
- package/dist/offsetPath.d.ts +15 -2
- package/dist/offsetPath.d.ts.map +1 -1
- package/dist/offsetPath.js +205 -215
- package/dist/offsetPath.js.map +1 -1
- package/dist/pathBoolean.d.ts +53 -0
- package/dist/pathBoolean.d.ts.map +1 -0
- package/dist/pathBoolean.js +115 -0
- package/dist/pathBoolean.js.map +1 -0
- package/dist/pathBooleanCompat.d.ts +3 -0
- package/dist/pathBooleanCompat.d.ts.map +1 -0
- package/dist/pathBooleanCompat.js +2 -0
- package/dist/pathBooleanCompat.js.map +1 -0
- package/dist/resolvePathRegions.d.ts +8 -2
- package/dist/resolvePathRegions.d.ts.map +1 -1
- package/dist/resolvePathRegions.js +11 -3
- package/dist/resolvePathRegions.js.map +1 -1
- package/dist/simplifyPath.d.ts +9 -2
- package/dist/simplifyPath.d.ts.map +1 -1
- package/dist/simplifyPath.js +12 -2
- package/dist/simplifyPath.js.map +1 -1
- package/dist/unionAllPaths.d.ts +9 -2
- package/dist/unionAllPaths.d.ts.map +1 -1
- package/dist/unionAllPaths.js +23 -8
- package/dist/unionAllPaths.js.map +1 -1
- package/dist/writePathBooleanContours.d.ts +8 -2
- package/dist/writePathBooleanContours.d.ts.map +1 -1
- package/dist/writePathBooleanContours.js +16 -4
- package/dist/writePathBooleanContours.js.map +1 -1
- package/package.json +5 -4
- package/src/booleanPaths.test.ts +237 -114
- package/src/explainPathBoolean.test.ts +35 -21
- package/src/fuzzInvariants.test.ts +241 -154
- package/src/martinezKernel.test.ts +334 -328
- package/src/offsetPath.test.ts +296 -166
- package/src/pathBoolean.test.ts +95 -0
- package/src/resolvePathRegions.test.ts +41 -30
- package/src/simplifyPath.test.ts +143 -112
- package/src/unionAllPaths.test.ts +93 -61
- package/src/writePathBooleanContours.test.ts +44 -6
|
@@ -1,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/
|
|
5
|
+
import type { Path } from '@flighthq/path/contract';
|
|
3
6
|
import { describe, expect, it } from 'vitest';
|
|
4
7
|
|
|
5
|
-
import {
|
|
6
|
-
import { resolvePathRegions } from './resolvePathRegions.ts';
|
|
8
|
+
import { createMartinezPathBooleanKernel, resolvePathRegions } from './contract.ts';
|
|
7
9
|
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
expect(
|
|
34
|
-
expect(
|
|
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
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
});
|
package/src/simplifyPath.test.ts
CHANGED
|
@@ -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
|
|
5
|
+
import type { Path } from '@flighthq/path/contract';
|
|
3
6
|
import { describe, expect, it } from 'vitest';
|
|
4
7
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
-
|
|
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:
|
|
11
|
-
|
|
20
|
+
function polygonPath(vertices: Readonly<number[]>): Path {
|
|
21
|
+
let path = createPath('nonZero');
|
|
12
22
|
appendPathMoveTo(path, vertices[0], vertices[1]);
|
|
13
|
-
|
|
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
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for (let i = 0; i <
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
44
|
+
return Math.abs(total);
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
function
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
70
|
+
return [minX, minY, maxX, maxY];
|
|
43
71
|
}
|
|
44
72
|
|
|
45
73
|
function ringCount(path: Readonly<Path>): number {
|
|
46
|
-
return
|
|
74
|
+
return flattenedPath(path, 0.25).length;
|
|
47
75
|
}
|
|
48
76
|
|
|
49
|
-
function pentagramPath(radius: number
|
|
77
|
+
function pentagramPath(radius: number): Path {
|
|
50
78
|
const order = [0, 2, 4, 1, 3];
|
|
51
|
-
|
|
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)
|
|
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
|
|
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
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
expect(
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
simplifyPath(
|
|
75
|
-
expect(ringCount(
|
|
76
|
-
expect(pathArea(
|
|
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
|
-
|
|
80
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
expect(
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
appendPathLineTo(path, 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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
expect(
|
|
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
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
expect(ringCount(
|
|
123
|
-
expect(pathArea(
|
|
124
|
-
expect(
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
simplifyPath(
|
|
130
|
-
expect(ringCount(
|
|
131
|
-
expect(pathArea(
|
|
132
|
-
const
|
|
133
|
-
expect(
|
|
134
|
-
expect(
|
|
135
|
-
expect(
|
|
136
|
-
expect(
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
expect(
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
simplifyPath(
|
|
149
|
-
expect(
|
|
150
|
-
expect(
|
|
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
|
-
|
|
2
|
-
|
|
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 {
|
|
6
|
-
import { unionAllPaths } from './unionAllPaths.ts';
|
|
15
|
+
import { createMartinezPathBooleanKernel, unionAllPaths } from './contract.ts';
|
|
7
16
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect(
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
56
|
-
unionAllPaths(
|
|
57
|
-
expect(ringCount(
|
|
58
|
-
expect(pathArea(
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
const a = squarePath(0, 0, 10);
|
|
78
|
-
const b = squarePath(5, 5, 10);
|
|
79
|
-
const c = squarePath(2, 8, 10);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
unionAllPaths(forward, kernel, [a, b, c]);
|
|
83
|
-
unionAllPaths(reversed, kernel, [c, b, a]);
|
|
84
|
-
expect(pathArea(forward)
|
|
85
|
-
expect(ringCount(forward)).
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
const b = squarePath(5, 5, 10);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(
|
|
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
|
-
|
|
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 './
|
|
7
|
+
import { writePathBooleanContours } from './contract.ts';
|
|
5
8
|
|
|
6
9
|
describe('writePathBooleanContours', () => {
|
|
7
10
|
it('rebuilds into a supplied path without allocation', () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
expect(out.
|
|
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
|
});
|