@flighthq/path 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e
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/package.json +3 -3
- package/src/tessellateStrokePath.test.ts +96 -0
package/README.md
CHANGED
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/path`. The `@flighth
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/path@0.5.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/path@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/path)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/path",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-edge.638.d4dbd4e",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/math": "0.5.
|
|
42
|
-
"@flighthq/types": "0.5.
|
|
41
|
+
"@flighthq/math": "0.5.1-edge.638.d4dbd4e",
|
|
42
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"typescript": "^5.3.0"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { StrokeStyle } from '@flighthq/types/contract';
|
|
2
|
+
|
|
1
3
|
import { appendPathClose, appendPathLineTo, appendPathMoveTo, appendPathRectangle, createPath } from './path';
|
|
2
4
|
import { tessellateStrokePath } from './tessellateStrokePath';
|
|
3
5
|
|
|
@@ -63,6 +65,81 @@ describe('tessellateStrokePath', () => {
|
|
|
63
65
|
expect(tessellateStrokePath(path, { width: 0 })).toBeNull();
|
|
64
66
|
expect(tessellateStrokePath(path, { width: Number.NaN })).toBeNull();
|
|
65
67
|
});
|
|
68
|
+
|
|
69
|
+
// The sharing contract on StrokeTessellator, which exists because a derived render pipeline
|
|
70
|
+
// INHERITS the tessellator bound to its source — one function object serves every state in that
|
|
71
|
+
// lineage. Each probe below asserts a property nothing else in this suite covers.
|
|
72
|
+
//
|
|
73
|
+
// Every one of them is grounded in a mesh asserted NONEMPTY first. A degenerate path returns an
|
|
74
|
+
// empty mesh, and against an empty mesh all four probes pass while proving nothing: two empty
|
|
75
|
+
// results always match, always have distinct empty arrays, and never disturb each other.
|
|
76
|
+
|
|
77
|
+
it('returns a nonempty mesh for the fixtures the sharing probes rely on', () => {
|
|
78
|
+
expect(tessellateStrokePath(makeOpenPath(), OPEN_STYLE)!.vertices.length).toBeGreaterThan(0);
|
|
79
|
+
expect(tessellateStrokePath(makeClosedPath(), CLOSED_STYLE, 0.1)!.vertices.length).toBeGreaterThan(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns identical geometry when calls for two paths are interleaved', () => {
|
|
83
|
+
const openBaseline = JSON.stringify(tessellateStrokePath(makeOpenPath(), OPEN_STYLE));
|
|
84
|
+
const closedBaseline = JSON.stringify(tessellateStrokePath(makeClosedPath(), CLOSED_STYLE, 0.1));
|
|
85
|
+
let drifted = 0;
|
|
86
|
+
for (let i = 0; i < 50; i++) {
|
|
87
|
+
if (JSON.stringify(tessellateStrokePath(makeOpenPath(), OPEN_STYLE)) !== openBaseline) drifted++;
|
|
88
|
+
if (JSON.stringify(tessellateStrokePath(makeClosedPath(), CLOSED_STYLE, 0.1)) !== closedBaseline) drifted++;
|
|
89
|
+
}
|
|
90
|
+
expect(drifted).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('allocates fresh result arrays for every call', () => {
|
|
94
|
+
const first = tessellateStrokePath(makeOpenPath(), OPEN_STYLE)!;
|
|
95
|
+
const second = tessellateStrokePath(makeOpenPath(), OPEN_STYLE)!;
|
|
96
|
+
expect(first.vertices).not.toBe(second.vertices);
|
|
97
|
+
expect(first.indices).not.toBe(second.indices);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('is unaffected by a caller mutating an earlier result', () => {
|
|
101
|
+
const baseline = JSON.stringify(tessellateStrokePath(makeOpenPath(), OPEN_STYLE));
|
|
102
|
+
const owned = tessellateStrokePath(makeOpenPath(), OPEN_STYLE)!;
|
|
103
|
+
owned.vertices.length = 0;
|
|
104
|
+
owned.indices.push(999_999);
|
|
105
|
+
expect(JSON.stringify(tessellateStrokePath(makeOpenPath(), OPEN_STYLE))).toBe(baseline);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('is reentrant when a nested call runs inside an in-progress one', () => {
|
|
109
|
+
const openBaseline = JSON.stringify(tessellateStrokePath(makeOpenPath(), OPEN_STYLE));
|
|
110
|
+
const closedBaseline = JSON.stringify(tessellateStrokePath(makeClosedPath(), CLOSED_STYLE, 0.1));
|
|
111
|
+
let nestedRan = false;
|
|
112
|
+
let nestedMatched = false;
|
|
113
|
+
// Reading `width` mid-tessellation triggers the inner call, so the two are genuinely interleaved
|
|
114
|
+
// rather than merely sequential.
|
|
115
|
+
const reentrantStyle = new Proxy(
|
|
116
|
+
{ ...OPEN_STYLE },
|
|
117
|
+
{
|
|
118
|
+
get(target, key) {
|
|
119
|
+
if (key === 'width' && !nestedRan) {
|
|
120
|
+
nestedRan = true;
|
|
121
|
+
nestedMatched =
|
|
122
|
+
JSON.stringify(tessellateStrokePath(makeClosedPath(), CLOSED_STYLE, 0.1)) === closedBaseline;
|
|
123
|
+
}
|
|
124
|
+
return (target as Record<string | symbol, unknown>)[key];
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
) as StrokeStyle;
|
|
128
|
+
const outer = JSON.stringify(tessellateStrokePath(makeOpenPath(), reentrantStyle));
|
|
129
|
+
expect(nestedRan).toBe(true);
|
|
130
|
+
expect(nestedMatched).toBe(true);
|
|
131
|
+
expect(outer).toBe(openBaseline);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('does not mutate the path or style it is given', () => {
|
|
135
|
+
const path = makeOpenPath();
|
|
136
|
+
const style = { ...OPEN_STYLE };
|
|
137
|
+
const pathBefore = JSON.stringify(path);
|
|
138
|
+
const styleBefore = JSON.stringify(style);
|
|
139
|
+
tessellateStrokePath(path, style);
|
|
140
|
+
expect(JSON.stringify(path)).toBe(pathBefore);
|
|
141
|
+
expect(JSON.stringify(style)).toBe(styleBefore);
|
|
142
|
+
});
|
|
66
143
|
});
|
|
67
144
|
|
|
68
145
|
function bowTie() {
|
|
@@ -89,3 +166,22 @@ function getMeshArea(mesh: { indices: readonly number[]; vertices: readonly numb
|
|
|
89
166
|
}
|
|
90
167
|
return area;
|
|
91
168
|
}
|
|
169
|
+
|
|
170
|
+
// Fixtures for the sharing-contract probes: one open polyline and one closed ring, both chosen because
|
|
171
|
+
// they produce real geometry (12 and 80 vertices).
|
|
172
|
+
const OPEN_STYLE: StrokeStyle = { cap: 'butt', join: 'miter', width: 10 };
|
|
173
|
+
const CLOSED_STYLE: StrokeStyle = { join: 'round', width: 7 };
|
|
174
|
+
|
|
175
|
+
function makeOpenPath(): ReturnType<typeof createPath> {
|
|
176
|
+
const path = createPath();
|
|
177
|
+
appendPathMoveTo(path, 0, 0);
|
|
178
|
+
appendPathLineTo(path, 100, 0);
|
|
179
|
+
appendPathLineTo(path, 100, 100);
|
|
180
|
+
return path;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function makeClosedPath(): ReturnType<typeof createPath> {
|
|
184
|
+
const path = createPath();
|
|
185
|
+
appendPathRectangle(path, 0, 0, 100, 50);
|
|
186
|
+
return path;
|
|
187
|
+
}
|