@flighthq/interaction 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/dist/cursorBackend.d.ts +12 -0
- package/dist/cursorBackend.d.ts.map +1 -0
- package/dist/cursorBackend.js +17 -0
- package/dist/cursorBackend.js.map +1 -0
- package/dist/displayHitTests.d.ts +10 -11
- package/dist/displayHitTests.d.ts.map +1 -1
- package/dist/displayHitTests.js +12 -14
- package/dist/displayHitTests.js.map +1 -1
- package/dist/enableInteractionGuards.d.ts +23 -0
- package/dist/enableInteractionGuards.d.ts.map +1 -0
- package/dist/enableInteractionGuards.js +75 -0
- package/dist/enableInteractionGuards.js.map +1 -0
- package/dist/focusManager.d.ts +46 -0
- package/dist/focusManager.d.ts.map +1 -0
- package/dist/focusManager.js +243 -0
- package/dist/focusManager.js.map +1 -0
- package/dist/hitTests.d.ts +45 -24
- package/dist/hitTests.d.ts.map +1 -1
- package/dist/hitTests.js +164 -47
- package/dist/hitTests.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/interactionManager.d.ts +10 -2
- package/dist/interactionManager.d.ts.map +1 -1
- package/dist/interactionManager.js +58 -6
- package/dist/interactionManager.js.map +1 -1
- package/dist/interactionSpatialIndex.d.ts +17 -0
- package/dist/interactionSpatialIndex.d.ts.map +1 -0
- package/dist/interactionSpatialIndex.js +77 -0
- package/dist/interactionSpatialIndex.js.map +1 -0
- package/dist/nodeInteractionState.d.ts +39 -0
- package/dist/nodeInteractionState.d.ts.map +1 -0
- package/dist/nodeInteractionState.js +72 -0
- package/dist/nodeInteractionState.js.map +1 -0
- package/dist/registerBitmapHitTest.d.ts +11 -0
- package/dist/registerBitmapHitTest.d.ts.map +1 -0
- package/dist/registerBitmapHitTest.js +55 -0
- package/dist/registerBitmapHitTest.js.map +1 -0
- package/dist/registerDefaultHitTests.d.ts +2 -0
- package/dist/registerDefaultHitTests.d.ts.map +1 -0
- package/dist/registerDefaultHitTests.js +20 -0
- package/dist/registerDefaultHitTests.js.map +1 -0
- package/dist/registerShapeHitTest.d.ts +10 -0
- package/dist/registerShapeHitTest.d.ts.map +1 -0
- package/dist/registerShapeHitTest.js +32 -0
- package/dist/registerShapeHitTest.js.map +1 -0
- package/dist/registerTextHitTest.d.ts +12 -0
- package/dist/registerTextHitTest.d.ts.map +1 -0
- package/dist/registerTextHitTest.js +32 -0
- package/dist/registerTextHitTest.js.map +1 -0
- package/dist/spriteHitTests.d.ts +3 -3
- package/dist/spriteHitTests.d.ts.map +1 -1
- package/dist/spriteHitTests.js +5 -5
- package/dist/spriteHitTests.js.map +1 -1
- package/package.json +13 -7
- package/src/cursorBackend.test.ts +24 -0
- package/src/displayHitTests.test.ts +39 -47
- package/src/enableInteractionGuards.test.ts +102 -0
- package/src/focusManager.test.ts +307 -0
- package/src/hitTests.test.ts +235 -98
- package/src/interactionManager.test.ts +59 -8
- package/src/interactionSpatialIndex.test.ts +103 -0
- package/src/nodeInteractionState.test.ts +141 -0
- package/src/registerBitmapHitTest.test.ts +22 -0
- package/src/{registerDefaultHitTestPoints.test.ts → registerDefaultHitTests.test.ts} +10 -7
- package/src/registerShapeHitTest.test.ts +27 -0
- package/src/registerTextHitTest.test.ts +54 -0
- package/src/spriteHitTests.test.ts +15 -15
- package/dist/registerDefaultHitTestPoints.d.ts +0 -2
- package/dist/registerDefaultHitTestPoints.d.ts.map +0 -1
- package/dist/registerDefaultHitTestPoints.js +0 -21
- package/dist/registerDefaultHitTestPoints.js.map +0 -1
package/src/hitTests.test.ts
CHANGED
|
@@ -1,32 +1,186 @@
|
|
|
1
1
|
import { createDisplayObject, createDisplayObjectGeneric, getDisplayObjectRuntime } from '@flighthq/displayobject';
|
|
2
|
-
import { setRectangle } from '@flighthq/geometry';
|
|
2
|
+
import { createRectangle, setRectangle } from '@flighthq/geometry';
|
|
3
3
|
import { addNodeChild, getNodeLocalBoundsRectangle, invalidateNodeLocalTransform } from '@flighthq/node';
|
|
4
|
-
import
|
|
4
|
+
import { appendPathRectangle, createPath } from '@flighthq/path';
|
|
5
|
+
import type { DisplayObject, DisplayObjectRuntime, HitTestResult, NodeAny } from '@flighthq/types';
|
|
5
6
|
import { DisplayObjectKind } from '@flighthq/types';
|
|
6
7
|
|
|
7
8
|
import {
|
|
9
|
+
describeGraphHit,
|
|
8
10
|
findGraphHitTarget,
|
|
11
|
+
findGraphHitTargetPrecise,
|
|
12
|
+
findGraphHitTargets,
|
|
13
|
+
findGraphHitTargetsPrecise,
|
|
9
14
|
hitTestDisplayObjects,
|
|
10
15
|
hitTestGraphLocalBounds,
|
|
11
16
|
hitTestGraphPoint,
|
|
12
|
-
|
|
17
|
+
hitTestGraphPointPrecise,
|
|
18
|
+
hitTestNodeRegion,
|
|
19
|
+
registerHitTest,
|
|
20
|
+
registerHitTestPrecise,
|
|
13
21
|
} from './hitTests';
|
|
22
|
+
import { setNodeHitArea, setNodeHitTestEnabled } from './nodeInteractionState';
|
|
23
|
+
|
|
24
|
+
// A precise provider over local bounds: 0 (hit) inside, -1 outside — the boolean-precise shape.
|
|
25
|
+
function boundsPrecise(source: NodeAny, x: number, y: number): number {
|
|
26
|
+
return hitTestGraphLocalBounds(source, x, y) ? 0 : -1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function boundsObject(w: number, h: number): DisplayObject {
|
|
30
|
+
const obj = createDisplayObject();
|
|
31
|
+
setRectangle(getNodeLocalBoundsRectangle(obj), 0, 0, w, h);
|
|
32
|
+
setNodeHitTestEnabled(obj, true);
|
|
33
|
+
return obj;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function emptyResult(node: DisplayObject): HitTestResult {
|
|
37
|
+
return { localX: 0, localY: 0, node, subIndex: -2 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeAll(() => {
|
|
41
|
+
registerHitTest(DisplayObjectKind, hitTestGraphLocalBounds);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('describeGraphHit', () => {
|
|
45
|
+
it('fills node, local coordinates, and the sub-index from the kind exact provider', () => {
|
|
46
|
+
const kind = 'DescribeKind';
|
|
47
|
+
const node = createDisplayObjectGeneric(kind);
|
|
48
|
+
node.x = 40;
|
|
49
|
+
node.y = 40;
|
|
50
|
+
invalidateNodeLocalTransform(node);
|
|
51
|
+
setRectangle(getNodeLocalBoundsRectangle(node), 0, 0, 100, 100);
|
|
52
|
+
registerHitTestPrecise(kind, () => 7);
|
|
53
|
+
const out = emptyResult(node);
|
|
54
|
+
describeGraphHit(node, 50, 50, out);
|
|
55
|
+
expect(out.node).toBe(node);
|
|
56
|
+
expect(out.localX).toBe(10);
|
|
57
|
+
expect(out.localY).toBe(10);
|
|
58
|
+
expect(out.subIndex).toBe(7);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('reports subIndex -1 when the kind has no exact provider', () => {
|
|
62
|
+
const obj = boundsObject(100, 100);
|
|
63
|
+
const out = emptyResult(obj);
|
|
64
|
+
describeGraphHit(obj, 50, 50, out);
|
|
65
|
+
expect(out.subIndex).toBe(-1);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
14
68
|
|
|
15
69
|
describe('findGraphHitTarget', () => {
|
|
16
70
|
it('returns null when node is disabled', () => {
|
|
17
|
-
const obj =
|
|
71
|
+
const obj = boundsObject(100, 100);
|
|
18
72
|
obj.enabled = false;
|
|
19
73
|
expect(findGraphHitTarget(obj, 50, 50)).toBeNull();
|
|
20
74
|
});
|
|
21
75
|
|
|
22
|
-
it('
|
|
76
|
+
it('is not a candidate until it opts in, then is', () => {
|
|
77
|
+
const obj = createDisplayObject();
|
|
78
|
+
setRectangle(getNodeLocalBoundsRectangle(obj), 0, 0, 100, 100);
|
|
79
|
+
expect(findGraphHitTarget(obj, 50, 50)).toBeNull();
|
|
80
|
+
setNodeHitTestEnabled(obj, true);
|
|
81
|
+
expect(findGraphHitTarget(obj, 50, 50)).toBe(obj);
|
|
82
|
+
setNodeHitTestEnabled(obj, false);
|
|
83
|
+
expect(findGraphHitTarget(obj, 50, 50)).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('descends into an un-opted-in parent to reach an opted-in child', () => {
|
|
23
87
|
const parent = createDisplayObject();
|
|
24
|
-
const child =
|
|
25
|
-
|
|
88
|
+
const child = boundsObject(100, 100);
|
|
89
|
+
addNodeChild(parent, child);
|
|
90
|
+
expect(findGraphHitTarget(parent, 50, 50)).toBe(child);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('treats a hitArea node as an atomic unit that consumes and hides its children', () => {
|
|
94
|
+
const parent = boundsObject(200, 200);
|
|
95
|
+
const child = boundsObject(50, 50);
|
|
26
96
|
addNodeChild(parent, child);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
97
|
+
setNodeHitArea(parent, 'bounds');
|
|
98
|
+
expect(findGraphHitTarget(parent, 25, 25)).toBe(parent);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('resolves a rectangle hitArea in the node local space, through the world transform', () => {
|
|
102
|
+
const obj = createDisplayObject();
|
|
103
|
+
obj.x = 40;
|
|
104
|
+
obj.y = 40;
|
|
105
|
+
invalidateNodeLocalTransform(obj);
|
|
106
|
+
setNodeHitTestEnabled(obj, true);
|
|
107
|
+
setNodeHitArea(obj, createRectangle(0, 0, 100, 100));
|
|
108
|
+
expect(findGraphHitTarget(obj, 50, 50)).toBe(obj);
|
|
109
|
+
expect(findGraphHitTarget(obj, 20, 20)).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('resolves a path hitArea by winding, and a node proxy in the proxy world space', () => {
|
|
113
|
+
const pathObj = createDisplayObject();
|
|
114
|
+
setNodeHitTestEnabled(pathObj, true);
|
|
115
|
+
const path = createPath();
|
|
116
|
+
appendPathRectangle(path, 0, 0, 30, 30);
|
|
117
|
+
setNodeHitArea(pathObj, path);
|
|
118
|
+
expect(findGraphHitTarget(pathObj, 10, 10)).toBe(pathObj);
|
|
119
|
+
expect(findGraphHitTarget(pathObj, 40, 40)).toBeNull();
|
|
120
|
+
|
|
121
|
+
const proxyOwner = createDisplayObject();
|
|
122
|
+
const proxy = createDisplayObjectGeneric(DisplayObjectKind);
|
|
123
|
+
setRectangle(getNodeLocalBoundsRectangle(proxy), 0, 0, 20, 20);
|
|
124
|
+
setNodeHitTestEnabled(proxyOwner, true);
|
|
125
|
+
setNodeHitArea(proxyOwner, proxy);
|
|
126
|
+
expect(findGraphHitTarget(proxyOwner, 5, 5)).toBe(proxyOwner);
|
|
127
|
+
expect(findGraphHitTarget(proxyOwner, 50, 50)).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('findGraphHitTargetPrecise', () => {
|
|
132
|
+
it('uses the kind exact provider, so a bbox hit off the real geometry misses', () => {
|
|
133
|
+
const kind = 'PreciseFirstKind';
|
|
134
|
+
const node = createDisplayObjectGeneric(kind);
|
|
135
|
+
setRectangle(getNodeLocalBoundsRectangle(node), 0, 0, 100, 100);
|
|
136
|
+
setNodeHitTestEnabled(node, true);
|
|
137
|
+
// Exact provider only accepts the left half.
|
|
138
|
+
registerHitTestPrecise(kind, (s, x, y) => (hitTestGraphLocalBounds(s, x, y) && x < 50 ? 0 : -1));
|
|
139
|
+
registerHitTest(kind, hitTestGraphLocalBounds);
|
|
140
|
+
expect(findGraphHitTargetPrecise(node, 25, 50)).toBe(node);
|
|
141
|
+
expect(findGraphHitTargetPrecise(node, 75, 50)).toBeNull();
|
|
142
|
+
// Coarse still hits the whole box.
|
|
143
|
+
expect(findGraphHitTarget(node, 75, 50)).toBe(node);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('falls back to the coarse bounds handler when no exact provider is registered', () => {
|
|
147
|
+
const obj = boundsObject(100, 100);
|
|
148
|
+
expect(findGraphHitTargetPrecise(obj, 50, 50)).toBe(obj);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('findGraphHitTargets', () => {
|
|
153
|
+
it('collects every hit under the point, front-to-back', () => {
|
|
154
|
+
const root = createDisplayObject();
|
|
155
|
+
const under = boundsObject(100, 100);
|
|
156
|
+
const over = boundsObject(100, 100);
|
|
157
|
+
addNodeChild(root, under);
|
|
158
|
+
addNodeChild(root, over);
|
|
159
|
+
const stack = findGraphHitTargets(root, 50, 50);
|
|
160
|
+
expect(stack).toEqual([over, under]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('clears the out array and returns it', () => {
|
|
164
|
+
const obj = boundsObject(100, 100);
|
|
165
|
+
const out: DisplayObject[] = [createDisplayObject()];
|
|
166
|
+
const result = findGraphHitTargets(obj, 50, 50, out);
|
|
167
|
+
expect(result).toBe(out);
|
|
168
|
+
expect(out).toEqual([obj]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('findGraphHitTargetsPrecise', () => {
|
|
173
|
+
it('collects hits using exact geometry per kind', () => {
|
|
174
|
+
const kind = 'PreciseStackKind';
|
|
175
|
+
registerHitTest(kind, hitTestGraphLocalBounds);
|
|
176
|
+
registerHitTestPrecise(kind, boundsPrecise);
|
|
177
|
+
const root = createDisplayObject();
|
|
178
|
+
const a = createDisplayObjectGeneric(kind);
|
|
179
|
+
setRectangle(getNodeLocalBoundsRectangle(a), 0, 0, 100, 100);
|
|
180
|
+
setNodeHitTestEnabled(a, true);
|
|
181
|
+
addNodeChild(root, a);
|
|
182
|
+
expect(findGraphHitTargetsPrecise(root, 50, 50)).toEqual([a]);
|
|
183
|
+
expect(findGraphHitTargetsPrecise(root, 500, 500)).toEqual([]);
|
|
30
184
|
});
|
|
31
185
|
});
|
|
32
186
|
|
|
@@ -37,53 +191,32 @@ describe('hitTestDisplayObjects', () => {
|
|
|
37
191
|
beforeEach(() => {
|
|
38
192
|
a = createDisplayObject();
|
|
39
193
|
b = createDisplayObject();
|
|
40
|
-
|
|
41
194
|
addNodeChild(createDisplayObject(), a);
|
|
42
195
|
addNodeChild(createDisplayObject(), b);
|
|
43
|
-
|
|
44
196
|
setRectangle(getNodeLocalBoundsRectangle(a), 0, 0, 10, 10);
|
|
45
197
|
setRectangle(getNodeLocalBoundsRectangle(b), 0, 0, 10, 10);
|
|
46
|
-
|
|
47
198
|
a.x = 0;
|
|
48
199
|
a.y = 0;
|
|
49
200
|
b.x = 5;
|
|
50
201
|
b.y = 5;
|
|
51
|
-
|
|
52
202
|
a.scaleX = a.scaleY = 1;
|
|
53
203
|
b.scaleX = b.scaleY = 1;
|
|
54
204
|
});
|
|
55
205
|
|
|
56
206
|
it('returns true when bounds intersect', () => {
|
|
57
|
-
|
|
58
|
-
expect(result).toBe(true);
|
|
207
|
+
expect(hitTestDisplayObjects(a, b)).toBe(true);
|
|
59
208
|
});
|
|
60
209
|
|
|
61
210
|
it('returns false when bounds do not intersect', () => {
|
|
62
211
|
b.x = 20;
|
|
63
212
|
b.y = 20;
|
|
64
213
|
invalidateNodeLocalTransform(b);
|
|
65
|
-
|
|
66
|
-
const result = hitTestDisplayObjects(a, b);
|
|
67
|
-
expect(result).toBe(false);
|
|
214
|
+
expect(hitTestDisplayObjects(a, b)).toBe(false);
|
|
68
215
|
});
|
|
69
216
|
|
|
70
217
|
it('returns false if either object has no parent', () => {
|
|
71
218
|
(getDisplayObjectRuntime(b) as DisplayObjectRuntime).parent = null;
|
|
72
|
-
|
|
73
|
-
const result = hitTestDisplayObjects(a, b);
|
|
74
|
-
expect(result).toBe(false);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('compares bounds in world space', () => {
|
|
78
|
-
a.scaleX = 1;
|
|
79
|
-
a.scaleY = 1;
|
|
80
|
-
|
|
81
|
-
b.x = 5;
|
|
82
|
-
b.y = 5;
|
|
83
|
-
invalidateNodeLocalTransform(b);
|
|
84
|
-
|
|
85
|
-
const result = hitTestDisplayObjects(a, b);
|
|
86
|
-
expect(result).toBe(true);
|
|
219
|
+
expect(hitTestDisplayObjects(a, b)).toBe(false);
|
|
87
220
|
});
|
|
88
221
|
|
|
89
222
|
it('includes child bounds when a child extends beyond the object local bounds', () => {
|
|
@@ -93,25 +226,18 @@ describe('hitTestDisplayObjects', () => {
|
|
|
93
226
|
invalidateNodeLocalTransform(child);
|
|
94
227
|
setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 20, 20);
|
|
95
228
|
addNodeChild(a, child);
|
|
96
|
-
|
|
97
229
|
b.x = 100;
|
|
98
230
|
b.y = 100;
|
|
99
231
|
invalidateNodeLocalTransform(b);
|
|
100
|
-
|
|
101
232
|
expect(hitTestDisplayObjects(a, b)).toBe(true);
|
|
102
233
|
});
|
|
103
234
|
});
|
|
104
235
|
|
|
105
236
|
describe('hitTestGraphLocalBounds', () => {
|
|
106
|
-
it('returns
|
|
237
|
+
it('returns whether a world-space point is inside local bounds', () => {
|
|
107
238
|
const obj = createDisplayObject();
|
|
108
239
|
setRectangle(getNodeLocalBoundsRectangle(obj), 0, 0, 100, 100);
|
|
109
240
|
expect(hitTestGraphLocalBounds(obj, 50, 50)).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('returns false when world-space point is outside local bounds', () => {
|
|
113
|
-
const obj = createDisplayObject();
|
|
114
|
-
setRectangle(getNodeLocalBoundsRectangle(obj), 0, 0, 100, 100);
|
|
115
241
|
expect(hitTestGraphLocalBounds(obj, 200, 200)).toBe(false);
|
|
116
242
|
});
|
|
117
243
|
});
|
|
@@ -119,88 +245,99 @@ describe('hitTestGraphLocalBounds', () => {
|
|
|
119
245
|
describe('hitTestGraphPoint', () => {
|
|
120
246
|
let obj: DisplayObject;
|
|
121
247
|
|
|
122
|
-
beforeAll(() => {
|
|
123
|
-
registerHitTestPoint(DisplayObjectKind, hitTestGraphLocalBounds);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
248
|
beforeEach(() => {
|
|
127
|
-
obj =
|
|
128
|
-
setRectangle(getNodeLocalBoundsRectangle(obj), 0, 0, 100, 100);
|
|
249
|
+
obj = boundsObject(100, 100);
|
|
129
250
|
});
|
|
130
251
|
|
|
131
|
-
it('returns true for point inside bounds', () => {
|
|
132
|
-
|
|
133
|
-
expect(
|
|
252
|
+
it('returns true for point inside bounds, false outside', () => {
|
|
253
|
+
expect(hitTestGraphPoint(obj, 50, 50)).toBe(true);
|
|
254
|
+
expect(hitTestGraphPoint(obj, 200, 200)).toBe(false);
|
|
134
255
|
});
|
|
135
256
|
|
|
136
|
-
it('returns false for
|
|
137
|
-
const
|
|
138
|
-
|
|
257
|
+
it('returns false for a node that has not opted in', () => {
|
|
258
|
+
const other = createDisplayObject();
|
|
259
|
+
setRectangle(getNodeLocalBoundsRectangle(other), 0, 0, 100, 100);
|
|
260
|
+
expect(hitTestGraphPoint(other, 50, 50)).toBe(false);
|
|
139
261
|
});
|
|
140
262
|
|
|
141
263
|
it('returns false if object is not enabled', () => {
|
|
142
264
|
obj.enabled = false;
|
|
143
|
-
|
|
144
|
-
expect(result).toBe(false);
|
|
265
|
+
expect(hitTestGraphPoint(obj, 50, 50)).toBe(false);
|
|
145
266
|
});
|
|
146
267
|
|
|
147
|
-
it('returns
|
|
148
|
-
const
|
|
149
|
-
|
|
268
|
+
it('returns true when an opted-in child is hit even if the parent is inert', () => {
|
|
269
|
+
const parent = createDisplayObject();
|
|
270
|
+
const child = boundsObject(100, 100);
|
|
271
|
+
addNodeChild(parent, child);
|
|
272
|
+
expect(hitTestGraphPoint(parent, 50, 50)).toBe(true);
|
|
150
273
|
});
|
|
151
274
|
|
|
152
|
-
it('
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
expect(inside).toBe(true);
|
|
160
|
-
expect(outside).toBe(false);
|
|
275
|
+
it('does not test children of a disabled parent', () => {
|
|
276
|
+
const parent = createDisplayObject();
|
|
277
|
+
parent.enabled = false;
|
|
278
|
+
const child = boundsObject(100, 100);
|
|
279
|
+
addNodeChild(parent, child);
|
|
280
|
+
expect(hitTestGraphPoint(parent, 50, 50)).toBe(false);
|
|
161
281
|
});
|
|
282
|
+
});
|
|
162
283
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
284
|
+
describe('hitTestGraphPointPrecise', () => {
|
|
285
|
+
it('uses exact geometry, so a bbox hit off the real geometry misses', () => {
|
|
286
|
+
const kind = 'PreciseAnyKind';
|
|
287
|
+
const node = createDisplayObjectGeneric(kind);
|
|
288
|
+
setRectangle(getNodeLocalBoundsRectangle(node), 0, 0, 100, 100);
|
|
289
|
+
setNodeHitTestEnabled(node, true);
|
|
290
|
+
registerHitTestPrecise(kind, (s, x, y) => (hitTestGraphLocalBounds(s, x, y) && x < 50 ? 0 : -1));
|
|
291
|
+
expect(hitTestGraphPointPrecise(node, 25, 50)).toBe(true);
|
|
292
|
+
expect(hitTestGraphPointPrecise(node, 75, 50)).toBe(false);
|
|
169
293
|
});
|
|
294
|
+
});
|
|
170
295
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
expect(hitTestGraphPoint(obj, 50, 50)).toBe(true);
|
|
296
|
+
describe('hitTestNodeRegion', () => {
|
|
297
|
+
it('tests a node kind geometry, ignoring eligibility and children', () => {
|
|
298
|
+
const obj = createDisplayObject();
|
|
299
|
+
setRectangle(getNodeLocalBoundsRectangle(obj), 0, 0, 100, 100);
|
|
300
|
+
expect(hitTestNodeRegion(obj, 50, 50)).toBe(true);
|
|
301
|
+
expect(hitTestNodeRegion(obj, 200, 200)).toBe(false);
|
|
178
302
|
});
|
|
179
303
|
|
|
180
|
-
it('
|
|
181
|
-
obj
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
addNodeChild(obj, child);
|
|
186
|
-
|
|
187
|
-
expect(hitTestGraphPoint(obj, 50, 50)).toBe(false);
|
|
304
|
+
it('uses the hitArea when one is set', () => {
|
|
305
|
+
const obj = createDisplayObject();
|
|
306
|
+
setNodeHitArea(obj, createRectangle(0, 0, 30, 30));
|
|
307
|
+
expect(hitTestNodeRegion(obj, 10, 10)).toBe(true);
|
|
308
|
+
expect(hitTestNodeRegion(obj, 40, 40)).toBe(false);
|
|
188
309
|
});
|
|
189
310
|
|
|
190
|
-
it('uses
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expect(
|
|
311
|
+
it('uses the exact provider when precise', () => {
|
|
312
|
+
const kind = 'RegionPreciseKind';
|
|
313
|
+
const node = createDisplayObjectGeneric(kind);
|
|
314
|
+
setRectangle(getNodeLocalBoundsRectangle(node), 0, 0, 100, 100);
|
|
315
|
+
registerHitTestPrecise(kind, (s, x, y) => (hitTestGraphLocalBounds(s, x, y) && x < 50 ? 0 : -1));
|
|
316
|
+
expect(hitTestNodeRegion(node, 25, 50, true)).toBe(true);
|
|
317
|
+
expect(hitTestNodeRegion(node, 75, 50, true)).toBe(false);
|
|
196
318
|
});
|
|
197
319
|
});
|
|
198
320
|
|
|
199
|
-
describe('
|
|
200
|
-
it('registers a handler that hitTestGraphPoint will use', () => {
|
|
321
|
+
describe('registerHitTest', () => {
|
|
322
|
+
it('registers a coarse handler that hitTestGraphPoint will use', () => {
|
|
201
323
|
const kind = 'RegisterTest';
|
|
202
|
-
|
|
324
|
+
registerHitTest(kind, () => true);
|
|
203
325
|
const node = createDisplayObjectGeneric(kind);
|
|
326
|
+
setNodeHitTestEnabled(node, true);
|
|
204
327
|
expect(hitTestGraphPoint(node, 0, 0)).toBe(true);
|
|
205
328
|
});
|
|
206
329
|
});
|
|
330
|
+
|
|
331
|
+
describe('registerHitTestPrecise', () => {
|
|
332
|
+
it('registers an exact provider used by the precise queries and describeGraphHit', () => {
|
|
333
|
+
const kind = 'RegisterPreciseTest';
|
|
334
|
+
registerHitTest(kind, () => true);
|
|
335
|
+
registerHitTestPrecise(kind, () => 3);
|
|
336
|
+
const node = createDisplayObjectGeneric(kind);
|
|
337
|
+
setNodeHitTestEnabled(node, true);
|
|
338
|
+
expect(findGraphHitTargetPrecise(node, 0, 0)).toBe(node);
|
|
339
|
+
const out = emptyResult(node);
|
|
340
|
+
describeGraphHit(node, 0, 0, out);
|
|
341
|
+
expect(out.subIndex).toBe(3);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
@@ -2,11 +2,11 @@ import { createDisplayObject } from '@flighthq/displayobject';
|
|
|
2
2
|
import { setRectangle } from '@flighthq/geometry';
|
|
3
3
|
import { addNodeChild, createNode, getNodeLocalBoundsRectangle, invalidateNodeLocalTransform } from '@flighthq/node';
|
|
4
4
|
import { connectSignal, createSignal, emitSignal } from '@flighthq/signals';
|
|
5
|
-
import type { InputKeyboardData, InputPointerData, InputSignals } from '@flighthq/types';
|
|
5
|
+
import type { Cursor, InputKeyboardData, InputPointerData, InputSignals } from '@flighthq/types';
|
|
6
6
|
import { DisplayObjectKind } from '@flighthq/types';
|
|
7
7
|
|
|
8
8
|
import { hitTestGraphLocalBounds } from './hitTests';
|
|
9
|
-
import {
|
|
9
|
+
import { registerHitTest } from './hitTests';
|
|
10
10
|
import {
|
|
11
11
|
captureInteractionPointer,
|
|
12
12
|
connectInputToInteraction,
|
|
@@ -25,10 +25,12 @@ import {
|
|
|
25
25
|
enableInteractionSignals,
|
|
26
26
|
getInteractionSignals,
|
|
27
27
|
releaseInteractionPointer,
|
|
28
|
+
setInteractionConnectGuard,
|
|
28
29
|
} from './interactionManager';
|
|
30
|
+
import { setNodeCursor, setNodeHitTestEnabled } from './nodeInteractionState';
|
|
29
31
|
|
|
30
32
|
beforeAll(() => {
|
|
31
|
-
|
|
33
|
+
registerHitTest(DisplayObjectKind, hitTestGraphLocalBounds);
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
describe('captureInteractionPointer', () => {
|
|
@@ -76,10 +78,11 @@ describe('connectInteractionSignal', () => {
|
|
|
76
78
|
it('can use tracked subscribers without scanning for direct signal connections', () => {
|
|
77
79
|
const kind = 'TrackedSubscriberHitTest';
|
|
78
80
|
const root = createNode(kind);
|
|
81
|
+
setNodeHitTestEnabled(root, true);
|
|
79
82
|
const manager = createInteractionManager(root, { trackedSubscribersOnly: true });
|
|
80
83
|
let fired = 0;
|
|
81
84
|
let hitTests = 0;
|
|
82
|
-
|
|
85
|
+
registerHitTest(kind, () => {
|
|
83
86
|
hitTests++;
|
|
84
87
|
return true;
|
|
85
88
|
});
|
|
@@ -98,10 +101,11 @@ describe('connectInteractionSignal', () => {
|
|
|
98
101
|
it('clears tracked once subscribers after dispatch', () => {
|
|
99
102
|
const kind = 'TrackedOnceHitTest';
|
|
100
103
|
const root = createNode(kind);
|
|
104
|
+
setNodeHitTestEnabled(root, true);
|
|
101
105
|
const manager = createInteractionManager(root, { trackedSubscribersOnly: true });
|
|
102
106
|
let fired = 0;
|
|
103
107
|
let hitTests = 0;
|
|
104
|
-
|
|
108
|
+
registerHitTest(kind, () => {
|
|
105
109
|
hitTests++;
|
|
106
110
|
return true;
|
|
107
111
|
});
|
|
@@ -137,6 +141,13 @@ describe('createInteractionManager', () => {
|
|
|
137
141
|
expect(manager.enabled).toBe(true);
|
|
138
142
|
expect(manager.trackedSubscribersOnly).toBe(true);
|
|
139
143
|
});
|
|
144
|
+
|
|
145
|
+
it('defaults cursorBackend to null and accepts one via options', () => {
|
|
146
|
+
const root = createDisplayObject();
|
|
147
|
+
expect(createInteractionManager(root).cursorBackend).toBeNull();
|
|
148
|
+
const backend = { setCursor: () => {} };
|
|
149
|
+
expect(createInteractionManager(root, { cursorBackend: backend }).cursorBackend).toBe(backend);
|
|
150
|
+
});
|
|
140
151
|
});
|
|
141
152
|
|
|
142
153
|
describe('createInteractionSignals', () => {
|
|
@@ -168,11 +179,12 @@ describe('disconnectInteractionSignal', () => {
|
|
|
168
179
|
it('disconnects a tracked subscriber and removes the dispatch cost', () => {
|
|
169
180
|
const kind = 'DisconnectTrackedHitTest';
|
|
170
181
|
const root = createNode(kind);
|
|
182
|
+
setNodeHitTestEnabled(root, true);
|
|
171
183
|
const manager = createInteractionManager(root, { trackedSubscribersOnly: true });
|
|
172
184
|
let fired = 0;
|
|
173
185
|
let hitTests = 0;
|
|
174
186
|
const slot = () => fired++;
|
|
175
|
-
|
|
187
|
+
registerHitTest(kind, () => {
|
|
176
188
|
hitTests++;
|
|
177
189
|
return true;
|
|
178
190
|
});
|
|
@@ -254,7 +266,7 @@ describe('dispatchInteractionPointerDown', () => {
|
|
|
254
266
|
const root = createNode(kind);
|
|
255
267
|
const manager = createInteractionManager(root);
|
|
256
268
|
let hitTests = 0;
|
|
257
|
-
|
|
269
|
+
registerHitTest(kind, () => {
|
|
258
270
|
hitTests++;
|
|
259
271
|
return true;
|
|
260
272
|
});
|
|
@@ -268,7 +280,7 @@ describe('dispatchInteractionPointerDown', () => {
|
|
|
268
280
|
const root = createNode(kind);
|
|
269
281
|
const manager = createInteractionManager(root);
|
|
270
282
|
let hitTests = 0;
|
|
271
|
-
|
|
283
|
+
registerHitTest(kind, () => {
|
|
272
284
|
hitTests++;
|
|
273
285
|
return true;
|
|
274
286
|
});
|
|
@@ -288,6 +300,7 @@ describe('dispatchInteractionPointerDown', () => {
|
|
|
288
300
|
const root = createDisplayObject();
|
|
289
301
|
const child = createDisplayObject();
|
|
290
302
|
setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
|
|
303
|
+
setNodeHitTestEnabled(child, true);
|
|
291
304
|
addNodeChild(root, child);
|
|
292
305
|
|
|
293
306
|
const signals = enableInteractionSignals(child);
|
|
@@ -303,6 +316,7 @@ describe('dispatchInteractionPointerDown', () => {
|
|
|
303
316
|
const root = createDisplayObject();
|
|
304
317
|
const child = createDisplayObject();
|
|
305
318
|
setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
|
|
319
|
+
setNodeHitTestEnabled(child, true);
|
|
306
320
|
addNodeChild(root, child);
|
|
307
321
|
|
|
308
322
|
const signals = enableInteractionSignals(child);
|
|
@@ -318,6 +332,7 @@ describe('dispatchInteractionPointerDown', () => {
|
|
|
318
332
|
const root = createDisplayObject();
|
|
319
333
|
const child = createDisplayObject();
|
|
320
334
|
setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
|
|
335
|
+
setNodeHitTestEnabled(child, true);
|
|
321
336
|
addNodeChild(root, child);
|
|
322
337
|
|
|
323
338
|
const signals = enableInteractionSignals(child);
|
|
@@ -341,6 +356,7 @@ describe('dispatchInteractionPointerDown', () => {
|
|
|
341
356
|
child.y = 20;
|
|
342
357
|
invalidateNodeLocalTransform(child);
|
|
343
358
|
setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
|
|
359
|
+
setNodeHitTestEnabled(child, true);
|
|
344
360
|
addNodeChild(root, child);
|
|
345
361
|
|
|
346
362
|
const manager = createInteractionManager(root);
|
|
@@ -379,6 +395,7 @@ describe('dispatchInteractionPointerDown', () => {
|
|
|
379
395
|
child.y = 7;
|
|
380
396
|
invalidateNodeLocalTransform(child);
|
|
381
397
|
setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
|
|
398
|
+
setNodeHitTestEnabled(child, true);
|
|
382
399
|
addNodeChild(root, parent);
|
|
383
400
|
addNodeChild(parent, child);
|
|
384
401
|
|
|
@@ -422,6 +439,23 @@ describe('dispatchInteractionPointerMove', () => {
|
|
|
422
439
|
expect(fired).toBe(1);
|
|
423
440
|
});
|
|
424
441
|
|
|
442
|
+
it('applies the rollover target cursor and clears it on exit — even with no move subscribers', () => {
|
|
443
|
+
const root = createDisplayObject();
|
|
444
|
+
const child = createDisplayObject();
|
|
445
|
+
setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
|
|
446
|
+
setNodeHitTestEnabled(child, true);
|
|
447
|
+
addNodeChild(root, child);
|
|
448
|
+
const applied: (Cursor | null)[] = [];
|
|
449
|
+
const manager = createInteractionManager(root, { cursorBackend: { setCursor: (c) => applied.push(c) } });
|
|
450
|
+
setNodeCursor(child, 'pointer');
|
|
451
|
+
|
|
452
|
+
dispatchInteractionPointerMove(manager, 50, 50);
|
|
453
|
+
expect(applied.at(-1)).toBe('pointer');
|
|
454
|
+
|
|
455
|
+
dispatchInteractionPointerMove(manager, 500, 500);
|
|
456
|
+
expect(applied.at(-1)).toBeNull();
|
|
457
|
+
});
|
|
458
|
+
|
|
425
459
|
it('fires over and roll over when entering a target', () => {
|
|
426
460
|
const { child, manager } = createHitScene();
|
|
427
461
|
const order: string[] = [];
|
|
@@ -562,10 +596,27 @@ describe('releaseInteractionPointer', () => {
|
|
|
562
596
|
});
|
|
563
597
|
});
|
|
564
598
|
|
|
599
|
+
describe('setInteractionConnectGuard', () => {
|
|
600
|
+
it('invokes the installed guard when a signal is connected, and stops after null', () => {
|
|
601
|
+
const root = createDisplayObject();
|
|
602
|
+
const manager = createInteractionManager(root);
|
|
603
|
+
const seen: string[] = [];
|
|
604
|
+
setInteractionConnectGuard((target, name) => {
|
|
605
|
+
void target;
|
|
606
|
+
seen.push(name);
|
|
607
|
+
});
|
|
608
|
+
connectInteractionSignal(manager, root, 'onPointerDown', () => {});
|
|
609
|
+
setInteractionConnectGuard(null);
|
|
610
|
+
connectInteractionSignal(manager, root, 'onPointerUp', () => {});
|
|
611
|
+
expect(seen).toEqual(['onPointerDown']);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
565
615
|
function createHitScene() {
|
|
566
616
|
const root = createDisplayObject();
|
|
567
617
|
const child = createDisplayObject();
|
|
568
618
|
setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
|
|
619
|
+
setNodeHitTestEnabled(child, true);
|
|
569
620
|
addNodeChild(root, child);
|
|
570
621
|
return { child, manager: createInteractionManager(root), root };
|
|
571
622
|
}
|