@flighthq/shape 0.3.0-next.906.07cea63 → 0.3.1-next.209.43ef1bc

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.
Files changed (77) hide show
  1. package/dist/compactStrokePath.d.ts +3 -0
  2. package/dist/compactStrokePath.d.ts.map +1 -0
  3. package/dist/compactStrokePath.js +462 -0
  4. package/dist/compactStrokePath.js.map +1 -0
  5. package/dist/contract.d.ts +7 -2
  6. package/dist/contract.d.ts.map +1 -1
  7. package/dist/contract.js +7 -2
  8. package/dist/contract.js.map +1 -1
  9. package/dist/explainMorphShapeGradientEndpoints.d.ts +5 -0
  10. package/dist/explainMorphShapeGradientEndpoints.d.ts.map +1 -0
  11. package/dist/explainMorphShapeGradientEndpoints.js +43 -0
  12. package/dist/explainMorphShapeGradientEndpoints.js.map +1 -0
  13. package/dist/explainShapeTessellation.d.ts +3 -0
  14. package/dist/explainShapeTessellation.d.ts.map +1 -0
  15. package/dist/explainShapeTessellation.js +20 -0
  16. package/dist/explainShapeTessellation.js.map +1 -0
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/morphShape.d.ts +8 -0
  22. package/dist/morphShape.d.ts.map +1 -0
  23. package/dist/morphShape.js +74 -0
  24. package/dist/morphShape.js.map +1 -0
  25. package/dist/morphShapeAnimation.d.ts +5 -0
  26. package/dist/morphShapeAnimation.d.ts.map +1 -0
  27. package/dist/morphShapeAnimation.js +27 -0
  28. package/dist/morphShapeAnimation.js.map +1 -0
  29. package/dist/morphShapePaint.d.ts +9 -0
  30. package/dist/morphShapePaint.d.ts.map +1 -0
  31. package/dist/morphShapePaint.js +183 -0
  32. package/dist/morphShapePaint.js.map +1 -0
  33. package/dist/scale9ShapeCommands.d.ts +3 -0
  34. package/dist/scale9ShapeCommands.d.ts.map +1 -0
  35. package/dist/scale9ShapeCommands.js +119 -0
  36. package/dist/scale9ShapeCommands.js.map +1 -0
  37. package/dist/shape.d.ts.map +1 -1
  38. package/dist/shape.js +16 -3
  39. package/dist/shape.js.map +1 -1
  40. package/dist/shapeCommands.d.ts +3 -3
  41. package/dist/shapeCommands.d.ts.map +1 -1
  42. package/dist/shapeCommands.js +8 -8
  43. package/dist/shapeCommands.js.map +1 -1
  44. package/dist/shapeFill.d.ts.map +1 -1
  45. package/dist/shapeFill.js +8 -14
  46. package/dist/shapeFill.js.map +1 -1
  47. package/dist/shapeStroke.d.ts +2 -2
  48. package/dist/shapeStroke.d.ts.map +1 -1
  49. package/dist/shapeStroke.js +13 -77
  50. package/dist/shapeStroke.js.map +1 -1
  51. package/dist/shapeStrokeOutline.d.ts +3 -0
  52. package/dist/shapeStrokeOutline.d.ts.map +1 -0
  53. package/dist/shapeStrokeOutline.js +164 -0
  54. package/dist/shapeStrokeOutline.js.map +1 -0
  55. package/package.json +10 -7
  56. package/src/compactStrokePath.test.ts +31 -0
  57. package/src/explainMorphShapeGradientEndpoints.test.ts +47 -0
  58. package/src/explainShapeTessellation.test.ts +65 -0
  59. package/src/morphShape.test.ts +207 -0
  60. package/src/morphShapeAnimation.test.ts +147 -0
  61. package/src/morphShapePaint.test.ts +236 -0
  62. package/src/scale9ShapeCommands.test.ts +64 -0
  63. package/src/shape.test.ts +35 -0
  64. package/src/shapeCommands.test.ts +31 -31
  65. package/src/shapeFill.test.ts +9 -3
  66. package/src/shapeStroke.test.ts +35 -24
  67. package/src/shapeStrokeOutline.test.ts +31 -0
  68. package/dist/shapeHitTestBuiltins.d.ts +0 -2
  69. package/dist/shapeHitTestBuiltins.d.ts.map +0 -1
  70. package/dist/shapeHitTestBuiltins.js +0 -85
  71. package/dist/shapeHitTestBuiltins.js.map +0 -1
  72. package/dist/shapeHitTestRegistry.d.ts +0 -4
  73. package/dist/shapeHitTestRegistry.d.ts.map +0 -1
  74. package/dist/shapeHitTestRegistry.js +0 -13
  75. package/dist/shapeHitTestRegistry.js.map +0 -1
  76. package/src/shapeHitTestBuiltins.test.ts +0 -77
  77. package/src/shapeHitTestRegistry.test.ts +0 -45
