@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/shape",
3
- "version": "0.2.0",
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.0",
36
- "@flighthq/geometry": "0.2.0",
37
- "@flighthq/node": "0.2.0",
38
- "@flighthq/types": "0.2.0"
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', () => {