@flighthq/interaction 0.1.0

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 (40) hide show
  1. package/dist/displayHitTests.d.ts +13 -0
  2. package/dist/displayHitTests.d.ts.map +1 -0
  3. package/dist/displayHitTests.js +38 -0
  4. package/dist/displayHitTests.js.map +1 -0
  5. package/dist/displayObjectOverlap.d.ts +5 -0
  6. package/dist/displayObjectOverlap.d.ts.map +1 -0
  7. package/dist/displayObjectOverlap.js +21 -0
  8. package/dist/displayObjectOverlap.js.map +1 -0
  9. package/dist/hitTests.d.ts +38 -0
  10. package/dist/hitTests.d.ts.map +1 -0
  11. package/dist/hitTests.js +80 -0
  12. package/dist/hitTests.js.map +1 -0
  13. package/dist/index.d.ts +8 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +8 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/interactionManager.d.ts +38 -0
  18. package/dist/interactionManager.d.ts.map +1 -0
  19. package/dist/interactionManager.js +470 -0
  20. package/dist/interactionManager.js.map +1 -0
  21. package/dist/registerDefaultHitTestPoints.d.ts +2 -0
  22. package/dist/registerDefaultHitTestPoints.d.ts.map +1 -0
  23. package/dist/registerDefaultHitTestPoints.js +21 -0
  24. package/dist/registerDefaultHitTestPoints.js.map +1 -0
  25. package/dist/spatialQuery.d.ts +4 -0
  26. package/dist/spatialQuery.d.ts.map +1 -0
  27. package/dist/spatialQuery.js +37 -0
  28. package/dist/spatialQuery.js.map +1 -0
  29. package/dist/spriteHitTests.d.ts +5 -0
  30. package/dist/spriteHitTests.d.ts.map +1 -0
  31. package/dist/spriteHitTests.js +11 -0
  32. package/dist/spriteHitTests.js.map +1 -0
  33. package/package.json +43 -0
  34. package/src/displayHitTests.test.ts +137 -0
  35. package/src/displayObjectOverlap.test.ts +78 -0
  36. package/src/hitTests.test.ts +206 -0
  37. package/src/interactionManager.test.ts +635 -0
  38. package/src/registerDefaultHitTestPoints.test.ts +29 -0
  39. package/src/spatialQuery.test.ts +47 -0
  40. package/src/spriteHitTests.test.ts +76 -0