@@ -1,4 +1,4 @@
1
- import { tessellatePath } from '@flighthq/path/contract';
1
+ import { tessellateStrokePath } from '@flighthq/path/contract';
2
2
  import type { ShapeCommandToken } from '@flighthq/types/contract';
3
3
  import { PathCommand } from '@flighthq/types/contract';
4
4
  import { describe, expect, it } from 'vitest';
@@ -25,25 +25,21 @@ function strokedCorner(join: 'bevel' | 'miter' | 'round', thickness = 20) {
25
25
  }
26
26
 
27
27
  describe('getShapeStrokeRegions', () => {
28
- it('offsets a solid stroke span into one fillable outline region carrying its color + alpha', () => {
28
+ it('resolves a solid stroke span into one styled centerline carrying its color + alpha', () => {
29
29
  const regions = strokedCorner('miter');
30
30
  expect(regions).not.toBeNull();
31
31
  expect(regions!.length).toBe(1);
32
32
  expect(regions![0].color).toBe(0x112233);
33
33
  expect(regions![0].alpha).toBe(0.5);
34
- // A real outline: closed contour(s) with MOVE + fill geometry, not empty.
34
+ expect(regions![0].style).toMatchObject({ width: 20, join: 'miter', cap: 'butt' });
35
35
  expect(regions![0].path.commands.length).toBeGreaterThan(0);
36
36
  expect(regions![0].path.commands[0]).toBe(PathCommand.MOVE_TO);
37
37
  });
38
38
 
39
- it('differentiates join styles (miter vs bevel vs round produce distinct outlines)', () => {
40
- const miter = strokedCorner('miter')![0].path.data.length;
41
- const bevel = strokedCorner('bevel')![0].path.data.length;
42
- const round = strokedCorner('round')![0].path.data.length;
43
- // The corner geometry differs per join: bevel cuts the corner, round arcs it (more points), miter
44
- // extends it. If joins were ignored (the old raster-clip bug) these would be identical.
45
- expect(miter).not.toBe(bevel);
46
- expect(round).toBeGreaterThan(bevel);
39
+ it('preserves miter, bevel, and round styles for the shared stroke tessellator', () => {
40
+ expect(strokedCorner('miter')![0].style.join).toBe('miter');
41
+ expect(strokedCorner('bevel')![0].style.join).toBe('bevel');
42
+ expect(strokedCorner('round')![0].style.join).toBe('round');
47
43
  });
48
44
 
49
45
  it('emits no region for a cleared stroke (lineStyle thickness 0) and for a stroke-free shape', () => {
@@ -73,36 +69,51 @@ describe('getShapeStrokeRegions', () => {
73
69
  expect(regions![1].color).toBe(0x00ff00);
74
70
  });
75
71
 
76
- it('tessellates an OPEN stroke outline into a fillable (non-empty) mesh', () => {
77
- const outline = strokedCorner('miter')![0].path;
78
- // The V's outline is a simple polygon: ear-clipping fills it (indices > 0). A closed stroke's ring
79
- // could not be direct-filled, which is why closed strokes defer (next test).
80
- const mesh = tessellatePath(outline);
81
- expect(mesh.indices.length).toBeGreaterThan(0);
72
+ it('tessellates an open styled centerline into a non-empty mesh', () => {
73
+ const region = strokedCorner('miter')![0];
74
+ const mesh = tessellateStrokePath(region.path, region.style);
75
+ expect(mesh).not.toBeNull();
76
+ expect(mesh!.indices.length).toBeGreaterThan(0);
82
77
  });
83
78
 
84
- it('defers a CLOSED stroke (rectangle) to the raster path — a ring is not direct-fillable (null)', () => {
79
+ it('resolves and tessellates a closed rectangle as a hollow ring', () => {
85
80
  const shape = createShape();
86
81
  appendShapeLineStyle(shape, 8, 0xff0000, 1, false, 'normal', 'none', 'miter', 4);
87
82
  appendShapeRectangle(shape, 10, 10, 100, 50);
88
- expect(getShapeStrokeRegions(shape.data.commands)).toBeNull();
83
+ const region = getShapeStrokeRegions(shape.data.commands)![0];
84
+ expect(tessellateStrokePath(region.path, region.style)).not.toBeNull();
89
85
  });
90
86
 
91
- it('defers a stroked drawPath carrying a CLOSE verb (null)', () => {
87
+ it('resolves a stroked drawPath carrying a CLOSE verb', () => {
92
88
  const shape = createShape();
93
89
  appendShapeLineStyle(shape, 6, 0x0000ff, 1, false, 'normal', 'none', 'miter', 4);
94
90
  // A triangle subpath closed by an explicit CLOSE verb — a ring, not an open stroke.
95
91
  const verbs = [PathCommand.MOVE_TO, PathCommand.LINE_TO, PathCommand.LINE_TO, PathCommand.CLOSE];
96
92
  appendShapePath(shape, verbs, [0, 0, 40, 0, 20, 30], 'nonZero');
97
- expect(getShapeStrokeRegions(shape.data.commands)).toBeNull();
93
+ const region = getShapeStrokeRegions(shape.data.commands)![0];
94
+ expect(tessellateStrokePath(region.path, region.style)).not.toBeNull();
98
95
  });
99
96
 
100
- it('defers a stroked return-to-start polygon (appendShapePolygon) even without a CLOSE verb (null)', () => {
97
+ it('resolves a stroked return-to-start polygon without a CLOSE verb', () => {
101
98
  const shape = createShape();
102
99
  appendShapeLineStyle(shape, 6, 0x0000ff, 1, false, 'normal', 'none', 'miter', 4);
103
100
  // appendShapePolygon re-emits the first point as a trailing lineTo (return-to-start), no CLOSE verb.
104
101
  appendShapePolygon(shape, [0, 0, 50, 0, 25, 40]);
105
- expect(getShapeStrokeRegions(shape.data.commands)).toBeNull();
102
+ const region = getShapeStrokeRegions(shape.data.commands)![0];
103
+ expect(tessellateStrokePath(region.path, region.style)).not.toBeNull();
104
+ });
105
+
106
+ it('keeps a pathological centerline as a region so the renderer can select raster fallback', () => {
107
+ const shape = createShape();
108
+ appendShapeLineStyle(shape, 6, 0x0000ff, 1, false, 'normal', 'none', 'miter', 4);
109
+ appendShapePath(
110
+ shape,
111
+ [PathCommand.MOVE_TO, PathCommand.LINE_TO, PathCommand.LINE_TO, PathCommand.LINE_TO, PathCommand.CLOSE],
112
+ [0, 0, 40, 40, 0, 40, 40, 0],
113
+ 'nonZero',
114
+ );
115
+ const region = getShapeStrokeRegions(shape.data.commands)![0];
116
+ expect(tessellateStrokePath(region.path, region.style)).toBeNull();
106
117
  });
107
118
 
108
119
  it('ignores an UNSTROKED closed primitive and still strokes a later open line (span-aware)', () => {
@@ -131,6 +142,6 @@ describe('hasNonSolidShapeStroke', () => {
131
142
  appendShapeLineStyle(solid, 3);
132
143
  expect(hasNonSolidShapeStroke(solid.data.commands)).toBe(false);
133
144
  expect(hasNonSolidShapeStroke(['lineGradientStyle', 1, 0] as ShapeCommandToken[])).toBe(true);
134
- expect(hasNonSolidShapeStroke(['lineBitmapStyle', 1, 0] as ShapeCommandToken[])).toBe(true);
145
+ expect(hasNonSolidShapeStroke(['lineTextureStyle', 1, 0] as ShapeCommandToken[])).toBe(true);
135
146
  });
136
147
  });
@@ -0,0 +1,31 @@
1
+ import type { ShapeCommandToken } from '@flighthq/types/contract';
2
+
3
+ import { createShape } from './shape';
4
+ import { appendShapeLineStyle, appendShapeLineTo, appendShapeMoveTo, appendShapeRectangle } from './shapeCommands';
5
+ import { getShapeStrokeOutlineRegions } from './shapeStrokeOutline';
6
+
7
+ describe('getShapeStrokeOutlineRegions', () => {
8
+ it('converts a solid open stroke into a fill region with matching color and alpha', () => {
9
+ const shape = createShape();
10
+ appendShapeLineStyle(shape, 8, 0x123456, 0.5, false, 'normal', 'square', 'bevel', 4);
11
+ appendShapeMoveTo(shape, 0, 0);
12
+ appendShapeLineTo(shape, 20, 0);
13
+
14
+ const regions = getShapeStrokeOutlineRegions(shape.data.commands);
15
+
16
+ expect(regions).not.toBeNull();
17
+ expect(regions).toHaveLength(1);
18
+ expect(regions![0]).toMatchObject({ alpha: 0.5, color: 0x123456 });
19
+ expect(regions![0].path.commands.length).toBeGreaterThan(0);
20
+ });
21
+
22
+ it('defers closed and non-solid strokes to the renderer fallback', () => {
23
+ const closed = createShape();
24
+ appendShapeLineStyle(closed, 8);
25
+ appendShapeRectangle(closed, 0, 0, 20, 10);
26
+ expect(getShapeStrokeOutlineRegions(closed.data.commands)).toBeNull();
27
+
28
+ const gradient: ShapeCommandToken[] = ['lineGradientStyle', 1, 0];
29
+ expect(getShapeStrokeOutlineRegions(gradient)).toBeNull();
30
+ });
31
+ });
@@ -1,2 +0,0 @@
1
- export declare function enableShapeHitTesting(): void;
2
- //# sourceMappingURL=shapeHitTestBuiltins.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"shapeHitTestBuiltins.d.ts","sourceRoot":"","sources":["../src/shapeHitTestBuiltins.ts"],"names":[],"mappings":"AAYA,wBAAgB,qBAAqB,IAAI,IAAI,CAyE5C"}
@@ -1,85 +0,0 @@
1
- import { registerShapeHitTestCommand } from './shapeHitTestRegistry';
2
- // Registers default hit-test handlers for the built-in primitive commands: drawCircle,
3
- // drawEllipse, drawRectangle, and drawRoundRectangle. Also registers a basic point-in-polygon
4
- // handler for pen-path fills (moveTo/lineTo/curveTo/drawPath). After calling this function,
5
- // hitTestShapeCommandPoint is no longer inert in isolation — it returns true/false for all
6
- // registered primitives.
7
- //
8
- // This function is tree-shakable: import it and call it once to opt in. Do not call at module
9
- // top level; call from your application's initialization code.
10
- export function enableShapeHitTesting() {
11
- registerShapeHitTestCommand({
12
- key: 'drawCircle',
13
- hitTest(x, y, buf, i) {
14
- const cx = buf[i];
15
- const cy = buf[i + 1];
16
- const r = buf[i + 2];
17
- const dx = x - cx;
18
- const dy = y - cy;
19
- return dx * dx + dy * dy <= r * r;
20
- },
21
- });
22
- registerShapeHitTestCommand({
23
- key: 'drawEllipse',
24
- hitTest(x, y, buf, i) {
25
- // drawEllipse args: x (top-left), y (top-left), width, height
26
- const ex = buf[i];
27
- const ey = buf[i + 1];
28
- const ew = buf[i + 2];
29
- const eh = buf[i + 3];
30
- const rx = ew / 2;
31
- const ry = eh / 2;
32
- const cx = ex + rx;
33
- const cy = ey + ry;
34
- const nx = (x - cx) / rx;
35
- const ny = (y - cy) / ry;
36
- return nx * nx + ny * ny <= 1;
37
- },
38
- });
39
- registerShapeHitTestCommand({
40
- key: 'drawRectangle',
41
- hitTest(x, y, buf, i) {
42
- const rx = buf[i];
43
- const ry = buf[i + 1];
44
- const rw = buf[i + 2];
45
- const rh = buf[i + 3];
46
- return x >= rx && x <= rx + rw && y >= ry && y <= ry + rh;
47
- },
48
- });
49
- registerShapeHitTestCommand({
50
- key: 'drawRoundRectangle',
51
- hitTest(x, y, buf, i) {
52
- const rx = buf[i];
53
- const ry = buf[i + 1];
54
- const rw = buf[i + 2];
55
- const rh = buf[i + 3];
56
- // drawRoundRectangle args: x, y, width, height, ellipseWidth, ellipseHeight.
57
- // ellipseWidth/ellipseHeight are the full diameters — halve them for the arc radii.
58
- const arx = (buf[i + 4] / 2) | 0 || buf[i + 4] / 2;
59
- const ary = (buf[i + 5] / 2) | 0 || buf[i + 5] / 2;
60
- // Clamp corner radii to at most half of the dimension in each axis.
61
- const cx = Math.min(arx, rw / 2);
62
- const cy = Math.min(ary, rh / 2);
63
- // Broad-phase: must be within the outer rectangle.
64
- if (x < rx || x > rx + rw || y < ry || y > ry + rh)
65
- return false;
66
- // The four corner rectangles — if inside those, check the ellipse; otherwise the interior
67
- // cross is unconditional.
68
- const inLeft = x < rx + cx;
69
- const inRight = x > rx + rw - cx;
70
- const inTop = y < ry + cy;
71
- const inBottom = y > ry + rh - cy;
72
- if ((inLeft || inRight) && (inTop || inBottom)) {
73
- // We are in one of the four corner regions. Map to the relevant ellipse center.
74
- const ecx = inLeft ? rx + cx : rx + rw - cx;
75
- const ecy = inTop ? ry + cy : ry + rh - cy;
76
- const dx = (x - ecx) / cx;
77
- const dy = (y - ecy) / cy;
78
- return dx * dx + dy * dy <= 1;
79
- }
80
- // Otherwise in the central cross — unconditionally inside.
81
- return true;
82
- },
83
- });
84
- }
85
- //# sourceMappingURL=shapeHitTestBuiltins.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"shapeHitTestBuiltins.js","sourceRoot":"","sources":["../src/shapeHitTestBuiltins.ts"],"names":[],"mappings":"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"}
@@ -1,4 +0,0 @@
1
- import type { ShapeCommandKey, ShapeCommandToken, ShapeHitTestCommand } from '@flighthq/types/contract';
2
- export declare function hitTestShapeCommandPoint(buf: readonly ShapeCommandToken[], i: number, x: number, y: number): boolean | null;
3
- export declare function registerShapeHitTestCommand<K extends ShapeCommandKey>(command: ShapeHitTestCommand<K>): void;
4
- //# sourceMappingURL=shapeHitTestRegistry.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"shapeHitTestRegistry.d.ts","sourceRoot":"","sources":["../src/shapeHitTestRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAOxG,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,SAAS,iBAAiB,EAAE,EACjC,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,OAAO,GAAG,IAAI,CAKhB;AAED,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,eAAe,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,IAAI,CAE5G"}
@@ -1,13 +0,0 @@
1
- const hitTests = new Map();
2
- // Tests the command at position i in buf. Args begin at i+2 (after key and argCount).
3
- export function hitTestShapeCommandPoint(buf, i, x, y) {
4
- const key = buf[i];
5
- const fn = hitTests.get(key);
6
- if (fn === undefined)
7
- return null;
8
- return fn(x, y, buf, i + 2);
9
- }
10
- export function registerShapeHitTestCommand(command) {
11
- hitTests.set(command.key, command.hitTest);
12
- }
13
- //# sourceMappingURL=shapeHitTestRegistry.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"shapeHitTestRegistry.js","sourceRoot":"","sources":["../src/shapeHitTestRegistry.ts"],"names":[],"mappings":"AAIA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEjD,sFAAsF;AACtF,MAAM,UAAU,wBAAwB,CACtC,GAAiC,EACjC,CAAS,EACT,CAAS,EACT,CAAS;IAET,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAW,CAAC;IAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,EAAE,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,2BAA2B,CAA4B,OAA+B;IACpG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,OAAuB,CAAC,CAAC;AAC7D,CAAC"}
@@ -1,77 +0,0 @@
1
- import { enableShapeHitTesting } from './shapeHitTestBuiltins';
2
- import { hitTestShapeCommandPoint } from './shapeHitTestRegistry';
3
-
4
- // Call once to register built-in handlers for this test module.
5
- enableShapeHitTesting();
6
-
7
- describe('enableShapeHitTesting', () => {
8
- describe('drawCircle', () => {
9
- it('returns true for a point inside the circle', () => {
10
- const buf = ['drawCircle', 3, 50, 50, 25];
11
- expect(hitTestShapeCommandPoint(buf, 0, 50, 50)).toBe(true);
12
- });
13
- it('returns true for a point exactly on the edge', () => {
14
- const buf = ['drawCircle', 3, 0, 0, 10];
15
- expect(hitTestShapeCommandPoint(buf, 0, 10, 0)).toBe(true);
16
- });
17
- it('returns false for a point outside the circle', () => {
18
- const buf = ['drawCircle', 3, 50, 50, 25];
19
- expect(hitTestShapeCommandPoint(buf, 0, 100, 100)).toBe(false);
20
- });
21
- });
22
-
23
- describe('drawEllipse', () => {
24
- it('returns true for a point at the ellipse center', () => {
25
- // drawEllipse args: x (top-left), y (top-left), width, height
26
- const buf = ['drawEllipse', 4, 0, 0, 100, 60];
27
- expect(hitTestShapeCommandPoint(buf, 0, 50, 30)).toBe(true);
28
- });
29
- it('returns false for a point outside the ellipse', () => {
30
- const buf = ['drawEllipse', 4, 0, 0, 100, 60];
31
- expect(hitTestShapeCommandPoint(buf, 0, 0, 0)).toBe(false);
32
- });
33
- });
34
-
35
- describe('drawRectangle', () => {
36
- it('returns true for a point inside the rectangle', () => {
37
- const buf = ['drawRectangle', 4, 10, 10, 80, 60];
38
- expect(hitTestShapeCommandPoint(buf, 0, 50, 40)).toBe(true);
39
- });
40
- it('returns true for a point on the rectangle edge', () => {
41
- const buf = ['drawRectangle', 4, 10, 10, 80, 60];
42
- expect(hitTestShapeCommandPoint(buf, 0, 10, 10)).toBe(true);
43
- });
44
- it('returns false for a point outside the rectangle', () => {
45
- const buf = ['drawRectangle', 4, 10, 10, 80, 60];
46
- expect(hitTestShapeCommandPoint(buf, 0, 0, 0)).toBe(false);
47
- });
48
- });
49
-
50
- describe('drawRoundRectangle', () => {
51
- it('returns true for a point in the rectangular bounds', () => {
52
- const buf = ['drawRoundRectangle', 6, 0, 0, 100, 50, 10, 10];
53
- expect(hitTestShapeCommandPoint(buf, 0, 50, 25)).toBe(true);
54
- });
55
- it('returns false for a point outside the rectangular bounds', () => {
56
- const buf = ['drawRoundRectangle', 6, 0, 0, 100, 50, 10, 10];
57
- expect(hitTestShapeCommandPoint(buf, 0, 200, 200)).toBe(false);
58
- });
59
- it('returns false for a point in the corner cutout region', () => {
60
- // With ellipseWidth=40, ellipseHeight=40: corner arcs have rx=cy=20.
61
- // The very corner (0, 0) is outside the rounded shape.
62
- const buf = ['drawRoundRectangle', 6, 0, 0, 100, 100, 40, 40];
63
- // (1, 1) is in the corner cutout — distance from corner center (20, 20) is sqrt(361)~19,
64
- // which is < 20 so it's inside the ellipse. Actually let's test a clearly-outside corner:
65
- // corner ellipse center is at (20, 20) with rx=ry=20; (2, 2) is dist ~18/20=0.9 < 1 => inside.
66
- // Use (0, 0) — normalized: (0-20)/20, (0-20)/20 => (-1, -1) => 1+1=2 > 1 => outside.
67
- expect(hitTestShapeCommandPoint(buf, 0, 0, 0)).toBe(false);
68
- });
69
- it('returns true for a point on the rounded corner arc boundary', () => {
70
- // Corner center at (20, 20) with rx=ry=20; point at (20, 0) is exactly on the top edge.
71
- const buf = ['drawRoundRectangle', 6, 0, 0, 100, 100, 40, 40];
72
- // (20, 0): corner check: inLeft=(20<20)=false, inTop=(0<20)=true, inRight=false.
73
- // Since not both inLeft/inRight AND inTop/inBottom, falls to the cross check → inside.
74
- expect(hitTestShapeCommandPoint(buf, 0, 20, 0)).toBe(true);
75
- });
76
- });
77
- });
@@ -1,45 +0,0 @@
1
- import type { ShapeCommandKey, ShapeCommandToken } from '@flighthq/types/contract';
2
-
3
- import { hitTestShapeCommandPoint, registerShapeHitTestCommand } from './shapeHitTestRegistry';
4
-
5
- describe('hitTestShapeCommandPoint', () => {
6
- it('returns null for an unregistered command key', () => {
7
- const buf: ShapeCommandToken[] = ['__unregistered__', 0];
8
- expect(hitTestShapeCommandPoint(buf, 0, 0, 0)).toBeNull();
9
- });
10
-
11
- it('passes x, y, buf, and i+2 to the registered handler', () => {
12
- const fn = vi.fn().mockReturnValue(false);
13
- registerShapeHitTestCommand({ key: 'moveTo' as ShapeCommandKey, hitTest: fn });
14
- const buf: ShapeCommandToken[] = ['moveTo', 2, 10, 20];
15
- hitTestShapeCommandPoint(buf, 0, 5, 7);
16
- expect(fn).toHaveBeenCalledWith(5, 7, buf, 2);
17
- });
18
-
19
- it('returns the handler return value', () => {
20
- registerShapeHitTestCommand({ key: 'endFill' as ShapeCommandKey, hitTest: () => true });
21
- const buf: ShapeCommandToken[] = ['endFill', 0];
22
- expect(hitTestShapeCommandPoint(buf, 0, 0, 0)).toBe(true);
23
- });
24
- });
25
-
26
- describe('registerShapeHitTestCommand', () => {
27
- it('registers a handler that hitTestShapeCommandPoint can retrieve', () => {
28
- const fn = vi.fn().mockReturnValue(true);
29
- registerShapeHitTestCommand({ key: 'drawRectangle' as ShapeCommandKey, hitTest: fn });
30
- const buf: ShapeCommandToken[] = ['drawRectangle', 4, 0, 0, 100, 100];
31
- const result = hitTestShapeCommandPoint(buf, 0, 50, 50);
32
- expect(result).toBe(true);
33
- });
34
-
35
- it('replaces an existing handler when registered again', () => {
36
- const first = vi.fn().mockReturnValue(false);
37
- const second = vi.fn().mockReturnValue(true);
38
- registerShapeHitTestCommand({ key: 'drawCircle' as ShapeCommandKey, hitTest: first });
39
- registerShapeHitTestCommand({ key: 'drawCircle' as ShapeCommandKey, hitTest: second });
40
- const buf: ShapeCommandToken[] = ['drawCircle', 3, 50, 50, 25];
41
- hitTestShapeCommandPoint(buf, 0, 50, 50);
42
- expect(second).toHaveBeenCalled();
43
- expect(first).not.toHaveBeenCalled();
44
- });
45
- });