@@ -75,8 +75,13 @@ export function createEvents(store: SignalState<NgtState>) {
7575 const intersections : NgtIntersection [ ] = [ ] ;
7676 // Allow callers to eliminate event objects
7777 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 ++ ) {
8085 const objectRootState = getInstanceState ( eventsObjects [ i ] ) ?. store ?. snapshot ;
8186 if ( objectRootState ) {
8287 objectRootState . raycaster . camera = undefined ! ;
@@ -88,6 +93,9 @@ export function createEvents(store: SignalState<NgtState>) {
8893 state . events . compute ?.( event , store , null ) ;
8994 }
9095
96+ // Pre-allocate array to avoid garbage collection
97+ const raycastResults : THREE . Intersection < THREE . Object3D > [ ] = [ ] ;
98+
9199 function handleRaycast ( obj : THREE . Object3D ) {
92100 const objStore = getInstanceState ( obj ) ?. store ;
93101 const objState = objStore ?. snapshot ;
@@ -106,30 +114,40 @@ export function createEvents(store: SignalState<NgtState>) {
106114 }
107115
108116 // 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+ }
126142
127143 // https://github.com/mrdoob/three.js/issues/16031
128144 // Allow custom userland intersect sort order, this likely only makes sense on the root filter
129145 if ( state . events . filter ) hits = state . events . filter ( hits , store ) ;
130146
131147 // 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 ] ;
133151 let eventObject : THREE . Object3D | null = hit . object ;
134152 // bubble event up
135153 while ( eventObject ) {
@@ -140,8 +158,10 @@ export function createEvents(store: SignalState<NgtState>) {
140158
141159 // If the interaction is captured, make all capturing targets part of the intersect.
142160 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 ) ;
145165 }
146166 }
147167 return intersections ;
@@ -300,30 +320,31 @@ export function createEvents(store: SignalState<NgtState>) {
300320 }
301321
302322 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+ } ;
325345 }
326346
347+ // Cache these values since they're used in the closure
327348 const isPointerMove = name === 'pointermove' ;
328349 const isClickEvent = name === 'click' || name === 'contextmenu' || name === 'dblclick' ;
329350 const filter = isPointerMove ? filterPointerEvents : undefined ;
@@ -334,11 +355,12 @@ export function createEvents(store: SignalState<NgtState>) {
334355 const pointerMissed$ : Subject < MouseEvent > = ( store as NgtAnyRecord ) [ '__pointerMissed$' ] ;
335356 const internal = store . snapshot . internal ;
336357
337- // prepareRay( event)
358+ // Cache the event
338359 internal . lastEvent . nativeElement = event ;
339360
340361 // Get fresh intersects
341362 const hits = intersect ( event , filter ) ;
363+ // Only calculate distance for click events to avoid unnecessary math
342364 const delta = isClickEvent ? calculateDistance ( event ) : 0 ;
343365
344366 // Save initial coordinates on pointer-down
@@ -347,94 +369,93 @@ export function createEvents(store: SignalState<NgtState>) {
347369 internal . initialHits = hits . map ( ( hit ) => hit . eventObject ) ;
348370 }
349371
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
357377 }
358378
359- // Take care of unhover
379+ // Take care of unhover for pointer moves
360380 if ( isPointerMove ) cancelPointer ( hits ) ;
361381
382+ // Define onIntersect handler - locally cache common properties for better performance
362383 function onIntersect ( data : NgtThreeEvent < NgtDomEvent > ) {
363384 const eventObject = data . eventObject ;
364385 const instance = getInstanceState ( eventObject ) ;
365- const handlers = instance ?. handlers ;
366386
367- // Check presence of handlers
387+ // Early return if no instance or event count
368388 if ( ! instance ?. eventCount ) return ;
369389
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 ;
384392
385393 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 ) {
394403 const id = makeId ( data ) ;
395404 const hoveredItem = internal . hovered . get ( id ) ;
396405 if ( ! hoveredItem ) {
397406 // If the object wasn't previously hovered, book it and call its handler
398407 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 > ) ;
401410 } else if ( hoveredItem . stopped ) {
402411 // If the object was previously hovered and stopped, we shouldn't allow other items to proceed
403412 data . stopPropagation ( ) ;
404413 }
405414 }
415+
406416 // Call mouse move
407- handlers ? .pointermove ?. ( data as NgtThreeEvent < PointerEvent > ) ;
417+ if ( handlers . pointermove ) handlers . pointermove ( data as NgtThreeEvent < PointerEvent > ) ;
408418 } else {
409419 // All other events ...
410- const handler = handlers ?. [ name as keyof NgtEventHandlers ] as (
420+ const handler = handlers [ name as keyof NgtEventHandlers ] as (
411421 event : NgtThreeEvent < PointerEvent > ,
412422 ) => void ;
423+
413424 if ( handler ) {
414425 // Forward all events back to their respective handlers with the exception of click events,
415426 // which must use the initial target
416427 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 ) ,
421431 ) ;
432+
433+ // Call pointerMissed only if we have objects to notify
434+ if ( missedObjects . length > 0 ) {
435+ pointerMissed ( event , missedObjects ) ;
436+ }
437+
422438 // Now call the handler
423439 handler ( data as NgtThreeEvent < PointerEvent > ) ;
424440 }
425- } else {
441+ } else if ( isClickEvent && internal . initialHits . includes ( eventObject ) ) {
426442 // 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 ) ;
432450 }
433451 }
434452 }
435453 }
436454
437- handleIntersects ( hits , event , delta , onIntersect ) ;
455+ // Process all intersections
456+ if ( hits . length > 0 ) {
457+ handleIntersects ( hits , event , delta , onIntersect ) ;
458+ }
438459 } ;
439460 }
440461
0 commit comments