@flighthq/path 0.1.0
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/dist/containsPathPoint.d.ts +13 -0
- package/dist/containsPathPoint.d.ts.map +1 -0
- package/dist/containsPathPoint.js +194 -0
- package/dist/containsPathPoint.js.map +1 -0
- package/dist/copyPath.d.ts +4 -0
- package/dist/copyPath.d.ts.map +1 -0
- package/dist/copyPath.js +28 -0
- package/dist/copyPath.js.map +1 -0
- package/dist/dashPath.d.ts +3 -0
- package/dist/dashPath.d.ts.map +1 -0
- package/dist/dashPath.js +105 -0
- package/dist/dashPath.js.map +1 -0
- package/dist/fitPathCurves.d.ts +3 -0
- package/dist/fitPathCurves.d.ts.map +1 -0
- package/dist/fitPathCurves.js +222 -0
- package/dist/fitPathCurves.js.map +1 -0
- package/dist/flattenPath.d.ts +3 -0
- package/dist/flattenPath.d.ts.map +1 -0
- package/dist/flattenPath.js +145 -0
- package/dist/flattenPath.js.map +1 -0
- package/dist/forEachPathSegment.d.ts +3 -0
- package/dist/forEachPathSegment.d.ts.map +1 -0
- package/dist/forEachPathSegment.js +66 -0
- package/dist/forEachPathSegment.js.map +1 -0
- package/dist/getPathBounds.d.ts +3 -0
- package/dist/getPathBounds.d.ts.map +1 -0
- package/dist/getPathBounds.js +167 -0
- package/dist/getPathBounds.js.map +1 -0
- package/dist/getPathContourLengths.d.ts +3 -0
- package/dist/getPathContourLengths.d.ts.map +1 -0
- package/dist/getPathContourLengths.js +21 -0
- package/dist/getPathContourLengths.js.map +1 -0
- package/dist/getPathLength.d.ts +3 -0
- package/dist/getPathLength.d.ts.map +1 -0
- package/dist/getPathLength.js +23 -0
- package/dist/getPathLength.js.map +1 -0
- package/dist/getPathNearestPoint.d.ts +3 -0
- package/dist/getPathNearestPoint.d.ts.map +1 -0
- package/dist/getPathNearestPoint.js +47 -0
- package/dist/getPathNearestPoint.js.map +1 -0
- package/dist/getPathPointAtDistance.d.ts +5 -0
- package/dist/getPathPointAtDistance.d.ts.map +1 -0
- package/dist/getPathPointAtDistance.js +100 -0
- package/dist/getPathPointAtDistance.js.map +1 -0
- package/dist/getPathSegmentAtParameter.d.ts +8 -0
- package/dist/getPathSegmentAtParameter.d.ts.map +1 -0
- package/dist/getPathSegmentAtParameter.js +163 -0
- package/dist/getPathSegmentAtParameter.js.map +1 -0
- package/dist/getPathSignedArea.d.ts +4 -0
- package/dist/getPathSignedArea.d.ts.map +1 -0
- package/dist/getPathSignedArea.js +43 -0
- package/dist/getPathSignedArea.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/offsetPath.d.ts +3 -0
- package/dist/offsetPath.d.ts.map +1 -0
- package/dist/offsetPath.js +146 -0
- package/dist/offsetPath.js.map +1 -0
- package/dist/path.d.ts +17 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +293 -0
- package/dist/path.js.map +1 -0
- package/dist/pathMeshPool.d.ts +6 -0
- package/dist/pathMeshPool.d.ts.map +1 -0
- package/dist/pathMeshPool.js +58 -0
- package/dist/pathMeshPool.js.map +1 -0
- package/dist/reversePath.d.ts +3 -0
- package/dist/reversePath.d.ts.map +1 -0
- package/dist/reversePath.js +95 -0
- package/dist/reversePath.js.map +1 -0
- package/dist/simplifyPath.d.ts +3 -0
- package/dist/simplifyPath.d.ts.map +1 -0
- package/dist/simplifyPath.js +78 -0
- package/dist/simplifyPath.js.map +1 -0
- package/dist/strokePath.d.ts +4 -0
- package/dist/strokePath.d.ts.map +1 -0
- package/dist/strokePath.js +458 -0
- package/dist/strokePath.js.map +1 -0
- package/dist/tessellatePath.d.ts +3 -0
- package/dist/tessellatePath.d.ts.map +1 -0
- package/dist/tessellatePath.js +107 -0
- package/dist/tessellatePath.js.map +1 -0
- package/dist/tessellatePathTyped.d.ts +3 -0
- package/dist/tessellatePathTyped.d.ts.map +1 -0
- package/dist/tessellatePathTyped.js +12 -0
- package/dist/tessellatePathTyped.js.map +1 -0
- package/dist/transformPath.d.ts +4 -0
- package/dist/transformPath.d.ts.map +1 -0
- package/dist/transformPath.js +62 -0
- package/dist/transformPath.js.map +1 -0
- package/package.json +37 -0
- package/src/containsPathPoint.test.ts +125 -0
- package/src/copyPath.test.ts +56 -0
- package/src/dashPath.test.ts +76 -0
- package/src/fitPathCurves.test.ts +65 -0
- package/src/flattenPath.test.ts +87 -0
- package/src/forEachPathSegment.test.ts +82 -0
- package/src/getPathBounds.test.ts +84 -0
- package/src/getPathContourLengths.test.ts +51 -0
- package/src/getPathLength.test.ts +33 -0
- package/src/getPathNearestPoint.test.ts +64 -0
- package/src/getPathPointAtDistance.test.ts +94 -0
- package/src/getPathSegmentAtParameter.test.ts +178 -0
- package/src/getPathSignedArea.test.ts +45 -0
- package/src/offsetPath.test.ts +78 -0
- package/src/path.test.ts +376 -0
- package/src/pathMeshPool.test.ts +78 -0
- package/src/reversePath.test.ts +51 -0
- package/src/simplifyPath.test.ts +69 -0
- package/src/strokePath.test.ts +150 -0
- package/src/tessellatePath.test.ts +62 -0
- package/src/tessellatePathTyped.test.ts +26 -0
- package/src/transformPath.test.ts +79 -0
package/src/path.test.ts
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { PathCommand } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
appendPathArc,
|
|
5
|
+
appendPathArcTo,
|
|
6
|
+
appendPathCircle,
|
|
7
|
+
appendPathClose,
|
|
8
|
+
appendPathCubicCurveTo,
|
|
9
|
+
appendPathCurveTo,
|
|
10
|
+
appendPathEllipse,
|
|
11
|
+
appendPathLineTo,
|
|
12
|
+
appendPathMoveTo,
|
|
13
|
+
appendPathPolygon,
|
|
14
|
+
appendPathPolyline,
|
|
15
|
+
appendPathRectangle,
|
|
16
|
+
appendPathRoundRectangle,
|
|
17
|
+
createPath,
|
|
18
|
+
getPathLastPoint,
|
|
19
|
+
} from './path';
|
|
20
|
+
|
|
21
|
+
describe('appendPathArc', () => {
|
|
22
|
+
it('produces a MOVE_TO and at least one CUBIC_CURVE_TO for a non-zero arc', () => {
|
|
23
|
+
const path = createPath();
|
|
24
|
+
appendPathArc(path, 0, 0, 50, 0, Math.PI / 2);
|
|
25
|
+
expect(path.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
26
|
+
expect(path.commands).toContain(PathCommand.CUBIC_CURVE_TO);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('starts at the correct point on the circle (angle=0 → rightmost)', () => {
|
|
30
|
+
const path = createPath();
|
|
31
|
+
appendPathArc(path, 10, 20, 30, 0, Math.PI / 2);
|
|
32
|
+
// MOVE_TO at (cx + radius*cos(0), cy + radius*sin(0)) = (40, 20)
|
|
33
|
+
expect(path.data[0]).toBeCloseTo(40);
|
|
34
|
+
expect(path.data[1]).toBeCloseTo(20);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('ends at the correct point for a quarter circle', () => {
|
|
38
|
+
const path = createPath();
|
|
39
|
+
appendPathArc(path, 0, 0, 100, 0, Math.PI / 2);
|
|
40
|
+
// The last CUBIC_CURVE_TO anchor should be near (cos(π/2)*100, sin(π/2)*100) = (0, 100)
|
|
41
|
+
const data = path.data;
|
|
42
|
+
expect(data[data.length - 2]).toBeCloseTo(0, 1);
|
|
43
|
+
expect(data[data.length - 1]).toBeCloseTo(100, 1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('supports anticlockwise arcs', () => {
|
|
47
|
+
const pathCW = createPath();
|
|
48
|
+
appendPathArc(pathCW, 0, 0, 50, 0, Math.PI, false);
|
|
49
|
+
const pathCCW = createPath();
|
|
50
|
+
appendPathArc(pathCCW, 0, 0, 50, 0, Math.PI, true);
|
|
51
|
+
// Both arcs should contain cubic curves but the y-extremes differ.
|
|
52
|
+
expect(pathCW.commands).toContain(PathCommand.CUBIC_CURVE_TO);
|
|
53
|
+
expect(pathCCW.commands).toContain(PathCommand.CUBIC_CURVE_TO);
|
|
54
|
+
// CW goes through negative y; CCW goes through positive y (in screen coords).
|
|
55
|
+
const cwMaxY = Math.max(...pathCW.data.filter((_, i) => i % 2 === 1));
|
|
56
|
+
const ccwMinY = Math.min(...pathCCW.data.filter((_, i) => i % 2 === 1));
|
|
57
|
+
expect(cwMaxY).toBeGreaterThan(0);
|
|
58
|
+
expect(ccwMinY).toBeLessThan(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('connectToCurrent=true appends a LINE_TO instead of MOVE_TO', () => {
|
|
62
|
+
const path = createPath();
|
|
63
|
+
appendPathMoveTo(path, 0, 0);
|
|
64
|
+
appendPathArc(path, 100, 0, 50, Math.PI, 0, false, true);
|
|
65
|
+
expect(path.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
66
|
+
expect(path.commands[1]).toBe(PathCommand.LINE_TO);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('appends nothing for zero radius', () => {
|
|
70
|
+
const path = createPath();
|
|
71
|
+
appendPathArc(path, 0, 0, 0, 0, Math.PI);
|
|
72
|
+
expect(path.commands.length).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('appendPathArcTo', () => {
|
|
77
|
+
it('appends a LINE_TO when radiusX is 0', () => {
|
|
78
|
+
const path = createPath();
|
|
79
|
+
appendPathMoveTo(path, 0, 0);
|
|
80
|
+
appendPathArcTo(path, 0, 10, 0, false, false, 50, 50);
|
|
81
|
+
expect(path.commands[1]).toBe(PathCommand.LINE_TO);
|
|
82
|
+
expect(path.data[path.data.length - 2]).toBeCloseTo(50);
|
|
83
|
+
expect(path.data[path.data.length - 1]).toBeCloseTo(50);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does nothing when start equals end', () => {
|
|
87
|
+
const path = createPath();
|
|
88
|
+
appendPathMoveTo(path, 10, 10);
|
|
89
|
+
const beforeLen = path.commands.length;
|
|
90
|
+
appendPathArcTo(path, 50, 50, 0, false, false, 10, 10);
|
|
91
|
+
expect(path.commands.length).toBe(beforeLen);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('produces cubic curves for a non-degenerate arc', () => {
|
|
95
|
+
const path = createPath();
|
|
96
|
+
appendPathMoveTo(path, 100, 0);
|
|
97
|
+
// Full circle: from (100,0) back to (100,0) via a large arc
|
|
98
|
+
appendPathArcTo(path, 50, 50, 0, true, false, 100, 1);
|
|
99
|
+
expect(path.commands).toContain(PathCommand.CUBIC_CURVE_TO);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('ends at the specified endpoint (non-rotated, unit circle)', () => {
|
|
103
|
+
// Quarter circle from (1,0) to (0,1) using a radius-1 circle centered at origin.
|
|
104
|
+
const path = createPath();
|
|
105
|
+
appendPathMoveTo(path, 1, 0);
|
|
106
|
+
appendPathArcTo(path, 1, 1, 0, false, true, 0, 1);
|
|
107
|
+
const data = path.data;
|
|
108
|
+
// Last anchor point of the last cubic should be close to (0,1).
|
|
109
|
+
expect(data[data.length - 2]).toBeCloseTo(0, 1);
|
|
110
|
+
expect(data[data.length - 1]).toBeCloseTo(1, 1);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('appendPathCircle', () => {
|
|
115
|
+
it('produces a closed path with a MOVE_TO, four CUBIC_CURVE_TO, and a CLOSE', () => {
|
|
116
|
+
const path = createPath();
|
|
117
|
+
appendPathCircle(path, 0, 0, 50);
|
|
118
|
+
expect(path.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
119
|
+
expect(path.commands[path.commands.length - 1]).toBe(PathCommand.CLOSE);
|
|
120
|
+
expect(path.commands.filter((c) => c === PathCommand.CUBIC_CURVE_TO).length).toBe(4);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('starts at the rightmost point of the circle', () => {
|
|
124
|
+
const path = createPath();
|
|
125
|
+
appendPathCircle(path, 10, 20, 30);
|
|
126
|
+
// MOVE_TO at (cx+radius, cy) = (40, 20)
|
|
127
|
+
expect(path.data[0]).toBeCloseTo(40);
|
|
128
|
+
expect(path.data[1]).toBeCloseTo(20);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('appendPathClose', () => {
|
|
133
|
+
it('appends a CLOSE command and no data', () => {
|
|
134
|
+
const path = createPath();
|
|
135
|
+
appendPathMoveTo(path, 0, 0);
|
|
136
|
+
appendPathClose(path);
|
|
137
|
+
expect(path.commands).toStrictEqual([PathCommand.MOVE_TO, PathCommand.CLOSE]);
|
|
138
|
+
expect(path.data).toStrictEqual([0, 0]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('appendPathCubicCurveTo', () => {
|
|
143
|
+
it('pushes the cubic verb and its six coordinates', () => {
|
|
144
|
+
const path = createPath();
|
|
145
|
+
appendPathCubicCurveTo(path, 1, 2, 3, 4, 5, 6);
|
|
146
|
+
expect(path.commands).toStrictEqual([PathCommand.CUBIC_CURVE_TO]);
|
|
147
|
+
expect(path.data).toStrictEqual([1, 2, 3, 4, 5, 6]);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('appendPathCurveTo', () => {
|
|
152
|
+
it('pushes the quadratic verb and its four coordinates', () => {
|
|
153
|
+
const path = createPath();
|
|
154
|
+
appendPathCurveTo(path, 1, 2, 3, 4);
|
|
155
|
+
expect(path.commands).toStrictEqual([PathCommand.CURVE_TO]);
|
|
156
|
+
expect(path.data).toStrictEqual([1, 2, 3, 4]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('appendPathEllipse', () => {
|
|
161
|
+
it('produces a closed path with four cubic segments', () => {
|
|
162
|
+
const path = createPath();
|
|
163
|
+
appendPathEllipse(path, 0, 0, 100, 50);
|
|
164
|
+
expect(path.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
165
|
+
expect(path.commands[path.commands.length - 1]).toBe(PathCommand.CLOSE);
|
|
166
|
+
expect(path.commands.filter((c) => c === PathCommand.CUBIC_CURVE_TO).length).toBe(4);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('starts at (cx+radiusX, cy)', () => {
|
|
170
|
+
const path = createPath();
|
|
171
|
+
appendPathEllipse(path, 10, 20, 100, 50);
|
|
172
|
+
expect(path.data[0]).toBeCloseTo(110);
|
|
173
|
+
expect(path.data[1]).toBeCloseTo(20);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('appendPathLineTo', () => {
|
|
178
|
+
it('pushes the line verb and its point', () => {
|
|
179
|
+
const path = createPath();
|
|
180
|
+
appendPathLineTo(path, 7, 8);
|
|
181
|
+
expect(path.commands).toStrictEqual([PathCommand.LINE_TO]);
|
|
182
|
+
expect(path.data).toStrictEqual([7, 8]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('appendPathMoveTo', () => {
|
|
187
|
+
it('pushes the move verb and its point', () => {
|
|
188
|
+
const path = createPath();
|
|
189
|
+
appendPathMoveTo(path, 9, 10);
|
|
190
|
+
expect(path.commands).toStrictEqual([PathCommand.MOVE_TO]);
|
|
191
|
+
expect(path.data).toStrictEqual([9, 10]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('appendPathPolygon', () => {
|
|
196
|
+
it('appends a closed polygon from a flat coordinate array', () => {
|
|
197
|
+
const path = createPath();
|
|
198
|
+
appendPathPolygon(path, [0, 0, 100, 0, 100, 100]);
|
|
199
|
+
expect(path.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
200
|
+
expect(path.commands[path.commands.length - 1]).toBe(PathCommand.CLOSE);
|
|
201
|
+
expect(path.data[0]).toBe(0);
|
|
202
|
+
expect(path.data[1]).toBe(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('does nothing when fewer than 3 points are provided', () => {
|
|
206
|
+
const path = createPath();
|
|
207
|
+
appendPathPolygon(path, [0, 0, 10, 10]);
|
|
208
|
+
expect(path.commands.length).toBe(0);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('appendPathPolyline', () => {
|
|
213
|
+
it('appends an open polyline with no CLOSE', () => {
|
|
214
|
+
const path = createPath();
|
|
215
|
+
appendPathPolyline(path, [0, 0, 10, 0, 10, 10]);
|
|
216
|
+
expect(path.commands).toStrictEqual([PathCommand.MOVE_TO, PathCommand.LINE_TO, PathCommand.LINE_TO]);
|
|
217
|
+
expect(path.commands).not.toContain(PathCommand.CLOSE);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('does nothing when fewer than 2 points are provided', () => {
|
|
221
|
+
const path = createPath();
|
|
222
|
+
appendPathPolyline(path, [0, 0]);
|
|
223
|
+
expect(path.commands.length).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('appendPathRectangle', () => {
|
|
228
|
+
it('produces a closed rectangle with four corners', () => {
|
|
229
|
+
const path = createPath();
|
|
230
|
+
appendPathRectangle(path, 10, 20, 100, 50);
|
|
231
|
+
expect(path.commands).toStrictEqual([
|
|
232
|
+
PathCommand.MOVE_TO,
|
|
233
|
+
PathCommand.LINE_TO,
|
|
234
|
+
PathCommand.LINE_TO,
|
|
235
|
+
PathCommand.LINE_TO,
|
|
236
|
+
PathCommand.CLOSE,
|
|
237
|
+
]);
|
|
238
|
+
expect(path.data).toStrictEqual([10, 20, 110, 20, 110, 70, 10, 70]);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('appendPathRoundRectangle', () => {
|
|
243
|
+
it('produces a closed path for a uniformly rounded rectangle', () => {
|
|
244
|
+
const path = createPath();
|
|
245
|
+
appendPathRoundRectangle(path, 0, 0, 100, 50, 10);
|
|
246
|
+
expect(path.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
247
|
+
expect(path.commands[path.commands.length - 1]).toBe(PathCommand.CLOSE);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('contains cubic curves for the corner arcs', () => {
|
|
251
|
+
const path = createPath();
|
|
252
|
+
appendPathRoundRectangle(path, 0, 0, 100, 50, 10);
|
|
253
|
+
expect(path.commands).toContain(PathCommand.CUBIC_CURVE_TO);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('produces a rectangle (no arcs) when radius is 0', () => {
|
|
257
|
+
const round = createPath();
|
|
258
|
+
appendPathRoundRectangle(round, 0, 0, 100, 50, 0);
|
|
259
|
+
// No arcs when radius is 0.
|
|
260
|
+
expect(round.commands).not.toContain(PathCommand.CUBIC_CURVE_TO);
|
|
261
|
+
// Must start with MOVE_TO and end with CLOSE.
|
|
262
|
+
expect(round.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
263
|
+
expect(round.commands[round.commands.length - 1]).toBe(PathCommand.CLOSE);
|
|
264
|
+
// All four corners should appear in the data.
|
|
265
|
+
const xs = round.data.filter((_, i) => i % 2 === 0);
|
|
266
|
+
const ys = round.data.filter((_, i) => i % 2 === 1);
|
|
267
|
+
expect(Math.min(...xs)).toBeCloseTo(0);
|
|
268
|
+
expect(Math.max(...xs)).toBeCloseTo(100);
|
|
269
|
+
expect(Math.min(...ys)).toBeCloseTo(0);
|
|
270
|
+
expect(Math.max(...ys)).toBeCloseTo(50);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('accepts per-corner radii as a 4-tuple', () => {
|
|
274
|
+
const path = createPath();
|
|
275
|
+
appendPathRoundRectangle(path, 0, 0, 100, 100, [10, 20, 30, 40]);
|
|
276
|
+
expect(path.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
277
|
+
expect(path.commands[path.commands.length - 1]).toBe(PathCommand.CLOSE);
|
|
278
|
+
expect(path.commands).toContain(PathCommand.CUBIC_CURVE_TO);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('clamps oversized radii to half the edge length', () => {
|
|
282
|
+
const path = createPath();
|
|
283
|
+
// Radius of 1000 on a 40×20 rectangle should be clamped to min(40/2, 20/2) = 10.
|
|
284
|
+
appendPathRoundRectangle(path, 0, 0, 40, 20, 1000);
|
|
285
|
+
// Should not throw and should produce a valid closed path.
|
|
286
|
+
expect(path.commands[path.commands.length - 1]).toBe(PathCommand.CLOSE);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('createPath', () => {
|
|
291
|
+
it('defaults to an empty nonZero path', () => {
|
|
292
|
+
const path = createPath();
|
|
293
|
+
expect(path.commands).toStrictEqual([]);
|
|
294
|
+
expect(path.data).toStrictEqual([]);
|
|
295
|
+
expect(path.winding).toStrictEqual('nonZero');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('accepts an evenOdd winding', () => {
|
|
299
|
+
expect(createPath('evenOdd').winding).toStrictEqual('evenOdd');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('getPathLastPoint', () => {
|
|
304
|
+
it('returns null for an empty path', () => {
|
|
305
|
+
expect(getPathLastPoint(createPath())).toBeNull();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('returns the point after appendPathMoveTo', () => {
|
|
309
|
+
const path = createPath();
|
|
310
|
+
appendPathMoveTo(path, 10, 20);
|
|
311
|
+
expect(getPathLastPoint(path)).toStrictEqual([10, 20]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('returns the point after appendPathLineTo', () => {
|
|
315
|
+
const path = createPath();
|
|
316
|
+
appendPathMoveTo(path, 0, 0);
|
|
317
|
+
appendPathLineTo(path, 30, 40);
|
|
318
|
+
expect(getPathLastPoint(path)).toStrictEqual([30, 40]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('returns the anchor after appendPathCurveTo', () => {
|
|
322
|
+
const path = createPath();
|
|
323
|
+
appendPathMoveTo(path, 0, 0);
|
|
324
|
+
appendPathCurveTo(path, 5, 5, 50, 60);
|
|
325
|
+
expect(getPathLastPoint(path)).toStrictEqual([50, 60]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('returns the anchor after appendPathCubicCurveTo', () => {
|
|
329
|
+
const path = createPath();
|
|
330
|
+
appendPathMoveTo(path, 0, 0);
|
|
331
|
+
appendPathCubicCurveTo(path, 1, 2, 3, 4, 70, 80);
|
|
332
|
+
expect(getPathLastPoint(path)).toStrictEqual([70, 80]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('returns the last anchor after appendPathClose', () => {
|
|
336
|
+
const path = createPath();
|
|
337
|
+
appendPathMoveTo(path, 10, 20);
|
|
338
|
+
appendPathLineTo(path, 30, 40);
|
|
339
|
+
appendPathClose(path);
|
|
340
|
+
expect(getPathLastPoint(path)).toStrictEqual([30, 40]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('returns the last point after appendPathRectangle', () => {
|
|
344
|
+
const path = createPath();
|
|
345
|
+
appendPathRectangle(path, 10, 20, 100, 50);
|
|
346
|
+
expect(getPathLastPoint(path)).toStrictEqual([10, 70]);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('returns the endpoint after appendPathArc', () => {
|
|
350
|
+
const path = createPath();
|
|
351
|
+
appendPathArc(path, 0, 0, 100, 0, Math.PI / 2);
|
|
352
|
+
const last = getPathLastPoint(path)!;
|
|
353
|
+
expect(last[0]).toBeCloseTo(0, 1);
|
|
354
|
+
expect(last[1]).toBeCloseTo(100, 1);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('returns the endpoint after appendPathCircle', () => {
|
|
358
|
+
const path = createPath();
|
|
359
|
+
appendPathCircle(path, 10, 20, 30);
|
|
360
|
+
const last = getPathLastPoint(path)!;
|
|
361
|
+
expect(last[0]).toBeCloseTo(40);
|
|
362
|
+
expect(last[1]).toBeCloseTo(20);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('returns the last point after appendPathPolygon', () => {
|
|
366
|
+
const path = createPath();
|
|
367
|
+
appendPathPolygon(path, [0, 0, 100, 0, 100, 100]);
|
|
368
|
+
expect(getPathLastPoint(path)).toStrictEqual([100, 100]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('returns the last point after appendPathPolyline', () => {
|
|
372
|
+
const path = createPath();
|
|
373
|
+
appendPathPolyline(path, [0, 0, 50, 60, 70, 80]);
|
|
374
|
+
expect(getPathLastPoint(path)).toStrictEqual([70, 80]);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { appendPathLineTo, appendPathMoveTo, appendPathRectangle, createPath } from './path';
|
|
2
|
+
import { acquirePathMesh, acquirePathMeshTyped, releasePathMesh, releasePathMeshTyped } from './pathMeshPool';
|
|
3
|
+
|
|
4
|
+
describe('acquirePathMesh', () => {
|
|
5
|
+
it('returns a PathMesh for an empty path', () => {
|
|
6
|
+
const mesh = acquirePathMesh(createPath());
|
|
7
|
+
expect(mesh.vertices).toBeDefined();
|
|
8
|
+
expect(mesh.indices).toBeDefined();
|
|
9
|
+
releasePathMesh(mesh);
|
|
10
|
+
});
|
|
11
|
+
it('returns a mesh with triangles for a non-degenerate path', () => {
|
|
12
|
+
const path = createPath();
|
|
13
|
+
appendPathRectangle(path, 0, 0, 10, 10);
|
|
14
|
+
const mesh = acquirePathMesh(path);
|
|
15
|
+
expect(mesh.indices.length).toBeGreaterThan(0);
|
|
16
|
+
expect(mesh.indices.length % 3).toBe(0);
|
|
17
|
+
releasePathMesh(mesh);
|
|
18
|
+
});
|
|
19
|
+
it('pool reuses a released mesh (object identity)', () => {
|
|
20
|
+
const path = createPath();
|
|
21
|
+
appendPathMoveTo(path, 0, 0);
|
|
22
|
+
appendPathLineTo(path, 10, 0);
|
|
23
|
+
appendPathLineTo(path, 5, 10);
|
|
24
|
+
const first = acquirePathMesh(path);
|
|
25
|
+
releasePathMesh(first);
|
|
26
|
+
const second = acquirePathMesh(path);
|
|
27
|
+
// The second acquire should hand back the same object that was released.
|
|
28
|
+
expect(second).toBe(first);
|
|
29
|
+
releasePathMesh(second);
|
|
30
|
+
});
|
|
31
|
+
it('returns consistent tessellation results regardless of whether a pooled mesh is reused', () => {
|
|
32
|
+
const path = createPath();
|
|
33
|
+
appendPathRectangle(path, 0, 0, 20, 20);
|
|
34
|
+
const fresh = acquirePathMesh(path);
|
|
35
|
+
const freshIndices = [...fresh.indices];
|
|
36
|
+
releasePathMesh(fresh);
|
|
37
|
+
const reused = acquirePathMesh(path);
|
|
38
|
+
expect([...reused.indices]).toEqual(freshIndices);
|
|
39
|
+
releasePathMesh(reused);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('acquirePathMeshTyped', () => {
|
|
44
|
+
it('returns a PathMeshTyped with Float32Array and Uint32Array', () => {
|
|
45
|
+
const path = createPath();
|
|
46
|
+
appendPathRectangle(path, 0, 0, 5, 5);
|
|
47
|
+
const mesh = acquirePathMeshTyped(path);
|
|
48
|
+
expect(mesh.vertices).toBeInstanceOf(Float32Array);
|
|
49
|
+
expect(mesh.indices).toBeInstanceOf(Uint32Array);
|
|
50
|
+
releasePathMeshTyped(mesh);
|
|
51
|
+
});
|
|
52
|
+
it('returns non-empty typed arrays for a rectangle', () => {
|
|
53
|
+
const path = createPath();
|
|
54
|
+
appendPathRectangle(path, 0, 0, 100, 50);
|
|
55
|
+
const mesh = acquirePathMeshTyped(path);
|
|
56
|
+
expect(mesh.vertices.length).toBeGreaterThan(0);
|
|
57
|
+
expect(mesh.indices.length).toBeGreaterThan(0);
|
|
58
|
+
releasePathMeshTyped(mesh);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('releasePathMesh', () => {
|
|
63
|
+
it('can be called multiple times without error (double-release is caller error, not a crash)', () => {
|
|
64
|
+
const mesh = acquirePathMesh(createPath());
|
|
65
|
+
expect(() => releasePathMesh(mesh)).not.toThrow();
|
|
66
|
+
// Note: double-release would add two copies to the pool; callers must not do this.
|
|
67
|
+
// This test only confirms a single release does not throw.
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('releasePathMeshTyped', () => {
|
|
72
|
+
it('can be called on an acquired typed mesh without error', () => {
|
|
73
|
+
const path = createPath();
|
|
74
|
+
appendPathRectangle(path, 0, 0, 10, 10);
|
|
75
|
+
const mesh = acquirePathMeshTyped(path);
|
|
76
|
+
expect(() => releasePathMeshTyped(mesh)).not.toThrow();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PathCommand } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { appendPathClose, appendPathLineTo, appendPathMoveTo, createPath } from './path';
|
|
4
|
+
import { reversePath } from './reversePath';
|
|
5
|
+
|
|
6
|
+
describe('reversePath', () => {
|
|
7
|
+
it('reverses a simple open polyline', () => {
|
|
8
|
+
// Source: moveTo(0,0) → lineTo(10,0) → lineTo(10,10)
|
|
9
|
+
// Reversed: moveTo(10,10) → lineTo(10,0) → lineTo(0,0)
|
|
10
|
+
const source = createPath();
|
|
11
|
+
appendPathMoveTo(source, 0, 0);
|
|
12
|
+
appendPathLineTo(source, 10, 0);
|
|
13
|
+
appendPathLineTo(source, 10, 10);
|
|
14
|
+
const out = createPath();
|
|
15
|
+
reversePath(source, out);
|
|
16
|
+
expect(out.commands).toStrictEqual([PathCommand.MOVE_TO, PathCommand.LINE_TO, PathCommand.LINE_TO]);
|
|
17
|
+
expect(out.data).toStrictEqual([10, 10, 10, 0, 0, 0]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('preserves the closed flag', () => {
|
|
21
|
+
const source = createPath();
|
|
22
|
+
appendPathMoveTo(source, 0, 0);
|
|
23
|
+
appendPathLineTo(source, 10, 0);
|
|
24
|
+
appendPathLineTo(source, 10, 10);
|
|
25
|
+
appendPathClose(source);
|
|
26
|
+
const out = createPath();
|
|
27
|
+
reversePath(source, out);
|
|
28
|
+
expect(out.commands[out.commands.length - 1]).toBe(PathCommand.CLOSE);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('copies the winding rule', () => {
|
|
32
|
+
const source = createPath('evenOdd');
|
|
33
|
+
appendPathMoveTo(source, 0, 0);
|
|
34
|
+
appendPathLineTo(source, 1, 0);
|
|
35
|
+
const out = createPath('nonZero');
|
|
36
|
+
reversePath(source, out);
|
|
37
|
+
expect(out.winding).toBe('evenOdd');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('is alias-safe when out is the same as source', () => {
|
|
41
|
+
const path = createPath();
|
|
42
|
+
appendPathMoveTo(path, 0, 0);
|
|
43
|
+
appendPathLineTo(path, 10, 0);
|
|
44
|
+
appendPathLineTo(path, 20, 0);
|
|
45
|
+
reversePath(path, path);
|
|
46
|
+
expect(path.data[0]).toBe(20);
|
|
47
|
+
expect(path.data[1]).toBe(0);
|
|
48
|
+
expect(path.data[4]).toBe(0);
|
|
49
|
+
expect(path.data[5]).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { PathCommand } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { appendPathClose, appendPathLineTo, appendPathMoveTo, createPath } from './path';
|
|
4
|
+
import { simplifyPath } from './simplifyPath';
|
|
5
|
+
|
|
6
|
+
describe('simplifyPath', () => {
|
|
7
|
+
it('keeps a straight line unchanged', () => {
|
|
8
|
+
const source = createPath();
|
|
9
|
+
appendPathMoveTo(source, 0, 0);
|
|
10
|
+
appendPathLineTo(source, 50, 0);
|
|
11
|
+
appendPathLineTo(source, 100, 0);
|
|
12
|
+
const out = createPath();
|
|
13
|
+
simplifyPath(source, 1, out);
|
|
14
|
+
expect(out.commands).toStrictEqual([PathCommand.MOVE_TO, PathCommand.LINE_TO]);
|
|
15
|
+
expect(out.data).toStrictEqual([0, 0, 100, 0]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('simplifies a noisy straight line', () => {
|
|
19
|
+
const source = createPath();
|
|
20
|
+
appendPathMoveTo(source, 0, 0);
|
|
21
|
+
appendPathLineTo(source, 25, 0.1);
|
|
22
|
+
appendPathLineTo(source, 50, -0.1);
|
|
23
|
+
appendPathLineTo(source, 75, 0.05);
|
|
24
|
+
appendPathLineTo(source, 100, 0);
|
|
25
|
+
const out = createPath();
|
|
26
|
+
simplifyPath(source, 1, out);
|
|
27
|
+
expect(out.commands).toStrictEqual([PathCommand.MOVE_TO, PathCommand.LINE_TO]);
|
|
28
|
+
expect(out.data[0]).toBe(0);
|
|
29
|
+
expect(out.data[2]).toBe(100);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('keeps all points when tolerance is 0', () => {
|
|
33
|
+
const source = createPath();
|
|
34
|
+
appendPathMoveTo(source, 0, 0);
|
|
35
|
+
appendPathLineTo(source, 50, 10);
|
|
36
|
+
appendPathLineTo(source, 100, 0);
|
|
37
|
+
const out = createPath();
|
|
38
|
+
simplifyPath(source, 0, out);
|
|
39
|
+
expect(out.commands).toHaveLength(3);
|
|
40
|
+
expect(out.data).toStrictEqual([0, 0, 50, 10, 100, 0]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('preserves closed paths', () => {
|
|
44
|
+
const source = createPath();
|
|
45
|
+
appendPathMoveTo(source, 0, 0);
|
|
46
|
+
appendPathLineTo(source, 100, 0);
|
|
47
|
+
appendPathLineTo(source, 100, 100);
|
|
48
|
+
appendPathLineTo(source, 0, 100);
|
|
49
|
+
appendPathClose(source);
|
|
50
|
+
const out = createPath();
|
|
51
|
+
simplifyPath(source, 1, out);
|
|
52
|
+
expect(out.commands[out.commands.length - 1]).toBe(PathCommand.CLOSE);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('preserves winding rule', () => {
|
|
56
|
+
const source = createPath('evenOdd');
|
|
57
|
+
appendPathMoveTo(source, 0, 0);
|
|
58
|
+
appendPathLineTo(source, 100, 0);
|
|
59
|
+
const out = createPath();
|
|
60
|
+
simplifyPath(source, 1, out);
|
|
61
|
+
expect(out.winding).toBe('evenOdd');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('handles an empty path', () => {
|
|
65
|
+
const out = createPath();
|
|
66
|
+
simplifyPath(createPath(), 1, out);
|
|
67
|
+
expect(out.commands).toStrictEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|