@@ -75,8 +75,13 @@ export function createEvents(store: SignalState<NgtState>) {
75
75
const intersections : NgtIntersection [ ] = [ ] ;
76
76
// Allow callers to eliminate event objects
77
77
const eventsObjects = filter ? filter ( state . internal . interaction ) : state . internal . interaction ;
78
- // Reset all raycaster cameras to undefined
79
- for ( let i = 0 ; i < eventsObjects . length ; i ++ ) {
78
+
79
+ // Skip work if there are no event objects
80
+ if ( eventsObjects . length === 0 ) return intersections ;
81
+
82
+ // Reset all raycaster cameras to undefined - use for loop for better performance
83
+ const eventsObjectsLen = eventsObjects . length ;
84
+ for ( let i = 0 ; i < eventsObjectsLen ; i ++ ) {
80
85
const objectRootState = getInstanceState ( eventsObjects [ i ] ) ?. store ?. snapshot ;
81
86
if ( objectRootState ) {
82
87
objectRootState . raycaster . camera = undefined ! ;
@@ -88,6 +93,9 @@ export function createEvents(store: SignalState<NgtState>) {
88
93
state . events . compute ?.( event , store , null ) ;
89
94
}
90
95
96
+ // Pre-allocate array to avoid garbage collection
97
+ const raycastResults : THREE . Intersection < THREE . Object3D > [ ] = [ ] ;
98
+
91
99
function handleRaycast ( obj : THREE . Object3D ) {
92
100
const objStore = getInstanceState ( obj ) ?. store ;
93
101
const objState = objStore ?. snapshot ;
@@ -106,30 +114,40 @@ export function createEvents(store: SignalState<NgtState>) {
106
114
}
107
115
108
116
// Collect events
109
- let hits : THREE . Intersection < THREE . Object3D > [ ] = eventsObjects
110
- // Intersect objects
111
- . flatMap ( handleRaycast )
112
- // Sort by event priority and distance
113
- . sort ( ( a , b ) => {
114
- const aState = getInstanceState ( a . object ) ?. store ?. snapshot ;
115
- const bState = getInstanceState ( b . object ) ?. store ?. snapshot ;
116
- if ( ! aState || ! bState ) return a . distance - b . distance ;
117
- return bState . events . priority - aState . events . priority || a . distance - b . distance ;
118
- } )
119
- // Filter out duplicates
120
- . filter ( ( item ) => {
121
- const id = makeId ( item as NgtIntersection ) ;
122
- if ( duplicates . has ( id ) ) return false ;
123
- duplicates . add ( id ) ;
124
- return true ;
125
- } ) ;
117
+ for ( let i = 0 ; i < eventsObjectsLen ; i ++ ) {
118
+ const objResults = handleRaycast ( eventsObjects [ i ] ) ;
119
+ if ( objResults . length <= 0 ) continue ;
120
+ for ( let j = 0 ; j < objResults . length ; j ++ ) {
121
+ raycastResults . push ( objResults [ j ] ) ;
122
+ }
123
+ }
124
+
125
+ // Sort by event priority and distance
126
+ raycastResults . sort ( ( a , b ) => {
127
+ const aState = getInstanceState ( a . object ) ?. store ?. snapshot ;
128
+ const bState = getInstanceState ( b . object ) ?. store ?. snapshot ;
129
+ if ( ! aState || ! bState ) return a . distance - b . distance ;
130
+ return bState . events . priority - aState . events . priority || a . distance - b . distance ;
131
+ } ) ;
132
+
133
+ // Filter out duplicates - more efficient than chaining
134
+ let hits : THREE . Intersection < THREE . Object3D > [ ] = [ ] ;
135
+ for ( let i = 0 ; i < raycastResults . length ; i ++ ) {
136
+ const item = raycastResults [ i ] ;
137
+ const id = makeId ( item as NgtIntersection ) ;
138
+ if ( duplicates . has ( id ) ) continue ;
139
+ duplicates . add ( id ) ;
140
+ hits . push ( item ) ;
141
+ }
126
142
127
143
// https://github.com/mrdoob/three.js/issues/16031
128
144
// Allow custom userland intersect sort order, this likely only makes sense on the root filter
129
145
if ( state . events . filter ) hits = state . events . filter ( hits , store ) ;
130
146
131
147
// Bubble up the events, find the event source (eventObject)
132
- for ( const hit of hits ) {
148
+ const hitsLen = hits . length ;
149
+ for ( let i = 0 ; i < hitsLen ; i ++ ) {
150
+ const hit = hits [ i ] ;
133
151
let eventObject : THREE . Object3D | null = hit . object ;
134
152
// bubble event up
135
153
while ( eventObject ) {
@@ -140,8 +158,10 @@ export function createEvents(store: SignalState<NgtState>) {
140
158
141
159
// If the interaction is captured, make all capturing targets part of the intersect.
142
160
if ( 'pointerId' in event && state . internal . capturedMap . has ( event . pointerId ) ) {
143
- for ( const captureData of state . internal . capturedMap . get ( event . pointerId ) ! . values ( ) ) {
144
- if ( ! duplicates . has ( makeId ( captureData . intersection ) ) ) intersections . push ( captureData . intersection ) ;
161
+ const captures = state . internal . capturedMap . get ( event . pointerId ) ! ;
162
+ for ( const captureData of captures . values ( ) ) {
163
+ if ( duplicates . has ( makeId ( captureData . intersection ) ) ) continue ;
164
+ intersections . push ( captureData . intersection ) ;
145
165
}
146
166
}
147
167
return intersections ;
@@ -300,30 +320,31 @@ export function createEvents(store: SignalState<NgtState>) {
300
320
}
301
321
302
322
function handlePointer ( name : string ) {
303
- // Deal with cancelation
304
- switch ( name ) {
305
- case 'pointerleave' :
306
- case 'pointercancel' :
307
- return ( ) => cancelPointer ( [ ] ) ;
308
- case 'lostpointercapture' :
309
- return ( event : NgtDomEvent ) => {
310
- const { internal } = store . snapshot ;
311
- if ( 'pointerId' in event && internal . capturedMap . has ( event . pointerId ) ) {
312
- // If the object event interface had lostpointercapture, we'd call it here on every
313
- // object that's getting removed. We call it on the next frame because lostpointercapture
314
- // fires before pointerup. Otherwise pointerUp would never be called if the event didn't
315
- // happen in the object it originated from, leaving components in a in-between state.
316
- requestAnimationFrame ( ( ) => {
317
- // Only release if pointer-up didn't do it already
318
- if ( internal . capturedMap . has ( event . pointerId ) ) {
319
- internal . capturedMap . delete ( event . pointerId ) ;
320
- cancelPointer ( [ ] ) ;
321
- }
322
- } ) ;
323
- }
324
- } ;
323
+ // Handle common cancelation events
324
+ if ( name === 'pointerleave' || name === 'pointercancel' ) {
325
+ return ( ) => cancelPointer ( [ ] ) ;
326
+ }
327
+
328
+ if ( name === 'lostpointercapture' ) {
329
+ return ( event : NgtDomEvent ) => {
330
+ const { internal } = store . snapshot ;
331
+ if ( 'pointerId' in event && internal . capturedMap . has ( event . pointerId ) ) {
332
+ // If the object event interface had lostpointercapture, we'd call it here on every
333
+ // object that's getting removed. We call it on the next frame because lostpointercapture
334
+ // fires before pointerup. Otherwise pointerUp would never be called if the event didn't
335
+ // happen in the object it originated from, leaving components in a in-between state.
336
+ requestAnimationFrame ( ( ) => {
337
+ // Only release if pointer-up didn't do it already
338
+ if ( internal . capturedMap . has ( event . pointerId ) ) {
339
+ internal . capturedMap . delete ( event . pointerId ) ;
340
+ cancelPointer ( [ ] ) ;
341
+ }
342
+ } ) ;
343
+ }
344
+ } ;
325
345
}
326
346
347
+ // Cache these values since they're used in the closure
327
348
const isPointerMove = name === 'pointermove' ;
328
349
const isClickEvent = name === 'click' || name === 'contextmenu' || name === 'dblclick' ;
329
350
const filter = isPointerMove ? filterPointerEvents : undefined ;
@@ -334,11 +355,12 @@ export function createEvents(store: SignalState<NgtState>) {
334
355
const pointerMissed$ : Subject < MouseEvent > = ( store as NgtAnyRecord ) [ '__pointerMissed$' ] ;
335
356
const internal = store . snapshot . internal ;
336
357
337
- // prepareRay( event)
358
+ // Cache the event
338
359
internal . lastEvent . nativeElement = event ;
339
360
340
361
// Get fresh intersects
341
362
const hits = intersect ( event , filter ) ;
363
+ // Only calculate distance for click events to avoid unnecessary math
342
364
const delta = isClickEvent ? calculateDistance ( event ) : 0 ;
343
365
344
366
// Save initial coordinates on pointer-down
@@ -347,94 +369,93 @@ export function createEvents(store: SignalState<NgtState>) {
347
369
internal . initialHits = hits . map ( ( hit ) => hit . eventObject ) ;
348
370
}
349
371
350
- // If a click yields no results, pass it back to the user as a miss
351
- // Missed events have to come first in order to establish user-land side-effect clean up
352
- if ( isClickEvent && ! hits . length ) {
353
- if ( delta <= 2 ) {
354
- pointerMissed ( event , internal . interaction ) ;
355
- pointerMissed$ . next ( event ) ;
356
- }
372
+ // Handle click miss events - early return optimization for better performance
373
+ if ( isClickEvent && hits . length === 0 && delta <= 2 ) {
374
+ pointerMissed ( event , internal . interaction ) ;
375
+ pointerMissed$ . next ( event ) ;
376
+ return ; // Early return if nothing was hit
357
377
}
358
378
359
- // Take care of unhover
379
+ // Take care of unhover for pointer moves
360
380
if ( isPointerMove ) cancelPointer ( hits ) ;
361
381
382
+ // Define onIntersect handler - locally cache common properties for better performance
362
383
function onIntersect ( data : NgtThreeEvent < NgtDomEvent > ) {
363
384
const eventObject = data . eventObject ;
364
385
const instance = getInstanceState ( eventObject ) ;
365
- const handlers = instance ?. handlers ;
366
386
367
- // Check presence of handlers
387
+ // Early return if no instance or event count
368
388
if ( ! instance ?. eventCount ) return ;
369
389
370
- /*
371
- MAYBE TODO, DELETE IF NOT:
372
- Check if the object is captured, captured events should not have intersects running in parallel
373
- But wouldn't it be better to just replace capturedMap with a single entry?
374
- Also, are we OK with straight up making picking up multiple objects impossible?
375
-
376
- const pointerId = (data as ThreeEvent<PointerEvent>).pointerId
377
- if (pointerId !== undefined) {
378
- const capturedMeshSet = internal.capturedMap.get(pointerId)
379
- if (capturedMeshSet) {
380
- const captured = capturedMeshSet.get(eventObject)
381
- if (captured && captured.localState.stopped) return
382
- }
383
- }*/
390
+ const handlers = instance . handlers ;
391
+ if ( ! handlers ) return ;
384
392
385
393
if ( isPointerMove ) {
386
- // Move event ...
387
- if (
388
- handlers ?. pointerover ||
389
- handlers ?. pointerenter ||
390
- handlers ?. pointerout ||
391
- handlers ?. pointerleave
392
- ) {
393
- // When enter or out is present take care of hover-state
394
+ // Handle pointer move events
395
+ const hasPointerOverHandlers = ! ! (
396
+ handlers . pointerover ||
397
+ handlers . pointerenter ||
398
+ handlers . pointerout ||
399
+ handlers . pointerleave
400
+ ) ;
401
+
402
+ if ( hasPointerOverHandlers ) {
394
403
const id = makeId ( data ) ;
395
404
const hoveredItem = internal . hovered . get ( id ) ;
396
405
if ( ! hoveredItem ) {
397
406
// If the object wasn't previously hovered, book it and call its handler
398
407
internal . hovered . set ( id , data ) ;
399
- handlers . pointerover ?. ( data as NgtThreeEvent < PointerEvent > ) ;
400
- handlers . pointerenter ?. ( data as NgtThreeEvent < PointerEvent > ) ;
408
+ if ( handlers . pointerover ) handlers . pointerover ( data as NgtThreeEvent < PointerEvent > ) ;
409
+ if ( handlers . pointerenter ) handlers . pointerenter ( data as NgtThreeEvent < PointerEvent > ) ;
401
410
} else if ( hoveredItem . stopped ) {
402
411
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed
403
412
data . stopPropagation ( ) ;
404
413
}
405
414
}
415
+
406
416
// Call mouse move
407
- handlers ? .pointermove ?. ( data as NgtThreeEvent < PointerEvent > ) ;
417
+ if ( handlers . pointermove ) handlers . pointermove ( data as NgtThreeEvent < PointerEvent > ) ;
408
418
} else {
409
419
// All other events ...
410
- const handler = handlers ?. [ name as keyof NgtEventHandlers ] as (
420
+ const handler = handlers [ name as keyof NgtEventHandlers ] as (
411
421
event : NgtThreeEvent < PointerEvent > ,
412
422
) => void ;
423
+
413
424
if ( handler ) {
414
425
// Forward all events back to their respective handlers with the exception of click events,
415
426
// which must use the initial target
416
427
if ( ! isClickEvent || internal . initialHits . includes ( eventObject ) ) {
417
- // Missed events have to come first
418
- pointerMissed (
419
- event ,
420
- internal . interaction . filter ( ( object ) => ! internal . initialHits . includes ( object ) ) ,
428
+ // Get objects not in initialHits for pointer missed - avoid creating new arrays if possible
429
+ const missedObjects = internal . interaction . filter (
430
+ ( object ) => ! internal . initialHits . includes ( object ) ,
421
431
) ;
432
+
433
+ // Call pointerMissed only if we have objects to notify
434
+ if ( missedObjects . length > 0 ) {
435
+ pointerMissed ( event , missedObjects ) ;
436
+ }
437
+
422
438
// Now call the handler
423
439
handler ( data as NgtThreeEvent < PointerEvent > ) ;
424
440
}
425
- } else {
441
+ } else if ( isClickEvent && internal . initialHits . includes ( eventObject ) ) {
426
442
// Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit
427
- if ( isClickEvent && internal . initialHits . includes ( eventObject ) ) {
428
- pointerMissed (
429
- event ,
430
- internal . interaction . filter ( ( object ) => ! internal . initialHits . includes ( object ) ) ,
431
- ) ;
443
+ const missedObjects = internal . interaction . filter (
444
+ ( object ) => ! internal . initialHits . includes ( object ) ,
445
+ ) ;
446
+
447
+ // Call pointerMissed only if we have objects to notify
448
+ if ( missedObjects . length > 0 ) {
449
+ pointerMissed ( event , missedObjects ) ;
432
450
}
433
451
}
434
452
}
435
453
}
436
454
437
- handleIntersects ( hits , event , delta , onIntersect ) ;
455
+ // Process all intersections
456
+ if ( hits . length > 0 ) {
457
+ handleIntersects ( hits , event , delta , onIntersect ) ;
458
+ }
438
459
} ;
439
460
}
440
461
0 commit comments