@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.
- package/dist/compactStrokePath.d.ts +3 -0
- package/dist/compactStrokePath.d.ts.map +1 -0
- package/dist/compactStrokePath.js +462 -0
- package/dist/compactStrokePath.js.map +1 -0
- package/dist/contract.d.ts +7 -2
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +7 -2
- package/dist/contract.js.map +1 -1
- package/dist/explainMorphShapeGradientEndpoints.d.ts +5 -0
- package/dist/explainMorphShapeGradientEndpoints.d.ts.map +1 -0
- package/dist/explainMorphShapeGradientEndpoints.js +43 -0
- package/dist/explainMorphShapeGradientEndpoints.js.map +1 -0
- package/dist/explainShapeTessellation.d.ts +3 -0
- package/dist/explainShapeTessellation.d.ts.map +1 -0
- package/dist/explainShapeTessellation.js +20 -0
- package/dist/explainShapeTessellation.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/morphShape.d.ts +8 -0
- package/dist/morphShape.d.ts.map +1 -0
- package/dist/morphShape.js +74 -0
- package/dist/morphShape.js.map +1 -0
- package/dist/morphShapeAnimation.d.ts +5 -0
- package/dist/morphShapeAnimation.d.ts.map +1 -0
- package/dist/morphShapeAnimation.js +27 -0
- package/dist/morphShapeAnimation.js.map +1 -0
- package/dist/morphShapePaint.d.ts +9 -0
- package/dist/morphShapePaint.d.ts.map +1 -0
- package/dist/morphShapePaint.js +183 -0
- package/dist/morphShapePaint.js.map +1 -0
- package/dist/scale9ShapeCommands.d.ts +3 -0
- package/dist/scale9ShapeCommands.d.ts.map +1 -0
- package/dist/scale9ShapeCommands.js +119 -0
- package/dist/scale9ShapeCommands.js.map +1 -0
- package/dist/shape.d.ts.map +1 -1
- package/dist/shape.js +16 -3
- package/dist/shape.js.map +1 -1
- package/dist/shapeCommands.d.ts +3 -3
- package/dist/shapeCommands.d.ts.map +1 -1
- package/dist/shapeCommands.js +8 -8
- package/dist/shapeCommands.js.map +1 -1
- package/dist/shapeFill.d.ts.map +1 -1
- package/dist/shapeFill.js +8 -14
- package/dist/shapeFill.js.map +1 -1
- package/dist/shapeStroke.d.ts +2 -2
- package/dist/shapeStroke.d.ts.map +1 -1
- package/dist/shapeStroke.js +13 -77
- package/dist/shapeStroke.js.map +1 -1
- package/dist/shapeStrokeOutline.d.ts +3 -0
- package/dist/shapeStrokeOutline.d.ts.map +1 -0
- package/dist/shapeStrokeOutline.js +164 -0
- package/dist/shapeStrokeOutline.js.map +1 -0
- package/package.json +10 -7
- package/src/compactStrokePath.test.ts +31 -0
- package/src/explainMorphShapeGradientEndpoints.test.ts +47 -0
- package/src/explainShapeTessellation.test.ts +65 -0
- package/src/morphShape.test.ts +207 -0
- package/src/morphShapeAnimation.test.ts +147 -0
- package/src/morphShapePaint.test.ts +236 -0
- package/src/scale9ShapeCommands.test.ts +64 -0
- package/src/shape.test.ts +35 -0
- package/src/shapeCommands.test.ts +31 -31
- package/src/shapeFill.test.ts +9 -3
- package/src/shapeStroke.test.ts +35 -24
- package/src/shapeStrokeOutline.test.ts +31 -0
- package/dist/shapeHitTestBuiltins.d.ts +0 -2
- package/dist/shapeHitTestBuiltins.d.ts.map +0 -1
- package/dist/shapeHitTestBuiltins.js +0 -85
- package/dist/shapeHitTestBuiltins.js.map +0 -1
- package/dist/shapeHitTestRegistry.d.ts +0 -4
- package/dist/shapeHitTestRegistry.d.ts.map +0 -1
- package/dist/shapeHitTestRegistry.js +0 -13
- package/dist/shapeHitTestRegistry.js.map +0 -1
- package/src/shapeHitTestBuiltins.test.ts +0 -77
- package/src/shapeHitTestRegistry.test.ts +0 -45
package/src/shapeStroke.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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('
|
|
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
|
-
|
|
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('
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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('
|
|
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
|
-
|
|
83
|
+
const region = getShapeStrokeRegions(shape.data.commands)![0];
|
|
84
|
+
expect(tessellateStrokePath(region.path, region.style)).not.toBeNull();
|
|
89
85
|
});
|
|
90
86
|
|
|
91
|
-
it('
|
|
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
|
-
|
|
93
|
+
const region = getShapeStrokeRegions(shape.data.commands)![0];
|
|
94
|
+
expect(tessellateStrokePath(region.path, region.style)).not.toBeNull();
|
|
98
95
|
});
|
|
99
96
|
|
|
100
|
-
it('
|
|
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
|
-
|
|
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(['
|
|
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 +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
|
-
});
|