@@ -0,0 +1,635 @@
1
+ import { createDisplayObject } from '@flighthq/displayobject';
2
+ import { setRectangle } from '@flighthq/geometry';
3
+ import { addNodeChild, createNode, getNodeLocalBoundsRectangle, invalidateNodeLocalTransform } from '@flighthq/node';
4
+ import { connectSignal, createSignal, emitSignal } from '@flighthq/signals';
5
+ import type { InputKeyboardData, InputPointerData, InputSignals } from '@flighthq/types';
6
+ import { DisplayObjectKind } from '@flighthq/types';
7
+
8
+ import { hitTestGraphLocalBounds } from './hitTests';
9
+ import { registerHitTestPoint } from './hitTests';
10
+ import {
11
+ captureInteractionPointer,
12
+ connectInputToInteraction,
13
+ connectInteractionSignal,
14
+ createInteractionManager,
15
+ createInteractionSignals,
16
+ disconnectInteractionSignal,
17
+ dispatchInteractionContextMenu,
18
+ dispatchInteractionKeyDown,
19
+ dispatchInteractionKeyUp,
20
+ dispatchInteractionPointerCancel,
21
+ dispatchInteractionPointerDown,
22
+ dispatchInteractionPointerMove,
23
+ dispatchInteractionPointerUp,
24
+ dispatchInteractionWheel,
25
+ enableInteractionSignals,
26
+ getInteractionSignals,
27
+ releaseInteractionPointer,
28
+ } from './interactionManager';
29
+
30
+ beforeAll(() => {
31
+ registerHitTestPoint(DisplayObjectKind, hitTestGraphLocalBounds);
32
+ });
33
+
34
+ describe('captureInteractionPointer', () => {
35
+ it('routes pointer events to the captured target', () => {
36
+ const { child, manager } = createHitScene();
37
+ let fired = 0;
38
+ connectSignal(enableInteractionSignals(child).onPointerMove, () => fired++);
39
+
40
+ captureInteractionPointer(manager, 3, child);
41
+ dispatchInteractionPointerMove(manager, 500, 500, 0, { pointerId: 3 });
42
+ expect(fired).toBe(1);
43
+ });
44
+ });
45
+
46
+ describe('connectInputToInteraction', () => {
47
+ it('routes normalized input signals into interaction dispatch', () => {
48
+ const input = createInputSource();
49
+ const { child, manager } = createHitScene();
50
+ let fired = 0;
51
+ connectSignal(enableInteractionSignals(child).onPointerDown, () => fired++);
52
+
53
+ const disconnect = connectInputToInteraction(input, manager);
54
+ emitSignal(input.onPointerDown, createInputPointerData(50, 50));
55
+ disconnect();
56
+ emitSignal(input.onPointerDown, createInputPointerData(50, 50));
57
+ expect(fired).toBe(1);
58
+ });
59
+
60
+ it('routes normalized keyboard input into interaction dispatch', () => {
61
+ const input = createInputSource();
62
+ const root = createDisplayObject();
63
+ const manager = createInteractionManager(root);
64
+ let received = '';
65
+ connectSignal(enableInteractionSignals(root).onKeyDown, (data) => {
66
+ received = data.key;
67
+ });
68
+
69
+ connectInputToInteraction(input, manager);
70
+ emitSignal(input.onKeyDown, createInputKeyboardData('a', 97));
71
+ expect(received).toBe('a');
72
+ });
73
+ });
74
+
75
+ describe('connectInteractionSignal', () => {
76
+ it('can use tracked subscribers without scanning for direct signal connections', () => {
77
+ const kind = 'TrackedSubscriberHitTest';
78
+ const root = createNode(kind);
79
+ const manager = createInteractionManager(root, { trackedSubscribersOnly: true });
80
+ let fired = 0;
81
+ let hitTests = 0;
82
+ registerHitTestPoint(kind, () => {
83
+ hitTests++;
84
+ return true;
85
+ });
86
+
87
+ connectSignal(enableInteractionSignals(root).onPointerDown, () => fired++);
88
+ dispatchInteractionPointerDown(manager, 50, 50);
89
+ expect(fired).toBe(0);
90
+ expect(hitTests).toBe(0);
91
+
92
+ connectInteractionSignal(manager, root, 'onPointerDown', () => fired++);
93
+ dispatchInteractionPointerDown(manager, 50, 50);
94
+ expect(fired).toBe(2);
95
+ expect(hitTests).toBe(1);
96
+ });
97
+
98
+ it('clears tracked once subscribers after dispatch', () => {
99
+ const kind = 'TrackedOnceHitTest';
100
+ const root = createNode(kind);
101
+ const manager = createInteractionManager(root, { trackedSubscribersOnly: true });
102
+ let fired = 0;
103
+ let hitTests = 0;
104
+ registerHitTestPoint(kind, () => {
105
+ hitTests++;
106
+ return true;
107
+ });
108
+
109
+ connectInteractionSignal(manager, root, 'onPointerDown', () => fired++, { once: true });
110
+ dispatchInteractionPointerDown(manager, 50, 50);
111
+ dispatchInteractionPointerDown(manager, 50, 50);
112
+ expect(fired).toBe(1);
113
+ expect(hitTests).toBe(1);
114
+ });
115
+ });
116
+
117
+ describe('createInteractionManager', () => {
118
+ it('creates an enabled manager by default', () => {
119
+ const root = createDisplayObject();
120
+ const manager = createInteractionManager(root);
121
+ expect(manager.enabled).toBe(true);
122
+ expect(manager.pointerCaptures.size).toBe(0);
123
+ expect(manager.pointerStates.size).toBe(0);
124
+ expect(manager.root).toBe(root);
125
+ expect(manager.trackedSubscribersOnly).toBe(false);
126
+ });
127
+
128
+ it('can create a disabled manager', () => {
129
+ const root = createDisplayObject();
130
+ const manager = createInteractionManager(root, { enabled: false });
131
+ expect(manager.enabled).toBe(false);
132
+ });
133
+
134
+ it('can create a manager that uses only tracked subscribers', () => {
135
+ const root = createDisplayObject();
136
+ const manager = createInteractionManager(root, { trackedSubscribersOnly: true });
137
+ expect(manager.enabled).toBe(true);
138
+ expect(manager.trackedSubscribersOnly).toBe(true);
139
+ });
140
+ });
141
+
142
+ describe('createInteractionSignals', () => {
143
+ it('returns an object with interaction signals', () => {
144
+ const signals = createInteractionSignals();
145
+ expect(signals.onClick).toBeDefined();
146
+ expect(signals.onContextMenu).toBeDefined();
147
+ expect(signals.onDoubleClick).toBeDefined();
148
+ expect(signals.onKeyDown).toBeDefined();
149
+ expect(signals.onKeyUp).toBeDefined();
150
+ expect(signals.onPointerCancel).toBeDefined();
151
+ expect(signals.onPointerDown).toBeDefined();
152
+ expect(signals.onPointerMove).toBeDefined();
153
+ expect(signals.onPointerOut).toBeDefined();
154
+ expect(signals.onPointerOver).toBeDefined();
155
+ expect(signals.onPointerRollOut).toBeDefined();
156
+ expect(signals.onPointerRollOver).toBeDefined();
157
+ expect(signals.onPointerUp).toBeDefined();
158
+ expect(signals.onReleaseOutside).toBeDefined();
159
+ expect(signals.onWheel).toBeDefined();
160
+ });
161
+
162
+ it('returns a new object each call', () => {
163
+ expect(createInteractionSignals()).not.toBe(createInteractionSignals());
164
+ });
165
+ });
166
+
167
+ describe('disconnectInteractionSignal', () => {
168
+ it('disconnects a tracked subscriber and removes the dispatch cost', () => {
169
+ const kind = 'DisconnectTrackedHitTest';
170
+ const root = createNode(kind);
171
+ const manager = createInteractionManager(root, { trackedSubscribersOnly: true });
172
+ let fired = 0;
173
+ let hitTests = 0;
174
+ const slot = () => fired++;
175
+ registerHitTestPoint(kind, () => {
176
+ hitTests++;
177
+ return true;
178
+ });
179
+
180
+ connectInteractionSignal(manager, root, 'onPointerDown', slot);
181
+ disconnectInteractionSignal(manager, root, 'onPointerDown', slot);
182
+ dispatchInteractionPointerDown(manager, 50, 50);
183
+ expect(fired).toBe(0);
184
+ expect(hitTests).toBe(0);
185
+ });
186
+ });
187
+
188
+ describe('dispatchInteractionContextMenu', () => {
189
+ it('fires onContextMenu on a hit target', () => {
190
+ const { child, manager } = createHitScene();
191
+ let fired = 0;
192
+ connectSignal(enableInteractionSignals(child).onContextMenu, () => fired++);
193
+
194
+ dispatchInteractionContextMenu(manager, 50, 50);
195
+ expect(fired).toBe(1);
196
+ });
197
+ });
198
+
199
+ describe('dispatchInteractionKeyDown', () => {
200
+ it('fires onKeyDown on the manager root', () => {
201
+ const root = createDisplayObject();
202
+ const manager = createInteractionManager(root);
203
+ let received = '';
204
+ connectSignal(enableInteractionSignals(root).onKeyDown, (data) => {
205
+ received = data.key;
206
+ });
207
+
208
+ dispatchInteractionKeyDown(manager, 'a', 65);
209
+ expect(received).toBe('a');
210
+ });
211
+ });
212
+
213
+ describe('dispatchInteractionKeyUp', () => {
214
+ it('fires onKeyUp on the manager root', () => {
215
+ const root = createDisplayObject();
216
+ const manager = createInteractionManager(root);
217
+ let received = 0;
218
+ connectSignal(enableInteractionSignals(root).onKeyUp, (data) => {
219
+ received = data.keyCode;
220
+ });
221
+
222
+ dispatchInteractionKeyUp(manager, 'a', 65);
223
+ expect(received).toBe(65);
224
+ });
225
+ });
226
+
227
+ describe('dispatchInteractionPointerCancel', () => {
228
+ it('fires onPointerCancel on the active pointer target', () => {
229
+ const { child, manager } = createHitScene();
230
+ let fired = 0;
231
+ connectSignal(enableInteractionSignals(child).onPointerCancel, () => fired++);
232
+
233
+ dispatchInteractionPointerDown(manager, 50, 50);
234
+ dispatchInteractionPointerCancel(manager, 60, 60);
235
+ expect(fired).toBe(1);
236
+ });
237
+
238
+ it('clears pointer capture', () => {
239
+ const { child, manager } = createHitScene();
240
+ let fired = 0;
241
+ connectSignal(enableInteractionSignals(child).onPointerCancel, () => fired++);
242
+ connectSignal(enableInteractionSignals(child).onPointerMove, () => fired++);
243
+
244
+ captureInteractionPointer(manager, 3, child);
245
+ dispatchInteractionPointerCancel(manager, 500, 500, { pointerId: 3 });
246
+ dispatchInteractionPointerMove(manager, 500, 500, 0, { pointerId: 3 });
247
+ expect(fired).toBe(1);
248
+ });
249
+ });
250
+
251
+ describe('dispatchInteractionPointerDown', () => {
252
+ it('does not hit test when no dependent signal has subscribers', () => {
253
+ const kind = 'NoSubscriberHitTest';
254
+ const root = createNode(kind);
255
+ const manager = createInteractionManager(root);
256
+ let hitTests = 0;
257
+ registerHitTestPoint(kind, () => {
258
+ hitTests++;
259
+ return true;
260
+ });
261
+
262
+ dispatchInteractionPointerDown(manager, 50, 50);
263
+ expect(hitTests).toBe(0);
264
+ });
265
+
266
+ it('does not hit test when only unrelated signals have subscribers', () => {
267
+ const kind = 'UnrelatedSubscriberHitTest';
268
+ const root = createNode(kind);
269
+ const manager = createInteractionManager(root);
270
+ let hitTests = 0;
271
+ registerHitTestPoint(kind, () => {
272
+ hitTests++;
273
+ return true;
274
+ });
275
+ connectSignal(enableInteractionSignals(root).onWheel, () => {});
276
+
277
+ dispatchInteractionPointerDown(manager, 50, 50);
278
+ expect(hitTests).toBe(0);
279
+ });
280
+
281
+ it('does nothing when no hit target is found', () => {
282
+ const root = createDisplayObject();
283
+ const manager = createInteractionManager(root);
284
+ expect(() => dispatchInteractionPointerDown(manager, 50, 50)).not.toThrow();
285
+ });
286
+
287
+ it('does nothing when the manager is disabled', () => {
288
+ const root = createDisplayObject();
289
+ const child = createDisplayObject();
290
+ setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
291
+ addNodeChild(root, child);
292
+
293
+ const signals = enableInteractionSignals(child);
294
+ const manager = createInteractionManager(root, { enabled: false });
295
+ let fired = 0;
296
+ connectSignal(signals.onPointerDown, () => fired++);
297
+
298
+ dispatchInteractionPointerDown(manager, 50, 50);
299
+ expect(fired).toBe(0);
300
+ });
301
+
302
+ it('fires onPointerDown on a hit target', () => {
303
+ const root = createDisplayObject();
304
+ const child = createDisplayObject();
305
+ setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
306
+ addNodeChild(root, child);
307
+
308
+ const signals = enableInteractionSignals(child);
309
+ const manager = createInteractionManager(root);
310
+ let fired = 0;
311
+ connectSignal(signals.onPointerDown, () => fired++);
312
+
313
+ dispatchInteractionPointerDown(manager, 50, 50);
314
+ expect(fired).toBe(1);
315
+ });
316
+
317
+ it('passes correct pointer data to the handler', () => {
318
+ const root = createDisplayObject();
319
+ const child = createDisplayObject();
320
+ setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
321
+ addNodeChild(root, child);
322
+
323
+ const signals = enableInteractionSignals(child);
324
+ const manager = createInteractionManager(root);
325
+ let receivedX = 0;
326
+ let receivedY = 0;
327
+ connectSignal(signals.onPointerDown, (data) => {
328
+ receivedX = data.x;
329
+ receivedY = data.y;
330
+ });
331
+
332
+ dispatchInteractionPointerDown(manager, 30, 40);
333
+ expect(receivedX).toBe(30);
334
+ expect(receivedY).toBe(40);
335
+ });
336
+
337
+ it('passes target, current target, local coordinates, and pointer metadata', () => {
338
+ const root = createDisplayObject();
339
+ const child = createDisplayObject();
340
+ child.x = 10;
341
+ child.y = 20;
342
+ invalidateNodeLocalTransform(child);
343
+ setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
344
+ addNodeChild(root, child);
345
+
346
+ const manager = createInteractionManager(root);
347
+ let receivedCurrentTarget = null;
348
+ let receivedLocalX = 0;
349
+ let receivedLocalY = 0;
350
+ let receivedPointerId = 0;
351
+ let receivedPointerType = '';
352
+ let receivedTarget = null;
353
+ connectSignal(enableInteractionSignals(child).onPointerDown, (data) => {
354
+ receivedCurrentTarget = data.currentTarget;
355
+ receivedLocalX = data.localX;
356
+ receivedLocalY = data.localY;
357
+ receivedPointerId = data.pointerId;
358
+ receivedPointerType = data.pointerType;
359
+ receivedTarget = data.target;
360
+ });
361
+
362
+ dispatchInteractionPointerDown(manager, 30, 40, 0, { pointerId: 7, pointerType: 'pen' });
363
+ expect(receivedCurrentTarget).toBe(child);
364
+ expect(receivedLocalX).toBe(20);
365
+ expect(receivedLocalY).toBe(20);
366
+ expect(receivedPointerId).toBe(7);
367
+ expect(receivedPointerType).toBe('pen');
368
+ expect(receivedTarget).toBe(child);
369
+ });
370
+
371
+ it('passes bubbled target, current target, and ancestor local coordinates', () => {
372
+ const root = createDisplayObject();
373
+ const parent = createDisplayObject();
374
+ const child = createDisplayObject();
375
+ parent.x = 10;
376
+ parent.y = 20;
377
+ invalidateNodeLocalTransform(parent);
378
+ child.x = 5;
379
+ child.y = 7;
380
+ invalidateNodeLocalTransform(child);
381
+ setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
382
+ addNodeChild(root, parent);
383
+ addNodeChild(parent, child);
384
+
385
+ const manager = createInteractionManager(root);
386
+ let receivedCurrentTarget = null;
387
+ let receivedLocalX = 0;
388
+ let receivedLocalY = 0;
389
+ let receivedTarget = null;
390
+ connectSignal(enableInteractionSignals(parent).onPointerDown, (data) => {
391
+ receivedCurrentTarget = data.currentTarget;
392
+ receivedLocalX = data.localX;
393
+ receivedLocalY = data.localY;
394
+ receivedTarget = data.target;
395
+ });
396
+
397
+ dispatchInteractionPointerDown(manager, 25, 37);
398
+ expect(receivedCurrentTarget).toBe(parent);
399
+ expect(receivedLocalX).toBe(15);
400
+ expect(receivedLocalY).toBe(17);
401
+ expect(receivedTarget).toBe(child);
402
+ });
403
+
404
+ it('tracks a click target when only onClick is connected', () => {
405
+ const { child, manager } = createHitScene();
406
+ let fired = 0;
407
+ connectSignal(enableInteractionSignals(child).onClick, () => fired++);
408
+
409
+ dispatchInteractionPointerDown(manager, 50, 50);
410
+ dispatchInteractionPointerUp(manager, 50, 50);
411
+ expect(fired).toBe(1);
412
+ });
413
+ });
414
+
415
+ describe('dispatchInteractionPointerMove', () => {
416
+ it('fires onPointerMove on a hit target', () => {
417
+ const { child, manager } = createHitScene();
418
+ let fired = 0;
419
+ connectSignal(enableInteractionSignals(child).onPointerMove, () => fired++);
420
+
421
+ dispatchInteractionPointerMove(manager, 50, 50);
422
+ expect(fired).toBe(1);
423
+ });
424
+
425
+ it('fires over and roll over when entering a target', () => {
426
+ const { child, manager } = createHitScene();
427
+ const order: string[] = [];
428
+ const signals = enableInteractionSignals(child);
429
+ connectSignal(signals.onPointerOver, () => order.push('over'));
430
+ connectSignal(signals.onPointerRollOver, () => order.push('rollOver'));
431
+
432
+ dispatchInteractionPointerMove(manager, 50, 50);
433
+ expect(order).toEqual(['rollOver', 'over']);
434
+ });
435
+
436
+ it('fires out and roll out when leaving a target', () => {
437
+ const { child, manager } = createHitScene();
438
+ const order: string[] = [];
439
+ const signals = enableInteractionSignals(child);
440
+ connectSignal(signals.onPointerOut, () => order.push('out'));
441
+ connectSignal(signals.onPointerRollOut, () => order.push('rollOut'));
442
+
443
+ dispatchInteractionPointerMove(manager, 50, 50);
444
+ dispatchInteractionPointerMove(manager, 500, 500);
445
+ expect(order).toEqual(['out', 'rollOut']);
446
+ });
447
+ });
448
+
449
+ describe('dispatchInteractionPointerUp', () => {
450
+ it('fires onPointerUp on a hit target', () => {
451
+ const { child, manager } = createHitScene();
452
+ let fired = 0;
453
+ connectSignal(enableInteractionSignals(child).onPointerUp, () => fired++);
454
+
455
+ dispatchInteractionPointerUp(manager, 50, 50);
456
+ expect(fired).toBe(1);
457
+ });
458
+
459
+ it('fires onClick after down and up on the same target', () => {
460
+ const { child, manager } = createHitScene();
461
+ let fired = 0;
462
+ connectSignal(enableInteractionSignals(child).onClick, () => fired++);
463
+
464
+ dispatchInteractionPointerDown(manager, 50, 50);
465
+ dispatchInteractionPointerUp(manager, 50, 50);
466
+ expect(fired).toBe(1);
467
+ });
468
+
469
+ it('fires onDoubleClick for two clicks within the manager delay', () => {
470
+ const { child, manager } = createHitScene();
471
+ let fired = 0;
472
+ connectSignal(enableInteractionSignals(child).onDoubleClick, () => fired++);
473
+
474
+ dispatchInteractionPointerDown(manager, 50, 50);
475
+ dispatchInteractionPointerUp(manager, 50, 50, 0, 1000);
476
+ dispatchInteractionPointerDown(manager, 50, 50);
477
+ dispatchInteractionPointerUp(manager, 50, 50, 0, 1200);
478
+ expect(fired).toBe(1);
479
+ });
480
+
481
+ it('tracks double clicks independently by pointer id', () => {
482
+ const { child, manager } = createHitScene();
483
+ let fired = 0;
484
+ connectSignal(enableInteractionSignals(child).onDoubleClick, () => fired++);
485
+
486
+ dispatchInteractionPointerDown(manager, 50, 50, 0, { pointerId: 1 });
487
+ dispatchInteractionPointerUp(manager, 50, 50, 0, 1000, { pointerId: 1 });
488
+ dispatchInteractionPointerDown(manager, 50, 50, 0, { pointerId: 2 });
489
+ dispatchInteractionPointerUp(manager, 50, 50, 0, 1200, { pointerId: 2 });
490
+ dispatchInteractionPointerDown(manager, 50, 50, 0, { pointerId: 1 });
491
+ dispatchInteractionPointerUp(manager, 50, 50, 0, 1300, { pointerId: 1 });
492
+ expect(fired).toBe(1);
493
+ });
494
+
495
+ it('fires onReleaseOutside on the original down target', () => {
496
+ const { child, manager } = createHitScene();
497
+ let fired = 0;
498
+ connectSignal(enableInteractionSignals(child).onReleaseOutside, () => fired++);
499
+
500
+ dispatchInteractionPointerDown(manager, 50, 50);
501
+ dispatchInteractionPointerUp(manager, 500, 500);
502
+ expect(fired).toBe(1);
503
+ });
504
+ });
505
+
506
+ describe('dispatchInteractionWheel', () => {
507
+ it('fires onWheel with delta values', () => {
508
+ const { child, manager } = createHitScene();
509
+ let receivedDeltaY = 0;
510
+ connectSignal(enableInteractionSignals(child).onWheel, (data) => {
511
+ receivedDeltaY = data.deltaY;
512
+ });
513
+
514
+ dispatchInteractionWheel(manager, 50, 50, 0, -120);
515
+ expect(receivedDeltaY).toBe(-120);
516
+ });
517
+ });
518
+
519
+ describe('enableInteractionSignals', () => {
520
+ it('creates and returns interaction signals on first call', () => {
521
+ const obj = createDisplayObject();
522
+ const signals = enableInteractionSignals(obj);
523
+ expect(signals).toBeDefined();
524
+ expect(signals.onPointerDown).toBeDefined();
525
+ });
526
+
527
+ it('returns the same object on subsequent calls', () => {
528
+ const obj = createDisplayObject();
529
+ expect(enableInteractionSignals(obj)).toBe(enableInteractionSignals(obj));
530
+ });
531
+
532
+ it('makes getInteractionSignals return the enabled object', () => {
533
+ const obj = createDisplayObject();
534
+ const signals = enableInteractionSignals(obj);
535
+ expect(getInteractionSignals(obj)).toBe(signals);
536
+ });
537
+ });
538
+
539
+ describe('getInteractionSignals', () => {
540
+ it('returns null before signals are enabled', () => {
541
+ const obj = createDisplayObject();
542
+ expect(getInteractionSignals(obj)).toBeNull();
543
+ });
544
+
545
+ it('returns the signals after enableInteractionSignals', () => {
546
+ const obj = createDisplayObject();
547
+ const signals = enableInteractionSignals(obj);
548
+ expect(getInteractionSignals(obj)).toBe(signals);
549
+ });
550
+ });
551
+
552
+ describe('releaseInteractionPointer', () => {
553
+ it('stops routing pointer events to the captured target', () => {
554
+ const { child, manager } = createHitScene();
555
+ let fired = 0;
556
+ connectSignal(enableInteractionSignals(child).onPointerMove, () => fired++);
557
+
558
+ captureInteractionPointer(manager, 3, child);
559
+ releaseInteractionPointer(manager, 3);
560
+ dispatchInteractionPointerMove(manager, 500, 500, 0, { pointerId: 3 });
561
+ expect(fired).toBe(0);
562
+ });
563
+ });
564
+
565
+ function createHitScene() {
566
+ const root = createDisplayObject();
567
+ const child = createDisplayObject();
568
+ setRectangle(getNodeLocalBoundsRectangle(child), 0, 0, 100, 100);
569
+ addNodeChild(root, child);
570
+ return { child, manager: createInteractionManager(root), root };
571
+ }
572
+
573
+ function createInputKeyboardData(key: string, keyCode: number): InputKeyboardData {
574
+ return {
575
+ altKey: false,
576
+ capsLock: false,
577
+ code: key,
578
+ ctrlKey: false,
579
+ key,
580
+ keyCode,
581
+ location: 0,
582
+ metaKey: false,
583
+ modifier: 0,
584
+ numLock: false,
585
+ repeat: false,
586
+ shiftKey: false,
587
+ timeStamp: 0,
588
+ };
589
+ }
590
+
591
+ function createInputPointerData(x: number, y: number): InputPointerData {
592
+ return {
593
+ altKey: false,
594
+ button: 0,
595
+ buttons: 1,
596
+ ctrlKey: false,
597
+ deltaX: 0,
598
+ deltaY: 0,
599
+ height: 1,
600
+ isPrimary: true,
601
+ metaKey: false,
602
+ pointerId: 0,
603
+ pointerType: 'mouse',
604
+ pressure: 0,
605
+ shiftKey: false,
606
+ tiltX: 0,
607
+ tiltY: 0,
608
+ timeStamp: 0,
609
+ twist: 0,
610
+ wheelMode: 'unknown',
611
+ width: 1,
612
+ x,
613
+ y,
614
+ };
615
+ }
616
+
617
+ function createInputSource(): InputSignals {
618
+ return {
619
+ onGamepadAxisMove: createSignal(),
620
+ onGamepadButtonDown: createSignal(),
621
+ onGamepadButtonUp: createSignal(),
622
+ onGamepadConnect: createSignal(),
623
+ onGamepadDisconnect: createSignal(),
624
+ onKeyDown: createSignal(),
625
+ onKeyUp: createSignal(),
626
+ onPointerCancel: createSignal(),
627
+ onPointerDown: createSignal(),
628
+ onPointerMove: createSignal(),
629
+ onPointerMoveRelative: createSignal(),
630
+ onPointerUp: createSignal(),
631
+ onTextEdit: createSignal(),
632
+ onTextInput: createSignal(),
633
+ onWheel: createSignal(),
634
+ };
635
+ }
@@ -0,0 +1,29 @@
1
+ import { createBitmap, createStage } from '@flighthq/displayobject';
2
+ import { setRectangle } from '@flighthq/geometry';
3
+ import { getNodeLocalBoundsRectangle } from '@flighthq/node';
4
+ import { createShape } from '@flighthq/shape';
5
+
6
+ import { findGraphHitTarget } from './hitTests';
7
+ import { registerDefaultHitTestPoints } from './registerDefaultHitTestPoints';
8
+
9
+ describe('registerDefaultHitTestPoints', () => {
10
+ it('registers all built-in kinds so findGraphHitTarget resolves them', () => {
11
+ registerDefaultHitTestPoints();
12
+
13
+ const bitmap = createBitmap();
14
+ setRectangle(getNodeLocalBoundsRectangle(bitmap), 0, 0, 100, 100);
15
+ expect(findGraphHitTarget(bitmap, 50, 50)).toBe(bitmap);
16
+
17
+ const shape = createShape();
18
+ setRectangle(getNodeLocalBoundsRectangle(shape), 0, 0, 100, 100);
19
+ expect(findGraphHitTarget(shape, 50, 50)).toBe(shape);
20
+ });
21
+
22
+ it('containers return null for self hit', () => {
23
+ registerDefaultHitTestPoints();
24
+
25
+ const stage = createStage();
26
+ setRectangle(getNodeLocalBoundsRectangle(stage), 0, 0, 100, 100);
27
+ expect(findGraphHitTarget(stage, 50, 50)).toBeNull();
28
+ });
29
+ });
@@ -0,0 +1,47 @@
1
+ import { createDisplayObject } from '@flighthq/displayobject';
2
+ import { createRectangle, setRectangle } from '@flighthq/geometry';
3
+ import { addNodeChild, getNodeLocalBoundsRectangle } from '@flighthq/node';
4
+
5
+ import { hitTestAreaQuery, hitTestAreaQueryCircle } from './spatialQuery';
6
+
7
+ function makeAt(x: number, y: number, w: number, h: number) {
8
+ const obj = createDisplayObject();
9
+ setRectangle(getNodeLocalBoundsRectangle(obj), x, y, w, h);
10
+ return obj;
11
+ }
12
+
13
+ describe('hitTestAreaQuery', () => {
14
+ it('collects nodes whose world bounds intersect the query rectangle', () => {
15
+ const root = makeAt(0, 0, 200, 200);
16
+ const a = makeAt(10, 10, 20, 20);
17
+ const b = makeAt(150, 150, 20, 20);
18
+ addNodeChild(root, a);
19
+ addNodeChild(root, b);
20
+
21
+ const hits = hitTestAreaQuery(root, createRectangle(0, 0, 50, 50));
22
+ expect(hits).toContain(root);
23
+ expect(hits).toContain(a);
24
+ expect(hits).not.toContain(b);
25
+ });
26
+
27
+ it('returns empty array for disabled root', () => {
28
+ const root = makeAt(0, 0, 100, 100);
29
+ root.enabled = false;
30
+ expect(hitTestAreaQuery(root, createRectangle(0, 0, 50, 50))).toEqual([]);
31
+ });
32
+ });
33
+
34
+ describe('hitTestAreaQueryCircle', () => {
35
+ it('collects nodes whose world bounds intersect the query circle', () => {
36
+ const root = makeAt(0, 0, 200, 200);
37
+ const a = makeAt(10, 10, 20, 20);
38
+ const b = makeAt(180, 180, 10, 10);
39
+ addNodeChild(root, a);
40
+ addNodeChild(root, b);
41
+
42
+ const hits = hitTestAreaQueryCircle(root, 20, 20, 30);
43
+ expect(hits).toContain(root);
44
+ expect(hits).toContain(a);
45
+ expect(hits).not.toContain(b);
46
+ });
47
+ });