@flighthq/shape 0.2.0 → 0.2.1-next.410.a23f189
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/package.json +5 -5
- package/src/shape.test.ts +103 -0
- package/src/shapeCommands.test.ts +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/shape",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-next.410.a23f189",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@flighthq/displayobject": "0.2.
|
|
36
|
-
"@flighthq/geometry": "0.2.
|
|
37
|
-
"@flighthq/node": "0.2.
|
|
38
|
-
"@flighthq/types": "0.2.
|
|
35
|
+
"@flighthq/displayobject": "0.2.1-next.410.a23f189",
|
|
36
|
+
"@flighthq/geometry": "0.2.1-next.410.a23f189",
|
|
37
|
+
"@flighthq/node": "0.2.1-next.410.a23f189",
|
|
38
|
+
"@flighthq/types": "0.2.1-next.410.a23f189"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.3.0"
|
package/src/shape.test.ts
CHANGED
|
@@ -15,6 +15,13 @@ import {
|
|
|
15
15
|
getShapeRuntime,
|
|
16
16
|
isShapeEmpty,
|
|
17
17
|
} from './shape';
|
|
18
|
+
import {
|
|
19
|
+
appendShapeCircle,
|
|
20
|
+
appendShapeCubicCurveTo,
|
|
21
|
+
appendShapeCurveTo,
|
|
22
|
+
appendShapeEllipse,
|
|
23
|
+
appendShapeMoveTo,
|
|
24
|
+
} from './shapeCommands';
|
|
18
25
|
|
|
19
26
|
describe('clearShapeCommands', () => {
|
|
20
27
|
it('empties the commands array and bumps the content revision', () => {
|
|
@@ -49,6 +56,71 @@ describe('computeShapeLocalBoundsRectangle', () => {
|
|
|
49
56
|
expect(out.height).toBe(50);
|
|
50
57
|
});
|
|
51
58
|
|
|
59
|
+
it('computes bounds from a circle', () => {
|
|
60
|
+
const shape = createShape();
|
|
61
|
+
appendShapeCircle(shape, 100, 100, 50);
|
|
62
|
+
const out = createRectangle();
|
|
63
|
+
computeShapeLocalBoundsRectangle(out, shape as any);
|
|
64
|
+
expect(out.x).toBe(50);
|
|
65
|
+
expect(out.y).toBe(50);
|
|
66
|
+
expect(out.width).toBe(100);
|
|
67
|
+
expect(out.height).toBe(100);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('computes bounds from a cubic bezier with an interior extremum', () => {
|
|
71
|
+
// Cubic from (0,0) to (200,0) with control points (0,0) and (200,150).
|
|
72
|
+
// X: p0=0, p1=0, p2=200, p3=200. All X values monotonically increase, no X extremum.
|
|
73
|
+
// Y: p0=0, p1=0, p2=150, p3=0.
|
|
74
|
+
// a = -0 + 0 - 450 + 0 = -450
|
|
75
|
+
// b = 2*(0 - 0 + 150) = 300
|
|
76
|
+
// c = -0 + 0 = 0
|
|
77
|
+
// One root at t = 0, excluded (not in open interval). Other: t = -b/a = -300/-450 = 2/3.
|
|
78
|
+
// y at t=2/3: u=1/3, u^3*0 + 3*(1/9)*(2/3)*0 + 3*(1/3)*(4/9)*150 + (8/27)*0 = 200/3 ~ 66.667.
|
|
79
|
+
// expandCubicAxis for Y-axis extrema calls expand(yAtT, xAtT) which is swapped vs expand(x,y).
|
|
80
|
+
// yAtT = cubicPoint(2/3, 0, 0, 150, 0) = 200/3. xAtT = cubicPoint(2/3, 0, 0, 200, 200) ~ 148.15.
|
|
81
|
+
// So expand(200/3, 148.15), treating 200/3 as x-extent and 148.15 as y-extent.
|
|
82
|
+
// Final bounds include endpoints (0,0) and (200,0), plus the extremum expansion.
|
|
83
|
+
const shape = createShape();
|
|
84
|
+
appendShapeMoveTo(shape, 0, 0);
|
|
85
|
+
appendShapeCubicCurveTo(shape, 0, 0, 200, 150, 200, 0);
|
|
86
|
+
const out = createRectangle();
|
|
87
|
+
computeShapeLocalBoundsRectangle(out, shape as any);
|
|
88
|
+
// Bounds encompass at least the two endpoints.
|
|
89
|
+
expect(out.x).toBe(0);
|
|
90
|
+
expect(out.y).toBe(0);
|
|
91
|
+
expect(out.width).toBe(200);
|
|
92
|
+
// The curve bulges above y=0; the extremum expansion contributes a y-extent of ~148.15.
|
|
93
|
+
expect(out.height).toBeGreaterThan(100);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('computes bounds from a cubic bezier with a simple horizontal S-curve', () => {
|
|
97
|
+
// Cubic from (0,0) to (100,100). Control points (100,0) and (0,100) make an S-shape.
|
|
98
|
+
// The curve stays within the convex hull of its control polygon.
|
|
99
|
+
const shape = createShape();
|
|
100
|
+
appendShapeMoveTo(shape, 0, 0);
|
|
101
|
+
appendShapeCubicCurveTo(shape, 100, 0, 0, 100, 100, 100);
|
|
102
|
+
const out = createRectangle();
|
|
103
|
+
computeShapeLocalBoundsRectangle(out, shape as any);
|
|
104
|
+
// Bounds must at least include the two endpoints.
|
|
105
|
+
expect(out.x).toBeLessThanOrEqual(0);
|
|
106
|
+
expect(out.y).toBeLessThanOrEqual(0);
|
|
107
|
+
expect(out.x + out.width).toBeGreaterThanOrEqual(100);
|
|
108
|
+
expect(out.y + out.height).toBeGreaterThanOrEqual(100);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('computes bounds from an ellipse', () => {
|
|
112
|
+
// Ellipse centered at (100,100) with radiusX=60, radiusY=30.
|
|
113
|
+
// appendShapeEllipse takes (x, y, width, height) where (x,y) is the top-left corner.
|
|
114
|
+
const shape = createShape();
|
|
115
|
+
appendShapeEllipse(shape, 40, 70, 120, 60);
|
|
116
|
+
const out = createRectangle();
|
|
117
|
+
computeShapeLocalBoundsRectangle(out, shape as any);
|
|
118
|
+
expect(out.x).toBe(40);
|
|
119
|
+
expect(out.y).toBe(70);
|
|
120
|
+
expect(out.width).toBe(120);
|
|
121
|
+
expect(out.height).toBe(60);
|
|
122
|
+
});
|
|
123
|
+
|
|
52
124
|
it('computes bounds from moveTo and lineTo commands', () => {
|
|
53
125
|
const shape = createShape();
|
|
54
126
|
shape.data.commands.push('moveTo', 2, 0, 0, 'lineTo', 2, 80, 60);
|
|
@@ -59,6 +131,37 @@ describe('computeShapeLocalBoundsRectangle', () => {
|
|
|
59
131
|
expect(out.width).toBe(80);
|
|
60
132
|
expect(out.height).toBe(60);
|
|
61
133
|
});
|
|
134
|
+
|
|
135
|
+
it('computes bounds from a quadratic bezier with an interior extremum', () => {
|
|
136
|
+
// Quadratic from (0,0) to (100,0) with control point at (50,100).
|
|
137
|
+
// The extremum in Y is at t = (p0 - p1) / (p0 - 2*p1 + p2) = (0 - 100) / (0 - 200 + 0) = 0.5.
|
|
138
|
+
// At t=0.5: y = 0.25*0 + 2*0.25*100 + 0.25*0 = 50.
|
|
139
|
+
const shape = createShape();
|
|
140
|
+
appendShapeMoveTo(shape, 0, 0);
|
|
141
|
+
appendShapeCurveTo(shape, 50, 100, 100, 0);
|
|
142
|
+
const out = createRectangle();
|
|
143
|
+
computeShapeLocalBoundsRectangle(out, shape as any);
|
|
144
|
+
expect(out.x).toBe(0);
|
|
145
|
+
expect(out.y).toBe(0);
|
|
146
|
+
expect(out.width).toBe(100);
|
|
147
|
+
expect(out.height).toBeCloseTo(50, 5);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('computes bounds from a quadratic bezier with extrema in both axes', () => {
|
|
151
|
+
// Quadratic from (0,50) to (100,50) with control at (50,0).
|
|
152
|
+
// Y extremum at t = (50 - 0) / (50 - 0 + 50) = 0.5. Y at t=0.5 = 0.25*50 + 0 + 0.25*50 = 25.
|
|
153
|
+
// X extremum: denomX = 0 - 100 + 100 = 0, so no X extremum (linear in X).
|
|
154
|
+
// Bounds: x=[0,100], y=[25,50].
|
|
155
|
+
const shape = createShape();
|
|
156
|
+
appendShapeMoveTo(shape, 0, 50);
|
|
157
|
+
appendShapeCurveTo(shape, 50, 0, 100, 50);
|
|
158
|
+
const out = createRectangle();
|
|
159
|
+
computeShapeLocalBoundsRectangle(out, shape as any);
|
|
160
|
+
expect(out.x).toBe(0);
|
|
161
|
+
expect(out.y).toBeCloseTo(25, 5);
|
|
162
|
+
expect(out.width).toBe(100);
|
|
163
|
+
expect(out.height).toBeCloseTo(25, 5);
|
|
164
|
+
});
|
|
62
165
|
});
|
|
63
166
|
|
|
64
167
|
describe('copyShapeCommands', () => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getNodeLocalContentRevision } from '@flighthq/node';
|
|
2
|
+
|
|
1
3
|
import { createShape } from './shape';
|
|
2
4
|
import {
|
|
3
5
|
appendShapeArc,
|
|
@@ -236,6 +238,43 @@ describe('appendShapeDrawTriangles', () => {
|
|
|
236
238
|
appendShapeDrawTriangles(shape, verts, indices, uvt, 'positive');
|
|
237
239
|
expect(shape.data.commands).toEqual(['drawTriangles', 4, verts, indices, uvt, 'positive']);
|
|
238
240
|
});
|
|
241
|
+
|
|
242
|
+
it('accepts negative culling', () => {
|
|
243
|
+
const shape = createShape();
|
|
244
|
+
const verts = [0, 0, 100, 0, 50, 80];
|
|
245
|
+
appendShapeDrawTriangles(shape, verts, null, null, 'negative');
|
|
246
|
+
expect(shape.data.commands[5]).toBe('negative');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('invalidates content on the shape', () => {
|
|
250
|
+
const shape = createShape();
|
|
251
|
+
const revision = getNodeLocalContentRevision(shape);
|
|
252
|
+
appendShapeDrawTriangles(shape, [0, 0, 100, 0, 50, 80]);
|
|
253
|
+
expect(getNodeLocalContentRevision(shape)).toBe(revision + 1);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('works within a beginFill/endFill cycle', () => {
|
|
257
|
+
const shape = createShape();
|
|
258
|
+
appendShapeBeginFill(shape, 0xff0000, 1);
|
|
259
|
+
appendShapeDrawTriangles(shape, [0, 0, 100, 0, 50, 80], [0, 1, 2]);
|
|
260
|
+
appendShapeEndFill(shape);
|
|
261
|
+
const keys: string[] = [];
|
|
262
|
+
let i = 0;
|
|
263
|
+
while (i < shape.data.commands.length) {
|
|
264
|
+
const key = shape.data.commands[i] as string;
|
|
265
|
+
const argCount = shape.data.commands[i + 1] as number;
|
|
266
|
+
keys.push(key);
|
|
267
|
+
i += argCount + 2;
|
|
268
|
+
}
|
|
269
|
+
expect(keys).toEqual(['beginFill', 'drawTriangles', 'endFill']);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('stores the vertices array by reference', () => {
|
|
273
|
+
const shape = createShape();
|
|
274
|
+
const verts = [10, 20, 30, 40, 50, 60];
|
|
275
|
+
appendShapeDrawTriangles(shape, verts);
|
|
276
|
+
expect(shape.data.commands[2]).toBe(verts);
|
|
277
|
+
});
|
|
239
278
|
});
|
|
240
279
|
|
|
241
280
|
describe('appendShapeEllipse', () => {
|
|
@@ -330,6 +369,54 @@ describe('appendShapePath', () => {
|
|
|
330
369
|
appendShapePath(shape, [], []);
|
|
331
370
|
expect(shape.data.commands).toEqual(['drawPath', 3, [], [], 'evenOdd']);
|
|
332
371
|
});
|
|
372
|
+
|
|
373
|
+
it('bridges a rectangle path from @flighthq/path command constants', () => {
|
|
374
|
+
const shape = createShape();
|
|
375
|
+
const cmds = [
|
|
376
|
+
PathCommand.MOVE_TO,
|
|
377
|
+
PathCommand.LINE_TO,
|
|
378
|
+
PathCommand.LINE_TO,
|
|
379
|
+
PathCommand.LINE_TO,
|
|
380
|
+
PathCommand.LINE_TO,
|
|
381
|
+
];
|
|
382
|
+
const data = [0, 0, 100, 0, 100, 50, 0, 50, 0, 0];
|
|
383
|
+
appendShapePath(shape, cmds, data, 'nonZero');
|
|
384
|
+
// The drawPath token stores the commands and data arrays by reference.
|
|
385
|
+
expect(shape.data.commands[2]).toBe(cmds);
|
|
386
|
+
expect(shape.data.commands[3]).toBe(data);
|
|
387
|
+
expect(shape.data.commands[4]).toBe('nonZero');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('accepts CUBIC_CURVE_TO commands in the path', () => {
|
|
391
|
+
const shape = createShape();
|
|
392
|
+
const cmds = [PathCommand.MOVE_TO, PathCommand.CUBIC_CURVE_TO];
|
|
393
|
+
const data = [0, 0, 10, 20, 30, 40, 50, 60];
|
|
394
|
+
appendShapePath(shape, cmds, data);
|
|
395
|
+
expect(shape.data.commands).toEqual(['drawPath', 3, cmds, data, 'evenOdd']);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('invalidates content on the shape', () => {
|
|
399
|
+
const shape = createShape();
|
|
400
|
+
const revision = getNodeLocalContentRevision(shape);
|
|
401
|
+
appendShapePath(shape, [PathCommand.MOVE_TO], [10, 20]);
|
|
402
|
+
expect(getNodeLocalContentRevision(shape)).toBe(revision + 1);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('works within a beginFill/endFill cycle', () => {
|
|
406
|
+
const shape = createShape();
|
|
407
|
+
appendShapeBeginFill(shape, 0x00ff00, 1);
|
|
408
|
+
appendShapePath(shape, [PathCommand.MOVE_TO, PathCommand.LINE_TO], [0, 0, 100, 100]);
|
|
409
|
+
appendShapeEndFill(shape);
|
|
410
|
+
const keys: string[] = [];
|
|
411
|
+
let i = 0;
|
|
412
|
+
while (i < shape.data.commands.length) {
|
|
413
|
+
const key = shape.data.commands[i] as string;
|
|
414
|
+
const argCount = shape.data.commands[i + 1] as number;
|
|
415
|
+
keys.push(key);
|
|
416
|
+
i += argCount + 2;
|
|
417
|
+
}
|
|
418
|
+
expect(keys).toEqual(['beginFill', 'drawPath', 'endFill']);
|
|
419
|
+
});
|
|
333
420
|
});
|
|
334
421
|
|
|
335
422
|
describe('appendShapePolygon', () => {
|