Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a0330e2

Browse files
committed
refactor(core): perf improvement to events
1 parent e11037f commit a0330e2

File tree

1 file changed

+113
-92
lines changed

1 file changed

+113
-92
lines changed

libs/core/src/lib/events.ts

Lines changed: 113 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)