@react-three/fiber
Advanced tools
Comparing version 6.0.8 to 6.0.9
@@ -53,4 +53,5 @@ import * as THREE from 'three'; | ||
} | ||
export declare function removeInteractivity(store: UseStore<RootState>, object: THREE.Object3D): void; | ||
export declare function createEvents(store: UseStore<RootState>): { | ||
handlePointer: (name: string) => (event: DomEvent) => void; | ||
}; |
@@ -6,3 +6,3 @@ import * as THREE from 'three'; | ||
import { Instance, InstanceProps } from './renderer'; | ||
import { EventManager } from './events'; | ||
import { DomEvent, EventManager } from './events'; | ||
export interface Intersection extends THREE.Intersection { | ||
@@ -49,2 +49,3 @@ eventObject: THREE.Object3D; | ||
interaction: THREE.Object3D[]; | ||
hovered: Map<string, DomEvent>; | ||
subscribers: Subscription[]; | ||
@@ -51,0 +52,0 @@ captured: Intersection[] | undefined; |
@@ -74,2 +74,329 @@ 'use strict'; | ||
function makeId(event) { | ||
return (event.eventObject || event.object).uuid + '/' + event.index; | ||
} | ||
function removeInteractivity(store, object) { | ||
const { | ||
internal | ||
} = store.getState(); // Removes every trace of an object from the data store | ||
internal.interaction = internal.interaction.filter(o => o !== object); | ||
internal.initialHits = internal.initialHits.filter(o => o !== object); | ||
internal.hovered.forEach((value, key) => { | ||
if (value.eventObject === object || value.object === object) { | ||
internal.hovered.delete(key); | ||
} | ||
}); | ||
} | ||
function createEvents(store) { | ||
const temp = new THREE__namespace.Vector3(); | ||
/** Sets up defaultRaycaster */ | ||
function prepareRay(event) { | ||
var _raycaster$computeOff; | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
size | ||
} = state; // https://github.com/pmndrs/react-three-fiber/pull/782 | ||
// Events trigger outside of canvas when moved | ||
const { | ||
offsetX, | ||
offsetY | ||
} = (_raycaster$computeOff = raycaster.computeOffsets == null ? void 0 : raycaster.computeOffsets(event, state)) != null ? _raycaster$computeOff : event; | ||
const { | ||
width, | ||
height | ||
} = size; | ||
mouse.set(offsetX / width * 2 - 1, -(offsetY / height) * 2 + 1); | ||
raycaster.setFromCamera(mouse, camera); | ||
} | ||
/** Calculates delta */ | ||
function calculateDistance(event) { | ||
const { | ||
internal | ||
} = store.getState(); | ||
const dx = event.offsetX - internal.initialClick[0]; | ||
const dy = event.offsetY - internal.initialClick[1]; | ||
return Math.round(Math.sqrt(dx * dx + dy * dy)); | ||
} | ||
/** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */ | ||
function filterPointerEvents(objects) { | ||
return objects.filter(obj => ['Move', 'Over', 'Enter', 'Out', 'Leave'].some(name => { | ||
var _r3f$handlers; | ||
return (_r3f$handlers = obj.__r3f.handlers) == null ? void 0 : _r3f$handlers['onPointer' + name]; | ||
})); | ||
} | ||
function intersect(filter) { | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
internal | ||
} = state; // Skip event handling when noEvents is set | ||
if (!raycaster.enabled) return []; | ||
const seen = new Set(); | ||
const intersections = []; // Allow callers to eliminate event objects | ||
const eventsObjects = filter ? filter(internal.interaction) : internal.interaction; // Intersect known handler objects and filter against duplicates | ||
let intersects = raycaster.intersectObjects(eventsObjects, true).filter(item => { | ||
const id = makeId(item); | ||
if (seen.has(id)) return false; | ||
seen.add(id); | ||
return true; | ||
}); // https://github.com/mrdoob/three.js/issues/16031 | ||
// Allow custom userland intersect sort order | ||
if (raycaster.filter) intersects = raycaster.filter(intersects, state); | ||
for (const intersect of intersects) { | ||
let eventObject = intersect.object; // Bubble event up | ||
while (eventObject) { | ||
var _r3f; | ||
const handlers = (_r3f = eventObject.__r3f) == null ? void 0 : _r3f.handlers; | ||
if (handlers) intersections.push({ ...intersect, | ||
eventObject | ||
}); | ||
eventObject = eventObject.parent; | ||
} | ||
} | ||
return intersections; | ||
} | ||
/** Creates filtered intersects and returns an array of positive hits */ | ||
function patchIntersects(intersections, event) { | ||
const { | ||
internal | ||
} = store.getState(); // If the interaction is captured take that into account, the captured event has to be part of the intersects | ||
if (internal.captured && event.type !== 'click' && event.type !== 'wheel') { | ||
internal.captured.forEach(captured => { | ||
if (!intersections.find(hit => hit.eventObject === captured.eventObject)) intersections.push(captured); | ||
}); | ||
} | ||
return intersections; | ||
} | ||
/** Handles intersections by forwarding them to handlers */ | ||
function handleIntersects(intersections, event, callback) { | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
internal | ||
} = store.getState(); // If anything has been found, forward it to the event listeners | ||
if (intersections.length) { | ||
const unprojectedPoint = temp.set(mouse.x, mouse.y, 0).unproject(camera); | ||
const delta = event.type === 'click' ? calculateDistance(event) : 0; | ||
const releasePointerCapture = id => event.target.releasePointerCapture(id); | ||
const localState = { | ||
stopped: false, | ||
captured: false | ||
}; | ||
for (const hit of intersections) { | ||
const setPointerCapture = id => { | ||
// If the hit is going to be captured flag that we're in captured state | ||
if (!localState.captured) { | ||
localState.captured = true; // The captured hit array is reset to collect hits | ||
internal.captured = []; | ||
} // Push hits to the array | ||
if (internal.captured) internal.captured.push(hit) // Call the original event now | ||
; | ||
event.target.setPointerCapture(id); | ||
}; // Add native event props | ||
let extractEventProps = {}; | ||
for (let prop in Object.getPrototypeOf(event)) { | ||
extractEventProps[prop] = event[prop]; | ||
} | ||
let raycastEvent = { ...hit, | ||
...extractEventProps, | ||
intersections, | ||
stopped: localState.stopped, | ||
delta, | ||
unprojectedPoint, | ||
ray: raycaster.ray, | ||
camera: camera, | ||
// Hijack stopPropagation, which just sets a flag | ||
stopPropagation: () => { | ||
// https://github.com/pmndrs/react-three-fiber/issues/596 | ||
// Events are not allowed to stop propagation if the pointer has been captured | ||
const cap = internal.captured; | ||
if (!cap || cap.find(h => h.eventObject.id === hit.eventObject.id)) { | ||
raycastEvent.stopped = localState.stopped = true; // Propagation is stopped, remove all other hover records | ||
// An event handler is only allowed to flush other handlers if it is hovered itself | ||
if (internal.hovered.size && Array.from(internal.hovered.values()).find(i => i.eventObject === hit.eventObject)) { | ||
// Objects cannot flush out higher up objects that have already caught the event | ||
const higher = intersections.slice(0, intersections.indexOf(hit)); | ||
cancelPointer([...higher, hit]); | ||
} | ||
} | ||
}, | ||
target: { ...event.target, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
currentTarget: { ...event.currentTarget, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
sourceEvent: event | ||
}; // Call subscribers | ||
callback(raycastEvent); // Event bubbling may be interrupted by stopPropagation | ||
if (localState.stopped === true) break; | ||
} | ||
} | ||
return intersections; | ||
} | ||
function cancelPointer(hits) { | ||
const { | ||
internal | ||
} = store.getState(); | ||
Array.from(internal.hovered.values()).forEach(hoveredObj => { | ||
// When no objects were hit or the the hovered object wasn't found underneath the cursor | ||
// we call onPointerOut and delete the object from the hovered-elements map | ||
if (!hits.length || !hits.find(hit => hit.object === hoveredObj.object && hit.index === hoveredObj.index)) { | ||
const eventObject = hoveredObj.eventObject; | ||
const handlers = eventObject.__r3f.handlers; | ||
internal.hovered.delete(makeId(hoveredObj)); | ||
if (handlers) { | ||
// Clear out intersects, they are outdated by now | ||
const data = { ...hoveredObj, | ||
intersections: hits || [] | ||
}; | ||
handlers.onPointerOut == null ? void 0 : handlers.onPointerOut(data); | ||
handlers.onPointerLeave == null ? void 0 : handlers.onPointerLeave(data); | ||
} | ||
} | ||
}); | ||
} | ||
const handlePointer = name => { | ||
// Deal with cancelation | ||
switch (name) { | ||
case 'onPointerLeave': | ||
case 'onPointerCancel': | ||
return () => cancelPointer([]); | ||
case 'onLostPointerCapture': | ||
return () => (store.getState().internal.captured = undefined, cancelPointer([])); | ||
} // Any other pointer goes here ... | ||
return event => { | ||
const { | ||
onPointerMissed, | ||
internal | ||
} = store.getState(); | ||
prepareRay(event); // Get fresh intersects | ||
const isPointerMove = name === 'onPointerMove'; | ||
const filter = isPointerMove ? filterPointerEvents : undefined; | ||
const hits = patchIntersects(intersect(filter), event); // Take care of unhover | ||
if (isPointerMove) cancelPointer(hits); | ||
handleIntersects(hits, event, data => { | ||
const eventObject = data.eventObject; | ||
const handlers = eventObject.__r3f.handlers; // Check presence of handlers | ||
if (!handlers) return; | ||
if (isPointerMove) { | ||
// Move event ... | ||
if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) { | ||
// When enter or out is present take care of hover-state | ||
const id = makeId(data); | ||
const hoveredItem = internal.hovered.get(id); | ||
if (!hoveredItem) { | ||
// If the object wasn't previously hovered, book it and call its handler | ||
internal.hovered.set(id, data); | ||
handlers.onPointerOver == null ? void 0 : handlers.onPointerOver(data); | ||
handlers.onPointerEnter == null ? void 0 : handlers.onPointerEnter(data); | ||
} else if (hoveredItem.stopped) { | ||
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed | ||
data.stopPropagation(); | ||
} | ||
} // Call mouse move | ||
handlers.onPointerMove == null ? void 0 : handlers.onPointerMove(data); | ||
} else { | ||
// All other events ... | ||
const handler = handlers == null ? void 0 : handlers[name]; | ||
if (handler) { | ||
// Forward all events back to their respective handlers with the exception of click events, | ||
// which must use the initial target | ||
if (name !== 'onClick' && name !== 'onContextMenu' && name !== 'onDoubleClick' || internal.initialHits.includes(eventObject)) { | ||
handler(data); | ||
pointerMissed(event, internal.interaction.filter(object => object !== eventObject)); | ||
} | ||
} | ||
} | ||
}); // Save initial coordinates on pointer-down | ||
if (name === 'onPointerDown') { | ||
internal.initialClick = [event.offsetX, event.offsetY]; | ||
internal.initialHits = hits.map(hit => hit.eventObject); | ||
} // If a click yields no results, pass it back to the user as a miss | ||
if ((name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick') && !hits.length) { | ||
if (calculateDistance(event) <= 2) { | ||
pointerMissed(event, internal.interaction); | ||
if (onPointerMissed) onPointerMissed(); | ||
} | ||
} | ||
}; | ||
}; | ||
function pointerMissed(event, objects) { | ||
objects.forEach(object => { | ||
var _r3f$handlers2; | ||
return (_r3f$handlers2 = object.__r3f.handlers) == null ? void 0 : _r3f$handlers2.onPointerMissed == null ? void 0 : _r3f$handlers2.onPointerMissed(event); | ||
}); | ||
} | ||
return { | ||
handlePointer | ||
}; | ||
} | ||
// Type guard to tell a store from a portal | ||
@@ -418,14 +745,16 @@ const isStore = def => def && !!def.getState; | ||
function removeRecursive(array, parent, clone = false) { | ||
if (array) { | ||
// Three uses splice op's internally we may have to shallow-clone the array in order to safely remove items | ||
const target = clone ? [...array] : array; | ||
target.forEach(child => removeChild(parent, child)); | ||
} | ||
function removeRecursive(array, parent, dispose = false) { | ||
if (array) [...array].forEach(child => removeChild(parent, child, dispose)); | ||
} | ||
function removeChild(parentInstance, child) { | ||
function removeChild(parentInstance, child, dispose) { | ||
if (child) { | ||
if (child.isObject3D) { | ||
parentInstance.remove(child); | ||
var _child$__r3f; | ||
parentInstance.remove(child); // Remove interactivity | ||
if ((_child$__r3f = child.__r3f) != null && _child$__r3f.root) { | ||
removeInteractivity(child.__r3f.root, child); | ||
} | ||
} else { | ||
@@ -442,23 +771,20 @@ child.parent = null; | ||
} | ||
} // Remove interactivity | ||
} // Allow objects to bail out of recursive dispose alltogether by passing dispose={null} | ||
// Never dispose of primitives because their state may be kept outside of React! | ||
// In order for an object to be able to dispose it has to have | ||
// - a dispose method, | ||
// - it cannot be an <instance object={...} /> | ||
// - it cannot be a THREE.Scene, because three has broken it's own api | ||
// | ||
// Since disposal is recursive, we can check the optional dispose arg, which will be undefined | ||
// when the reconciler calls it, but then carry our own check recursively | ||
if (child.__r3f.root) { | ||
const rootState = child.__r3f.root.getState(); | ||
const shouldDispose = dispose === undefined ? child.dispose !== null && !child.__r3f.instance : dispose; // Remove nested child objects | ||
rootState.internal.interaction = rootState.internal.interaction.filter(x => x !== child); | ||
} | ||
removeRecursive(child.__r3f.objects, child, shouldDispose); | ||
removeRecursive(child.children, child, shouldDispose); // Dispose item whenever the reconciler feels like it | ||
invalidateInstance(parentInstance); // Allow objects to bail out of recursive dispose alltogether by passing dispose={null} | ||
// Never dispose of primitives because their state may be kept outside of React! | ||
if (child.dispose && !child.__r3f.instance) { | ||
const objects = child.__r3f.objects; | ||
scheduler.unstable_runWithPriority(scheduler.unstable_IdlePriority, () => { | ||
// Remove nested child objects | ||
removeRecursive(objects, child); | ||
removeRecursive(child.children, child, true); // Dispose item | ||
if (child.dispose && child.type !== 'Scene') child.dispose(); | ||
}); | ||
if (shouldDispose && child.dispose && child.type !== 'Scene') { | ||
scheduler.unstable_runWithPriority(scheduler.unstable_IdlePriority, () => child.dispose()); | ||
} // Remove references | ||
@@ -472,2 +798,3 @@ | ||
delete child.__r3f; | ||
invalidateInstance(parentInstance); | ||
} | ||
@@ -814,2 +1141,3 @@ } | ||
interaction: [], | ||
hovered: new Map(), | ||
subscribers: [], | ||
@@ -972,314 +1300,2 @@ captured: undefined, | ||
function createEvents(store) { | ||
const hovered = new Map(); | ||
const temp = new THREE__namespace.Vector3(); | ||
/** Sets up defaultRaycaster */ | ||
function prepareRay(event) { | ||
var _raycaster$computeOff; | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
size | ||
} = state; // https://github.com/pmndrs/react-three-fiber/pull/782 | ||
// Events trigger outside of canvas when moved | ||
const { | ||
offsetX, | ||
offsetY | ||
} = (_raycaster$computeOff = raycaster.computeOffsets == null ? void 0 : raycaster.computeOffsets(event, state)) != null ? _raycaster$computeOff : event; | ||
const { | ||
width, | ||
height | ||
} = size; | ||
mouse.set(offsetX / width * 2 - 1, -(offsetY / height) * 2 + 1); | ||
raycaster.setFromCamera(mouse, camera); | ||
} | ||
/** Calculates delta */ | ||
function calculateDistance(event) { | ||
const { | ||
internal | ||
} = store.getState(); | ||
const dx = event.offsetX - internal.initialClick[0]; | ||
const dy = event.offsetY - internal.initialClick[1]; | ||
return Math.round(Math.sqrt(dx * dx + dy * dy)); | ||
} | ||
/** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */ | ||
function filterPointerEvents(objects) { | ||
return objects.filter(obj => ['Move', 'Over', 'Enter', 'Out', 'Leave'].some(name => { | ||
var _r3f$handlers; | ||
return (_r3f$handlers = obj.__r3f.handlers) == null ? void 0 : _r3f$handlers['onPointer' + name]; | ||
})); | ||
} | ||
function makeId(event) { | ||
return (event.eventObject || event.object).uuid + '/' + event.index; | ||
} | ||
function intersect(filter) { | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
internal | ||
} = state; // Skip event handling when noEvents is set | ||
if (!raycaster.enabled) return []; | ||
const seen = new Set(); | ||
const intersections = []; // Allow callers to eliminate event objects | ||
const eventsObjects = filter ? filter(internal.interaction) : internal.interaction; // Intersect known handler objects and filter against duplicates | ||
let intersects = raycaster.intersectObjects(eventsObjects, true).filter(item => { | ||
const id = makeId(item); | ||
if (seen.has(id)) return false; | ||
seen.add(id); | ||
return true; | ||
}); // https://github.com/mrdoob/three.js/issues/16031 | ||
// Allow custom userland intersect sort order | ||
if (raycaster.filter) intersects = raycaster.filter(intersects, state); | ||
for (const intersect of intersects) { | ||
let eventObject = intersect.object; // Bubble event up | ||
while (eventObject) { | ||
var _r3f; | ||
const handlers = (_r3f = eventObject.__r3f) == null ? void 0 : _r3f.handlers; | ||
if (handlers) intersections.push({ ...intersect, | ||
eventObject | ||
}); | ||
eventObject = eventObject.parent; | ||
} | ||
} | ||
return intersections; | ||
} | ||
/** Creates filtered intersects and returns an array of positive hits */ | ||
function patchIntersects(intersections, event) { | ||
const { | ||
internal | ||
} = store.getState(); // If the interaction is captured take that into account, the captured event has to be part of the intersects | ||
if (internal.captured && event.type !== 'click' && event.type !== 'wheel') { | ||
internal.captured.forEach(captured => { | ||
if (!intersections.find(hit => hit.eventObject === captured.eventObject)) intersections.push(captured); | ||
}); | ||
} | ||
return intersections; | ||
} | ||
/** Handles intersections by forwarding them to handlers */ | ||
function handleIntersects(intersections, event, callback) { | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
internal | ||
} = store.getState(); // If anything has been found, forward it to the event listeners | ||
if (intersections.length) { | ||
const unprojectedPoint = temp.set(mouse.x, mouse.y, 0).unproject(camera); | ||
const delta = event.type === 'click' ? calculateDistance(event) : 0; | ||
const releasePointerCapture = id => event.target.releasePointerCapture(id); | ||
const localState = { | ||
stopped: false, | ||
captured: false | ||
}; | ||
for (const hit of intersections) { | ||
const setPointerCapture = id => { | ||
// If the hit is going to be captured flag that we're in captured state | ||
if (!localState.captured) { | ||
localState.captured = true; // The captured hit array is reset to collect hits | ||
internal.captured = []; | ||
} // Push hits to the array | ||
if (internal.captured) internal.captured.push(hit) // Call the original event now | ||
; | ||
event.target.setPointerCapture(id); | ||
}; // Add native event props | ||
let extractEventProps = {}; | ||
for (let prop in Object.getPrototypeOf(event)) { | ||
extractEventProps[prop] = event[prop]; | ||
} | ||
let raycastEvent = { ...hit, | ||
...extractEventProps, | ||
intersections, | ||
stopped: localState.stopped, | ||
delta, | ||
unprojectedPoint, | ||
ray: raycaster.ray, | ||
camera: camera, | ||
// Hijack stopPropagation, which just sets a flag | ||
stopPropagation: () => { | ||
// https://github.com/pmndrs/react-three-fiber/issues/596 | ||
// Events are not allowed to stop propagation if the pointer has been captured | ||
const cap = internal.captured; | ||
if (!cap || cap.find(h => h.eventObject.id === hit.eventObject.id)) { | ||
raycastEvent.stopped = localState.stopped = true; // Propagation is stopped, remove all other hover records | ||
// An event handler is only allowed to flush other handlers if it is hovered itself | ||
if (hovered.size && Array.from(hovered.values()).find(i => i.eventObject === hit.eventObject)) { | ||
// Objects cannot flush out higher up objects that have already caught the event | ||
const higher = intersections.slice(0, intersections.indexOf(hit)); | ||
cancelPointer([...higher, hit]); | ||
} | ||
} | ||
}, | ||
target: { ...event.target, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
currentTarget: { ...event.currentTarget, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
sourceEvent: event | ||
}; // Call subscribers | ||
callback(raycastEvent); // Event bubbling may be interrupted by stopPropagation | ||
if (localState.stopped === true) break; | ||
} | ||
} | ||
return intersections; | ||
} | ||
function cancelPointer(hits) { | ||
Array.from(hovered.values()).forEach(hoveredObj => { | ||
// When no objects were hit or the the hovered object wasn't found underneath the cursor | ||
// we call onPointerOut and delete the object from the hovered-elements map | ||
if (!hits.length || !hits.find(hit => hit.object === hoveredObj.object && hit.index === hoveredObj.index)) { | ||
const eventObject = hoveredObj.eventObject; | ||
const handlers = eventObject.__r3f.handlers; | ||
hovered.delete(makeId(hoveredObj)); | ||
if (handlers) { | ||
// Clear out intersects, they are outdated by now | ||
const data = { ...hoveredObj, | ||
intersections: hits || [] | ||
}; | ||
handlers.onPointerOut == null ? void 0 : handlers.onPointerOut(data); | ||
handlers.onPointerLeave == null ? void 0 : handlers.onPointerLeave(data); | ||
} | ||
} | ||
}); | ||
} | ||
const handlePointer = name => { | ||
// Deal with cancelation | ||
switch (name) { | ||
case 'onPointerLeave': | ||
case 'onPointerCancel': | ||
return () => cancelPointer([]); | ||
case 'onLostPointerCapture': | ||
return () => (store.getState().internal.captured = undefined, cancelPointer([])); | ||
} // Any other pointer goes here ... | ||
return event => { | ||
const { | ||
onPointerMissed, | ||
internal | ||
} = store.getState(); | ||
prepareRay(event); // Get fresh intersects | ||
const isPointerMove = name === 'onPointerMove'; | ||
const filter = isPointerMove ? filterPointerEvents : undefined; | ||
const hits = patchIntersects(intersect(filter), event); // Take care of unhover | ||
if (isPointerMove) cancelPointer(hits); | ||
handleIntersects(hits, event, data => { | ||
const eventObject = data.eventObject; | ||
const handlers = eventObject.__r3f.handlers; // Check presence of handlers | ||
if (!handlers) return; | ||
if (isPointerMove) { | ||
// Move event ... | ||
if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) { | ||
// When enter or out is present take care of hover-state | ||
const id = makeId(data); | ||
const hoveredItem = hovered.get(id); | ||
if (!hoveredItem) { | ||
// If the object wasn't previously hovered, book it and call its handler | ||
hovered.set(id, data); | ||
handlers.onPointerOver == null ? void 0 : handlers.onPointerOver(data); | ||
handlers.onPointerEnter == null ? void 0 : handlers.onPointerEnter(data); | ||
} else if (hoveredItem.stopped) { | ||
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed | ||
data.stopPropagation(); | ||
} | ||
} // Call mouse move | ||
handlers.onPointerMove == null ? void 0 : handlers.onPointerMove(data); | ||
} else { | ||
// All other events ... | ||
const handler = handlers == null ? void 0 : handlers[name]; | ||
if (handler) { | ||
// Forward all events back to their respective handlers with the exception of click events, | ||
// which must use the initial target | ||
if (name !== 'onClick' && name !== 'onContextMenu' && name !== 'onDoubleClick' || internal.initialHits.includes(eventObject)) { | ||
handler(data); | ||
pointerMissed(event, internal.interaction.filter(object => object !== eventObject)); | ||
} | ||
} | ||
} | ||
}); // Save initial coordinates on pointer-down | ||
if (name === 'onPointerDown') { | ||
internal.initialClick = [event.offsetX, event.offsetY]; | ||
internal.initialHits = hits.map(hit => hit.eventObject); | ||
} // If a click yields no results, pass it back to the user as a miss | ||
if ((name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick') && !hits.length) { | ||
if (calculateDistance(event) <= 2) { | ||
pointerMissed(event, internal.interaction); | ||
if (onPointerMissed) onPointerMissed(); | ||
} | ||
} | ||
}; | ||
}; | ||
function pointerMissed(event, objects) { | ||
objects.forEach(object => { | ||
var _r3f$handlers2; | ||
return (_r3f$handlers2 = object.__r3f.handlers) == null ? void 0 : _r3f$handlers2.onPointerMissed == null ? void 0 : _r3f$handlers2.onPointerMissed(event); | ||
}); | ||
} | ||
return { | ||
handlePointer | ||
}; | ||
} | ||
function createPointerEvents(store) { | ||
@@ -1286,0 +1302,0 @@ const { |
@@ -74,2 +74,329 @@ 'use strict'; | ||
function makeId(event) { | ||
return (event.eventObject || event.object).uuid + '/' + event.index; | ||
} | ||
function removeInteractivity(store, object) { | ||
const { | ||
internal | ||
} = store.getState(); // Removes every trace of an object from the data store | ||
internal.interaction = internal.interaction.filter(o => o !== object); | ||
internal.initialHits = internal.initialHits.filter(o => o !== object); | ||
internal.hovered.forEach((value, key) => { | ||
if (value.eventObject === object || value.object === object) { | ||
internal.hovered.delete(key); | ||
} | ||
}); | ||
} | ||
function createEvents(store) { | ||
const temp = new THREE__namespace.Vector3(); | ||
/** Sets up defaultRaycaster */ | ||
function prepareRay(event) { | ||
var _raycaster$computeOff; | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
size | ||
} = state; // https://github.com/pmndrs/react-three-fiber/pull/782 | ||
// Events trigger outside of canvas when moved | ||
const { | ||
offsetX, | ||
offsetY | ||
} = (_raycaster$computeOff = raycaster.computeOffsets == null ? void 0 : raycaster.computeOffsets(event, state)) != null ? _raycaster$computeOff : event; | ||
const { | ||
width, | ||
height | ||
} = size; | ||
mouse.set(offsetX / width * 2 - 1, -(offsetY / height) * 2 + 1); | ||
raycaster.setFromCamera(mouse, camera); | ||
} | ||
/** Calculates delta */ | ||
function calculateDistance(event) { | ||
const { | ||
internal | ||
} = store.getState(); | ||
const dx = event.offsetX - internal.initialClick[0]; | ||
const dy = event.offsetY - internal.initialClick[1]; | ||
return Math.round(Math.sqrt(dx * dx + dy * dy)); | ||
} | ||
/** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */ | ||
function filterPointerEvents(objects) { | ||
return objects.filter(obj => ['Move', 'Over', 'Enter', 'Out', 'Leave'].some(name => { | ||
var _r3f$handlers; | ||
return (_r3f$handlers = obj.__r3f.handlers) == null ? void 0 : _r3f$handlers['onPointer' + name]; | ||
})); | ||
} | ||
function intersect(filter) { | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
internal | ||
} = state; // Skip event handling when noEvents is set | ||
if (!raycaster.enabled) return []; | ||
const seen = new Set(); | ||
const intersections = []; // Allow callers to eliminate event objects | ||
const eventsObjects = filter ? filter(internal.interaction) : internal.interaction; // Intersect known handler objects and filter against duplicates | ||
let intersects = raycaster.intersectObjects(eventsObjects, true).filter(item => { | ||
const id = makeId(item); | ||
if (seen.has(id)) return false; | ||
seen.add(id); | ||
return true; | ||
}); // https://github.com/mrdoob/three.js/issues/16031 | ||
// Allow custom userland intersect sort order | ||
if (raycaster.filter) intersects = raycaster.filter(intersects, state); | ||
for (const intersect of intersects) { | ||
let eventObject = intersect.object; // Bubble event up | ||
while (eventObject) { | ||
var _r3f; | ||
const handlers = (_r3f = eventObject.__r3f) == null ? void 0 : _r3f.handlers; | ||
if (handlers) intersections.push({ ...intersect, | ||
eventObject | ||
}); | ||
eventObject = eventObject.parent; | ||
} | ||
} | ||
return intersections; | ||
} | ||
/** Creates filtered intersects and returns an array of positive hits */ | ||
function patchIntersects(intersections, event) { | ||
const { | ||
internal | ||
} = store.getState(); // If the interaction is captured take that into account, the captured event has to be part of the intersects | ||
if (internal.captured && event.type !== 'click' && event.type !== 'wheel') { | ||
internal.captured.forEach(captured => { | ||
if (!intersections.find(hit => hit.eventObject === captured.eventObject)) intersections.push(captured); | ||
}); | ||
} | ||
return intersections; | ||
} | ||
/** Handles intersections by forwarding them to handlers */ | ||
function handleIntersects(intersections, event, callback) { | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
internal | ||
} = store.getState(); // If anything has been found, forward it to the event listeners | ||
if (intersections.length) { | ||
const unprojectedPoint = temp.set(mouse.x, mouse.y, 0).unproject(camera); | ||
const delta = event.type === 'click' ? calculateDistance(event) : 0; | ||
const releasePointerCapture = id => event.target.releasePointerCapture(id); | ||
const localState = { | ||
stopped: false, | ||
captured: false | ||
}; | ||
for (const hit of intersections) { | ||
const setPointerCapture = id => { | ||
// If the hit is going to be captured flag that we're in captured state | ||
if (!localState.captured) { | ||
localState.captured = true; // The captured hit array is reset to collect hits | ||
internal.captured = []; | ||
} // Push hits to the array | ||
if (internal.captured) internal.captured.push(hit) // Call the original event now | ||
; | ||
event.target.setPointerCapture(id); | ||
}; // Add native event props | ||
let extractEventProps = {}; | ||
for (let prop in Object.getPrototypeOf(event)) { | ||
extractEventProps[prop] = event[prop]; | ||
} | ||
let raycastEvent = { ...hit, | ||
...extractEventProps, | ||
intersections, | ||
stopped: localState.stopped, | ||
delta, | ||
unprojectedPoint, | ||
ray: raycaster.ray, | ||
camera: camera, | ||
// Hijack stopPropagation, which just sets a flag | ||
stopPropagation: () => { | ||
// https://github.com/pmndrs/react-three-fiber/issues/596 | ||
// Events are not allowed to stop propagation if the pointer has been captured | ||
const cap = internal.captured; | ||
if (!cap || cap.find(h => h.eventObject.id === hit.eventObject.id)) { | ||
raycastEvent.stopped = localState.stopped = true; // Propagation is stopped, remove all other hover records | ||
// An event handler is only allowed to flush other handlers if it is hovered itself | ||
if (internal.hovered.size && Array.from(internal.hovered.values()).find(i => i.eventObject === hit.eventObject)) { | ||
// Objects cannot flush out higher up objects that have already caught the event | ||
const higher = intersections.slice(0, intersections.indexOf(hit)); | ||
cancelPointer([...higher, hit]); | ||
} | ||
} | ||
}, | ||
target: { ...event.target, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
currentTarget: { ...event.currentTarget, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
sourceEvent: event | ||
}; // Call subscribers | ||
callback(raycastEvent); // Event bubbling may be interrupted by stopPropagation | ||
if (localState.stopped === true) break; | ||
} | ||
} | ||
return intersections; | ||
} | ||
function cancelPointer(hits) { | ||
const { | ||
internal | ||
} = store.getState(); | ||
Array.from(internal.hovered.values()).forEach(hoveredObj => { | ||
// When no objects were hit or the the hovered object wasn't found underneath the cursor | ||
// we call onPointerOut and delete the object from the hovered-elements map | ||
if (!hits.length || !hits.find(hit => hit.object === hoveredObj.object && hit.index === hoveredObj.index)) { | ||
const eventObject = hoveredObj.eventObject; | ||
const handlers = eventObject.__r3f.handlers; | ||
internal.hovered.delete(makeId(hoveredObj)); | ||
if (handlers) { | ||
// Clear out intersects, they are outdated by now | ||
const data = { ...hoveredObj, | ||
intersections: hits || [] | ||
}; | ||
handlers.onPointerOut == null ? void 0 : handlers.onPointerOut(data); | ||
handlers.onPointerLeave == null ? void 0 : handlers.onPointerLeave(data); | ||
} | ||
} | ||
}); | ||
} | ||
const handlePointer = name => { | ||
// Deal with cancelation | ||
switch (name) { | ||
case 'onPointerLeave': | ||
case 'onPointerCancel': | ||
return () => cancelPointer([]); | ||
case 'onLostPointerCapture': | ||
return () => (store.getState().internal.captured = undefined, cancelPointer([])); | ||
} // Any other pointer goes here ... | ||
return event => { | ||
const { | ||
onPointerMissed, | ||
internal | ||
} = store.getState(); | ||
prepareRay(event); // Get fresh intersects | ||
const isPointerMove = name === 'onPointerMove'; | ||
const filter = isPointerMove ? filterPointerEvents : undefined; | ||
const hits = patchIntersects(intersect(filter), event); // Take care of unhover | ||
if (isPointerMove) cancelPointer(hits); | ||
handleIntersects(hits, event, data => { | ||
const eventObject = data.eventObject; | ||
const handlers = eventObject.__r3f.handlers; // Check presence of handlers | ||
if (!handlers) return; | ||
if (isPointerMove) { | ||
// Move event ... | ||
if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) { | ||
// When enter or out is present take care of hover-state | ||
const id = makeId(data); | ||
const hoveredItem = internal.hovered.get(id); | ||
if (!hoveredItem) { | ||
// If the object wasn't previously hovered, book it and call its handler | ||
internal.hovered.set(id, data); | ||
handlers.onPointerOver == null ? void 0 : handlers.onPointerOver(data); | ||
handlers.onPointerEnter == null ? void 0 : handlers.onPointerEnter(data); | ||
} else if (hoveredItem.stopped) { | ||
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed | ||
data.stopPropagation(); | ||
} | ||
} // Call mouse move | ||
handlers.onPointerMove == null ? void 0 : handlers.onPointerMove(data); | ||
} else { | ||
// All other events ... | ||
const handler = handlers == null ? void 0 : handlers[name]; | ||
if (handler) { | ||
// Forward all events back to their respective handlers with the exception of click events, | ||
// which must use the initial target | ||
if (name !== 'onClick' && name !== 'onContextMenu' && name !== 'onDoubleClick' || internal.initialHits.includes(eventObject)) { | ||
handler(data); | ||
pointerMissed(event, internal.interaction.filter(object => object !== eventObject)); | ||
} | ||
} | ||
} | ||
}); // Save initial coordinates on pointer-down | ||
if (name === 'onPointerDown') { | ||
internal.initialClick = [event.offsetX, event.offsetY]; | ||
internal.initialHits = hits.map(hit => hit.eventObject); | ||
} // If a click yields no results, pass it back to the user as a miss | ||
if ((name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick') && !hits.length) { | ||
if (calculateDistance(event) <= 2) { | ||
pointerMissed(event, internal.interaction); | ||
if (onPointerMissed) onPointerMissed(); | ||
} | ||
} | ||
}; | ||
}; | ||
function pointerMissed(event, objects) { | ||
objects.forEach(object => { | ||
var _r3f$handlers2; | ||
return (_r3f$handlers2 = object.__r3f.handlers) == null ? void 0 : _r3f$handlers2.onPointerMissed == null ? void 0 : _r3f$handlers2.onPointerMissed(event); | ||
}); | ||
} | ||
return { | ||
handlePointer | ||
}; | ||
} | ||
// Type guard to tell a store from a portal | ||
@@ -418,14 +745,16 @@ const isStore = def => def && !!def.getState; | ||
function removeRecursive(array, parent, clone = false) { | ||
if (array) { | ||
// Three uses splice op's internally we may have to shallow-clone the array in order to safely remove items | ||
const target = clone ? [...array] : array; | ||
target.forEach(child => removeChild(parent, child)); | ||
} | ||
function removeRecursive(array, parent, dispose = false) { | ||
if (array) [...array].forEach(child => removeChild(parent, child, dispose)); | ||
} | ||
function removeChild(parentInstance, child) { | ||
function removeChild(parentInstance, child, dispose) { | ||
if (child) { | ||
if (child.isObject3D) { | ||
parentInstance.remove(child); | ||
var _child$__r3f; | ||
parentInstance.remove(child); // Remove interactivity | ||
if ((_child$__r3f = child.__r3f) != null && _child$__r3f.root) { | ||
removeInteractivity(child.__r3f.root, child); | ||
} | ||
} else { | ||
@@ -442,23 +771,20 @@ child.parent = null; | ||
} | ||
} // Remove interactivity | ||
} // Allow objects to bail out of recursive dispose alltogether by passing dispose={null} | ||
// Never dispose of primitives because their state may be kept outside of React! | ||
// In order for an object to be able to dispose it has to have | ||
// - a dispose method, | ||
// - it cannot be an <instance object={...} /> | ||
// - it cannot be a THREE.Scene, because three has broken it's own api | ||
// | ||
// Since disposal is recursive, we can check the optional dispose arg, which will be undefined | ||
// when the reconciler calls it, but then carry our own check recursively | ||
if (child.__r3f.root) { | ||
const rootState = child.__r3f.root.getState(); | ||
const shouldDispose = dispose === undefined ? child.dispose !== null && !child.__r3f.instance : dispose; // Remove nested child objects | ||
rootState.internal.interaction = rootState.internal.interaction.filter(x => x !== child); | ||
} | ||
removeRecursive(child.__r3f.objects, child, shouldDispose); | ||
removeRecursive(child.children, child, shouldDispose); // Dispose item whenever the reconciler feels like it | ||
invalidateInstance(parentInstance); // Allow objects to bail out of recursive dispose alltogether by passing dispose={null} | ||
// Never dispose of primitives because their state may be kept outside of React! | ||
if (child.dispose && !child.__r3f.instance) { | ||
const objects = child.__r3f.objects; | ||
scheduler.unstable_runWithPriority(scheduler.unstable_IdlePriority, () => { | ||
// Remove nested child objects | ||
removeRecursive(objects, child); | ||
removeRecursive(child.children, child, true); // Dispose item | ||
if (child.dispose && child.type !== 'Scene') child.dispose(); | ||
}); | ||
if (shouldDispose && child.dispose && child.type !== 'Scene') { | ||
scheduler.unstable_runWithPriority(scheduler.unstable_IdlePriority, () => child.dispose()); | ||
} // Remove references | ||
@@ -472,2 +798,3 @@ | ||
delete child.__r3f; | ||
invalidateInstance(parentInstance); | ||
} | ||
@@ -814,2 +1141,3 @@ } | ||
interaction: [], | ||
hovered: new Map(), | ||
subscribers: [], | ||
@@ -972,314 +1300,2 @@ captured: undefined, | ||
function createEvents(store) { | ||
const hovered = new Map(); | ||
const temp = new THREE__namespace.Vector3(); | ||
/** Sets up defaultRaycaster */ | ||
function prepareRay(event) { | ||
var _raycaster$computeOff; | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
size | ||
} = state; // https://github.com/pmndrs/react-three-fiber/pull/782 | ||
// Events trigger outside of canvas when moved | ||
const { | ||
offsetX, | ||
offsetY | ||
} = (_raycaster$computeOff = raycaster.computeOffsets == null ? void 0 : raycaster.computeOffsets(event, state)) != null ? _raycaster$computeOff : event; | ||
const { | ||
width, | ||
height | ||
} = size; | ||
mouse.set(offsetX / width * 2 - 1, -(offsetY / height) * 2 + 1); | ||
raycaster.setFromCamera(mouse, camera); | ||
} | ||
/** Calculates delta */ | ||
function calculateDistance(event) { | ||
const { | ||
internal | ||
} = store.getState(); | ||
const dx = event.offsetX - internal.initialClick[0]; | ||
const dy = event.offsetY - internal.initialClick[1]; | ||
return Math.round(Math.sqrt(dx * dx + dy * dy)); | ||
} | ||
/** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */ | ||
function filterPointerEvents(objects) { | ||
return objects.filter(obj => ['Move', 'Over', 'Enter', 'Out', 'Leave'].some(name => { | ||
var _r3f$handlers; | ||
return (_r3f$handlers = obj.__r3f.handlers) == null ? void 0 : _r3f$handlers['onPointer' + name]; | ||
})); | ||
} | ||
function makeId(event) { | ||
return (event.eventObject || event.object).uuid + '/' + event.index; | ||
} | ||
function intersect(filter) { | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
internal | ||
} = state; // Skip event handling when noEvents is set | ||
if (!raycaster.enabled) return []; | ||
const seen = new Set(); | ||
const intersections = []; // Allow callers to eliminate event objects | ||
const eventsObjects = filter ? filter(internal.interaction) : internal.interaction; // Intersect known handler objects and filter against duplicates | ||
let intersects = raycaster.intersectObjects(eventsObjects, true).filter(item => { | ||
const id = makeId(item); | ||
if (seen.has(id)) return false; | ||
seen.add(id); | ||
return true; | ||
}); // https://github.com/mrdoob/three.js/issues/16031 | ||
// Allow custom userland intersect sort order | ||
if (raycaster.filter) intersects = raycaster.filter(intersects, state); | ||
for (const intersect of intersects) { | ||
let eventObject = intersect.object; // Bubble event up | ||
while (eventObject) { | ||
var _r3f; | ||
const handlers = (_r3f = eventObject.__r3f) == null ? void 0 : _r3f.handlers; | ||
if (handlers) intersections.push({ ...intersect, | ||
eventObject | ||
}); | ||
eventObject = eventObject.parent; | ||
} | ||
} | ||
return intersections; | ||
} | ||
/** Creates filtered intersects and returns an array of positive hits */ | ||
function patchIntersects(intersections, event) { | ||
const { | ||
internal | ||
} = store.getState(); // If the interaction is captured take that into account, the captured event has to be part of the intersects | ||
if (internal.captured && event.type !== 'click' && event.type !== 'wheel') { | ||
internal.captured.forEach(captured => { | ||
if (!intersections.find(hit => hit.eventObject === captured.eventObject)) intersections.push(captured); | ||
}); | ||
} | ||
return intersections; | ||
} | ||
/** Handles intersections by forwarding them to handlers */ | ||
function handleIntersects(intersections, event, callback) { | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
internal | ||
} = store.getState(); // If anything has been found, forward it to the event listeners | ||
if (intersections.length) { | ||
const unprojectedPoint = temp.set(mouse.x, mouse.y, 0).unproject(camera); | ||
const delta = event.type === 'click' ? calculateDistance(event) : 0; | ||
const releasePointerCapture = id => event.target.releasePointerCapture(id); | ||
const localState = { | ||
stopped: false, | ||
captured: false | ||
}; | ||
for (const hit of intersections) { | ||
const setPointerCapture = id => { | ||
// If the hit is going to be captured flag that we're in captured state | ||
if (!localState.captured) { | ||
localState.captured = true; // The captured hit array is reset to collect hits | ||
internal.captured = []; | ||
} // Push hits to the array | ||
if (internal.captured) internal.captured.push(hit) // Call the original event now | ||
; | ||
event.target.setPointerCapture(id); | ||
}; // Add native event props | ||
let extractEventProps = {}; | ||
for (let prop in Object.getPrototypeOf(event)) { | ||
extractEventProps[prop] = event[prop]; | ||
} | ||
let raycastEvent = { ...hit, | ||
...extractEventProps, | ||
intersections, | ||
stopped: localState.stopped, | ||
delta, | ||
unprojectedPoint, | ||
ray: raycaster.ray, | ||
camera: camera, | ||
// Hijack stopPropagation, which just sets a flag | ||
stopPropagation: () => { | ||
// https://github.com/pmndrs/react-three-fiber/issues/596 | ||
// Events are not allowed to stop propagation if the pointer has been captured | ||
const cap = internal.captured; | ||
if (!cap || cap.find(h => h.eventObject.id === hit.eventObject.id)) { | ||
raycastEvent.stopped = localState.stopped = true; // Propagation is stopped, remove all other hover records | ||
// An event handler is only allowed to flush other handlers if it is hovered itself | ||
if (hovered.size && Array.from(hovered.values()).find(i => i.eventObject === hit.eventObject)) { | ||
// Objects cannot flush out higher up objects that have already caught the event | ||
const higher = intersections.slice(0, intersections.indexOf(hit)); | ||
cancelPointer([...higher, hit]); | ||
} | ||
} | ||
}, | ||
target: { ...event.target, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
currentTarget: { ...event.currentTarget, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
sourceEvent: event | ||
}; // Call subscribers | ||
callback(raycastEvent); // Event bubbling may be interrupted by stopPropagation | ||
if (localState.stopped === true) break; | ||
} | ||
} | ||
return intersections; | ||
} | ||
function cancelPointer(hits) { | ||
Array.from(hovered.values()).forEach(hoveredObj => { | ||
// When no objects were hit or the the hovered object wasn't found underneath the cursor | ||
// we call onPointerOut and delete the object from the hovered-elements map | ||
if (!hits.length || !hits.find(hit => hit.object === hoveredObj.object && hit.index === hoveredObj.index)) { | ||
const eventObject = hoveredObj.eventObject; | ||
const handlers = eventObject.__r3f.handlers; | ||
hovered.delete(makeId(hoveredObj)); | ||
if (handlers) { | ||
// Clear out intersects, they are outdated by now | ||
const data = { ...hoveredObj, | ||
intersections: hits || [] | ||
}; | ||
handlers.onPointerOut == null ? void 0 : handlers.onPointerOut(data); | ||
handlers.onPointerLeave == null ? void 0 : handlers.onPointerLeave(data); | ||
} | ||
} | ||
}); | ||
} | ||
const handlePointer = name => { | ||
// Deal with cancelation | ||
switch (name) { | ||
case 'onPointerLeave': | ||
case 'onPointerCancel': | ||
return () => cancelPointer([]); | ||
case 'onLostPointerCapture': | ||
return () => (store.getState().internal.captured = undefined, cancelPointer([])); | ||
} // Any other pointer goes here ... | ||
return event => { | ||
const { | ||
onPointerMissed, | ||
internal | ||
} = store.getState(); | ||
prepareRay(event); // Get fresh intersects | ||
const isPointerMove = name === 'onPointerMove'; | ||
const filter = isPointerMove ? filterPointerEvents : undefined; | ||
const hits = patchIntersects(intersect(filter), event); // Take care of unhover | ||
if (isPointerMove) cancelPointer(hits); | ||
handleIntersects(hits, event, data => { | ||
const eventObject = data.eventObject; | ||
const handlers = eventObject.__r3f.handlers; // Check presence of handlers | ||
if (!handlers) return; | ||
if (isPointerMove) { | ||
// Move event ... | ||
if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) { | ||
// When enter or out is present take care of hover-state | ||
const id = makeId(data); | ||
const hoveredItem = hovered.get(id); | ||
if (!hoveredItem) { | ||
// If the object wasn't previously hovered, book it and call its handler | ||
hovered.set(id, data); | ||
handlers.onPointerOver == null ? void 0 : handlers.onPointerOver(data); | ||
handlers.onPointerEnter == null ? void 0 : handlers.onPointerEnter(data); | ||
} else if (hoveredItem.stopped) { | ||
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed | ||
data.stopPropagation(); | ||
} | ||
} // Call mouse move | ||
handlers.onPointerMove == null ? void 0 : handlers.onPointerMove(data); | ||
} else { | ||
// All other events ... | ||
const handler = handlers == null ? void 0 : handlers[name]; | ||
if (handler) { | ||
// Forward all events back to their respective handlers with the exception of click events, | ||
// which must use the initial target | ||
if (name !== 'onClick' && name !== 'onContextMenu' && name !== 'onDoubleClick' || internal.initialHits.includes(eventObject)) { | ||
handler(data); | ||
pointerMissed(event, internal.interaction.filter(object => object !== eventObject)); | ||
} | ||
} | ||
} | ||
}); // Save initial coordinates on pointer-down | ||
if (name === 'onPointerDown') { | ||
internal.initialClick = [event.offsetX, event.offsetY]; | ||
internal.initialHits = hits.map(hit => hit.eventObject); | ||
} // If a click yields no results, pass it back to the user as a miss | ||
if ((name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick') && !hits.length) { | ||
if (calculateDistance(event) <= 2) { | ||
pointerMissed(event, internal.interaction); | ||
if (onPointerMissed) onPointerMissed(); | ||
} | ||
} | ||
}; | ||
}; | ||
function pointerMissed(event, objects) { | ||
objects.forEach(object => { | ||
var _r3f$handlers2; | ||
return (_r3f$handlers2 = object.__r3f.handlers) == null ? void 0 : _r3f$handlers2.onPointerMissed == null ? void 0 : _r3f$handlers2.onPointerMissed(event); | ||
}); | ||
} | ||
return { | ||
handlePointer | ||
}; | ||
} | ||
function createPointerEvents(store) { | ||
@@ -1286,0 +1302,0 @@ const { |
@@ -41,2 +41,329 @@ import * as THREE from 'three'; | ||
function makeId(event) { | ||
return (event.eventObject || event.object).uuid + '/' + event.index; | ||
} | ||
function removeInteractivity(store, object) { | ||
const { | ||
internal | ||
} = store.getState(); // Removes every trace of an object from the data store | ||
internal.interaction = internal.interaction.filter(o => o !== object); | ||
internal.initialHits = internal.initialHits.filter(o => o !== object); | ||
internal.hovered.forEach((value, key) => { | ||
if (value.eventObject === object || value.object === object) { | ||
internal.hovered.delete(key); | ||
} | ||
}); | ||
} | ||
function createEvents(store) { | ||
const temp = new THREE.Vector3(); | ||
/** Sets up defaultRaycaster */ | ||
function prepareRay(event) { | ||
var _raycaster$computeOff; | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
size | ||
} = state; // https://github.com/pmndrs/react-three-fiber/pull/782 | ||
// Events trigger outside of canvas when moved | ||
const { | ||
offsetX, | ||
offsetY | ||
} = (_raycaster$computeOff = raycaster.computeOffsets == null ? void 0 : raycaster.computeOffsets(event, state)) != null ? _raycaster$computeOff : event; | ||
const { | ||
width, | ||
height | ||
} = size; | ||
mouse.set(offsetX / width * 2 - 1, -(offsetY / height) * 2 + 1); | ||
raycaster.setFromCamera(mouse, camera); | ||
} | ||
/** Calculates delta */ | ||
function calculateDistance(event) { | ||
const { | ||
internal | ||
} = store.getState(); | ||
const dx = event.offsetX - internal.initialClick[0]; | ||
const dy = event.offsetY - internal.initialClick[1]; | ||
return Math.round(Math.sqrt(dx * dx + dy * dy)); | ||
} | ||
/** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */ | ||
function filterPointerEvents(objects) { | ||
return objects.filter(obj => ['Move', 'Over', 'Enter', 'Out', 'Leave'].some(name => { | ||
var _r3f$handlers; | ||
return (_r3f$handlers = obj.__r3f.handlers) == null ? void 0 : _r3f$handlers['onPointer' + name]; | ||
})); | ||
} | ||
function intersect(filter) { | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
internal | ||
} = state; // Skip event handling when noEvents is set | ||
if (!raycaster.enabled) return []; | ||
const seen = new Set(); | ||
const intersections = []; // Allow callers to eliminate event objects | ||
const eventsObjects = filter ? filter(internal.interaction) : internal.interaction; // Intersect known handler objects and filter against duplicates | ||
let intersects = raycaster.intersectObjects(eventsObjects, true).filter(item => { | ||
const id = makeId(item); | ||
if (seen.has(id)) return false; | ||
seen.add(id); | ||
return true; | ||
}); // https://github.com/mrdoob/three.js/issues/16031 | ||
// Allow custom userland intersect sort order | ||
if (raycaster.filter) intersects = raycaster.filter(intersects, state); | ||
for (const intersect of intersects) { | ||
let eventObject = intersect.object; // Bubble event up | ||
while (eventObject) { | ||
var _r3f; | ||
const handlers = (_r3f = eventObject.__r3f) == null ? void 0 : _r3f.handlers; | ||
if (handlers) intersections.push({ ...intersect, | ||
eventObject | ||
}); | ||
eventObject = eventObject.parent; | ||
} | ||
} | ||
return intersections; | ||
} | ||
/** Creates filtered intersects and returns an array of positive hits */ | ||
function patchIntersects(intersections, event) { | ||
const { | ||
internal | ||
} = store.getState(); // If the interaction is captured take that into account, the captured event has to be part of the intersects | ||
if (internal.captured && event.type !== 'click' && event.type !== 'wheel') { | ||
internal.captured.forEach(captured => { | ||
if (!intersections.find(hit => hit.eventObject === captured.eventObject)) intersections.push(captured); | ||
}); | ||
} | ||
return intersections; | ||
} | ||
/** Handles intersections by forwarding them to handlers */ | ||
function handleIntersects(intersections, event, callback) { | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
internal | ||
} = store.getState(); // If anything has been found, forward it to the event listeners | ||
if (intersections.length) { | ||
const unprojectedPoint = temp.set(mouse.x, mouse.y, 0).unproject(camera); | ||
const delta = event.type === 'click' ? calculateDistance(event) : 0; | ||
const releasePointerCapture = id => event.target.releasePointerCapture(id); | ||
const localState = { | ||
stopped: false, | ||
captured: false | ||
}; | ||
for (const hit of intersections) { | ||
const setPointerCapture = id => { | ||
// If the hit is going to be captured flag that we're in captured state | ||
if (!localState.captured) { | ||
localState.captured = true; // The captured hit array is reset to collect hits | ||
internal.captured = []; | ||
} // Push hits to the array | ||
if (internal.captured) internal.captured.push(hit) // Call the original event now | ||
; | ||
event.target.setPointerCapture(id); | ||
}; // Add native event props | ||
let extractEventProps = {}; | ||
for (let prop in Object.getPrototypeOf(event)) { | ||
extractEventProps[prop] = event[prop]; | ||
} | ||
let raycastEvent = { ...hit, | ||
...extractEventProps, | ||
intersections, | ||
stopped: localState.stopped, | ||
delta, | ||
unprojectedPoint, | ||
ray: raycaster.ray, | ||
camera: camera, | ||
// Hijack stopPropagation, which just sets a flag | ||
stopPropagation: () => { | ||
// https://github.com/pmndrs/react-three-fiber/issues/596 | ||
// Events are not allowed to stop propagation if the pointer has been captured | ||
const cap = internal.captured; | ||
if (!cap || cap.find(h => h.eventObject.id === hit.eventObject.id)) { | ||
raycastEvent.stopped = localState.stopped = true; // Propagation is stopped, remove all other hover records | ||
// An event handler is only allowed to flush other handlers if it is hovered itself | ||
if (internal.hovered.size && Array.from(internal.hovered.values()).find(i => i.eventObject === hit.eventObject)) { | ||
// Objects cannot flush out higher up objects that have already caught the event | ||
const higher = intersections.slice(0, intersections.indexOf(hit)); | ||
cancelPointer([...higher, hit]); | ||
} | ||
} | ||
}, | ||
target: { ...event.target, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
currentTarget: { ...event.currentTarget, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
sourceEvent: event | ||
}; // Call subscribers | ||
callback(raycastEvent); // Event bubbling may be interrupted by stopPropagation | ||
if (localState.stopped === true) break; | ||
} | ||
} | ||
return intersections; | ||
} | ||
function cancelPointer(hits) { | ||
const { | ||
internal | ||
} = store.getState(); | ||
Array.from(internal.hovered.values()).forEach(hoveredObj => { | ||
// When no objects were hit or the the hovered object wasn't found underneath the cursor | ||
// we call onPointerOut and delete the object from the hovered-elements map | ||
if (!hits.length || !hits.find(hit => hit.object === hoveredObj.object && hit.index === hoveredObj.index)) { | ||
const eventObject = hoveredObj.eventObject; | ||
const handlers = eventObject.__r3f.handlers; | ||
internal.hovered.delete(makeId(hoveredObj)); | ||
if (handlers) { | ||
// Clear out intersects, they are outdated by now | ||
const data = { ...hoveredObj, | ||
intersections: hits || [] | ||
}; | ||
handlers.onPointerOut == null ? void 0 : handlers.onPointerOut(data); | ||
handlers.onPointerLeave == null ? void 0 : handlers.onPointerLeave(data); | ||
} | ||
} | ||
}); | ||
} | ||
const handlePointer = name => { | ||
// Deal with cancelation | ||
switch (name) { | ||
case 'onPointerLeave': | ||
case 'onPointerCancel': | ||
return () => cancelPointer([]); | ||
case 'onLostPointerCapture': | ||
return () => (store.getState().internal.captured = undefined, cancelPointer([])); | ||
} // Any other pointer goes here ... | ||
return event => { | ||
const { | ||
onPointerMissed, | ||
internal | ||
} = store.getState(); | ||
prepareRay(event); // Get fresh intersects | ||
const isPointerMove = name === 'onPointerMove'; | ||
const filter = isPointerMove ? filterPointerEvents : undefined; | ||
const hits = patchIntersects(intersect(filter), event); // Take care of unhover | ||
if (isPointerMove) cancelPointer(hits); | ||
handleIntersects(hits, event, data => { | ||
const eventObject = data.eventObject; | ||
const handlers = eventObject.__r3f.handlers; // Check presence of handlers | ||
if (!handlers) return; | ||
if (isPointerMove) { | ||
// Move event ... | ||
if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) { | ||
// When enter or out is present take care of hover-state | ||
const id = makeId(data); | ||
const hoveredItem = internal.hovered.get(id); | ||
if (!hoveredItem) { | ||
// If the object wasn't previously hovered, book it and call its handler | ||
internal.hovered.set(id, data); | ||
handlers.onPointerOver == null ? void 0 : handlers.onPointerOver(data); | ||
handlers.onPointerEnter == null ? void 0 : handlers.onPointerEnter(data); | ||
} else if (hoveredItem.stopped) { | ||
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed | ||
data.stopPropagation(); | ||
} | ||
} // Call mouse move | ||
handlers.onPointerMove == null ? void 0 : handlers.onPointerMove(data); | ||
} else { | ||
// All other events ... | ||
const handler = handlers == null ? void 0 : handlers[name]; | ||
if (handler) { | ||
// Forward all events back to their respective handlers with the exception of click events, | ||
// which must use the initial target | ||
if (name !== 'onClick' && name !== 'onContextMenu' && name !== 'onDoubleClick' || internal.initialHits.includes(eventObject)) { | ||
handler(data); | ||
pointerMissed(event, internal.interaction.filter(object => object !== eventObject)); | ||
} | ||
} | ||
} | ||
}); // Save initial coordinates on pointer-down | ||
if (name === 'onPointerDown') { | ||
internal.initialClick = [event.offsetX, event.offsetY]; | ||
internal.initialHits = hits.map(hit => hit.eventObject); | ||
} // If a click yields no results, pass it back to the user as a miss | ||
if ((name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick') && !hits.length) { | ||
if (calculateDistance(event) <= 2) { | ||
pointerMissed(event, internal.interaction); | ||
if (onPointerMissed) onPointerMissed(); | ||
} | ||
} | ||
}; | ||
}; | ||
function pointerMissed(event, objects) { | ||
objects.forEach(object => { | ||
var _r3f$handlers2; | ||
return (_r3f$handlers2 = object.__r3f.handlers) == null ? void 0 : _r3f$handlers2.onPointerMissed == null ? void 0 : _r3f$handlers2.onPointerMissed(event); | ||
}); | ||
} | ||
return { | ||
handlePointer | ||
}; | ||
} | ||
// Type guard to tell a store from a portal | ||
@@ -385,14 +712,16 @@ const isStore = def => def && !!def.getState; | ||
function removeRecursive(array, parent, clone = false) { | ||
if (array) { | ||
// Three uses splice op's internally we may have to shallow-clone the array in order to safely remove items | ||
const target = clone ? [...array] : array; | ||
target.forEach(child => removeChild(parent, child)); | ||
} | ||
function removeRecursive(array, parent, dispose = false) { | ||
if (array) [...array].forEach(child => removeChild(parent, child, dispose)); | ||
} | ||
function removeChild(parentInstance, child) { | ||
function removeChild(parentInstance, child, dispose) { | ||
if (child) { | ||
if (child.isObject3D) { | ||
parentInstance.remove(child); | ||
var _child$__r3f; | ||
parentInstance.remove(child); // Remove interactivity | ||
if ((_child$__r3f = child.__r3f) != null && _child$__r3f.root) { | ||
removeInteractivity(child.__r3f.root, child); | ||
} | ||
} else { | ||
@@ -409,23 +738,20 @@ child.parent = null; | ||
} | ||
} // Remove interactivity | ||
} // Allow objects to bail out of recursive dispose alltogether by passing dispose={null} | ||
// Never dispose of primitives because their state may be kept outside of React! | ||
// In order for an object to be able to dispose it has to have | ||
// - a dispose method, | ||
// - it cannot be an <instance object={...} /> | ||
// - it cannot be a THREE.Scene, because three has broken it's own api | ||
// | ||
// Since disposal is recursive, we can check the optional dispose arg, which will be undefined | ||
// when the reconciler calls it, but then carry our own check recursively | ||
if (child.__r3f.root) { | ||
const rootState = child.__r3f.root.getState(); | ||
const shouldDispose = dispose === undefined ? child.dispose !== null && !child.__r3f.instance : dispose; // Remove nested child objects | ||
rootState.internal.interaction = rootState.internal.interaction.filter(x => x !== child); | ||
} | ||
removeRecursive(child.__r3f.objects, child, shouldDispose); | ||
removeRecursive(child.children, child, shouldDispose); // Dispose item whenever the reconciler feels like it | ||
invalidateInstance(parentInstance); // Allow objects to bail out of recursive dispose alltogether by passing dispose={null} | ||
// Never dispose of primitives because their state may be kept outside of React! | ||
if (child.dispose && !child.__r3f.instance) { | ||
const objects = child.__r3f.objects; | ||
unstable_runWithPriority(unstable_IdlePriority, () => { | ||
// Remove nested child objects | ||
removeRecursive(objects, child); | ||
removeRecursive(child.children, child, true); // Dispose item | ||
if (child.dispose && child.type !== 'Scene') child.dispose(); | ||
}); | ||
if (shouldDispose && child.dispose && child.type !== 'Scene') { | ||
unstable_runWithPriority(unstable_IdlePriority, () => child.dispose()); | ||
} // Remove references | ||
@@ -439,2 +765,3 @@ | ||
delete child.__r3f; | ||
invalidateInstance(parentInstance); | ||
} | ||
@@ -781,2 +1108,3 @@ } | ||
interaction: [], | ||
hovered: new Map(), | ||
subscribers: [], | ||
@@ -939,314 +1267,2 @@ captured: undefined, | ||
function createEvents(store) { | ||
const hovered = new Map(); | ||
const temp = new THREE.Vector3(); | ||
/** Sets up defaultRaycaster */ | ||
function prepareRay(event) { | ||
var _raycaster$computeOff; | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
size | ||
} = state; // https://github.com/pmndrs/react-three-fiber/pull/782 | ||
// Events trigger outside of canvas when moved | ||
const { | ||
offsetX, | ||
offsetY | ||
} = (_raycaster$computeOff = raycaster.computeOffsets == null ? void 0 : raycaster.computeOffsets(event, state)) != null ? _raycaster$computeOff : event; | ||
const { | ||
width, | ||
height | ||
} = size; | ||
mouse.set(offsetX / width * 2 - 1, -(offsetY / height) * 2 + 1); | ||
raycaster.setFromCamera(mouse, camera); | ||
} | ||
/** Calculates delta */ | ||
function calculateDistance(event) { | ||
const { | ||
internal | ||
} = store.getState(); | ||
const dx = event.offsetX - internal.initialClick[0]; | ||
const dy = event.offsetY - internal.initialClick[1]; | ||
return Math.round(Math.sqrt(dx * dx + dy * dy)); | ||
} | ||
/** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */ | ||
function filterPointerEvents(objects) { | ||
return objects.filter(obj => ['Move', 'Over', 'Enter', 'Out', 'Leave'].some(name => { | ||
var _r3f$handlers; | ||
return (_r3f$handlers = obj.__r3f.handlers) == null ? void 0 : _r3f$handlers['onPointer' + name]; | ||
})); | ||
} | ||
function makeId(event) { | ||
return (event.eventObject || event.object).uuid + '/' + event.index; | ||
} | ||
function intersect(filter) { | ||
const state = store.getState(); | ||
const { | ||
raycaster, | ||
internal | ||
} = state; // Skip event handling when noEvents is set | ||
if (!raycaster.enabled) return []; | ||
const seen = new Set(); | ||
const intersections = []; // Allow callers to eliminate event objects | ||
const eventsObjects = filter ? filter(internal.interaction) : internal.interaction; // Intersect known handler objects and filter against duplicates | ||
let intersects = raycaster.intersectObjects(eventsObjects, true).filter(item => { | ||
const id = makeId(item); | ||
if (seen.has(id)) return false; | ||
seen.add(id); | ||
return true; | ||
}); // https://github.com/mrdoob/three.js/issues/16031 | ||
// Allow custom userland intersect sort order | ||
if (raycaster.filter) intersects = raycaster.filter(intersects, state); | ||
for (const intersect of intersects) { | ||
let eventObject = intersect.object; // Bubble event up | ||
while (eventObject) { | ||
var _r3f; | ||
const handlers = (_r3f = eventObject.__r3f) == null ? void 0 : _r3f.handlers; | ||
if (handlers) intersections.push({ ...intersect, | ||
eventObject | ||
}); | ||
eventObject = eventObject.parent; | ||
} | ||
} | ||
return intersections; | ||
} | ||
/** Creates filtered intersects and returns an array of positive hits */ | ||
function patchIntersects(intersections, event) { | ||
const { | ||
internal | ||
} = store.getState(); // If the interaction is captured take that into account, the captured event has to be part of the intersects | ||
if (internal.captured && event.type !== 'click' && event.type !== 'wheel') { | ||
internal.captured.forEach(captured => { | ||
if (!intersections.find(hit => hit.eventObject === captured.eventObject)) intersections.push(captured); | ||
}); | ||
} | ||
return intersections; | ||
} | ||
/** Handles intersections by forwarding them to handlers */ | ||
function handleIntersects(intersections, event, callback) { | ||
const { | ||
raycaster, | ||
mouse, | ||
camera, | ||
internal | ||
} = store.getState(); // If anything has been found, forward it to the event listeners | ||
if (intersections.length) { | ||
const unprojectedPoint = temp.set(mouse.x, mouse.y, 0).unproject(camera); | ||
const delta = event.type === 'click' ? calculateDistance(event) : 0; | ||
const releasePointerCapture = id => event.target.releasePointerCapture(id); | ||
const localState = { | ||
stopped: false, | ||
captured: false | ||
}; | ||
for (const hit of intersections) { | ||
const setPointerCapture = id => { | ||
// If the hit is going to be captured flag that we're in captured state | ||
if (!localState.captured) { | ||
localState.captured = true; // The captured hit array is reset to collect hits | ||
internal.captured = []; | ||
} // Push hits to the array | ||
if (internal.captured) internal.captured.push(hit) // Call the original event now | ||
; | ||
event.target.setPointerCapture(id); | ||
}; // Add native event props | ||
let extractEventProps = {}; | ||
for (let prop in Object.getPrototypeOf(event)) { | ||
extractEventProps[prop] = event[prop]; | ||
} | ||
let raycastEvent = { ...hit, | ||
...extractEventProps, | ||
intersections, | ||
stopped: localState.stopped, | ||
delta, | ||
unprojectedPoint, | ||
ray: raycaster.ray, | ||
camera: camera, | ||
// Hijack stopPropagation, which just sets a flag | ||
stopPropagation: () => { | ||
// https://github.com/pmndrs/react-three-fiber/issues/596 | ||
// Events are not allowed to stop propagation if the pointer has been captured | ||
const cap = internal.captured; | ||
if (!cap || cap.find(h => h.eventObject.id === hit.eventObject.id)) { | ||
raycastEvent.stopped = localState.stopped = true; // Propagation is stopped, remove all other hover records | ||
// An event handler is only allowed to flush other handlers if it is hovered itself | ||
if (hovered.size && Array.from(hovered.values()).find(i => i.eventObject === hit.eventObject)) { | ||
// Objects cannot flush out higher up objects that have already caught the event | ||
const higher = intersections.slice(0, intersections.indexOf(hit)); | ||
cancelPointer([...higher, hit]); | ||
} | ||
} | ||
}, | ||
target: { ...event.target, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
currentTarget: { ...event.currentTarget, | ||
setPointerCapture, | ||
releasePointerCapture | ||
}, | ||
sourceEvent: event | ||
}; // Call subscribers | ||
callback(raycastEvent); // Event bubbling may be interrupted by stopPropagation | ||
if (localState.stopped === true) break; | ||
} | ||
} | ||
return intersections; | ||
} | ||
function cancelPointer(hits) { | ||
Array.from(hovered.values()).forEach(hoveredObj => { | ||
// When no objects were hit or the the hovered object wasn't found underneath the cursor | ||
// we call onPointerOut and delete the object from the hovered-elements map | ||
if (!hits.length || !hits.find(hit => hit.object === hoveredObj.object && hit.index === hoveredObj.index)) { | ||
const eventObject = hoveredObj.eventObject; | ||
const handlers = eventObject.__r3f.handlers; | ||
hovered.delete(makeId(hoveredObj)); | ||
if (handlers) { | ||
// Clear out intersects, they are outdated by now | ||
const data = { ...hoveredObj, | ||
intersections: hits || [] | ||
}; | ||
handlers.onPointerOut == null ? void 0 : handlers.onPointerOut(data); | ||
handlers.onPointerLeave == null ? void 0 : handlers.onPointerLeave(data); | ||
} | ||
} | ||
}); | ||
} | ||
const handlePointer = name => { | ||
// Deal with cancelation | ||
switch (name) { | ||
case 'onPointerLeave': | ||
case 'onPointerCancel': | ||
return () => cancelPointer([]); | ||
case 'onLostPointerCapture': | ||
return () => (store.getState().internal.captured = undefined, cancelPointer([])); | ||
} // Any other pointer goes here ... | ||
return event => { | ||
const { | ||
onPointerMissed, | ||
internal | ||
} = store.getState(); | ||
prepareRay(event); // Get fresh intersects | ||
const isPointerMove = name === 'onPointerMove'; | ||
const filter = isPointerMove ? filterPointerEvents : undefined; | ||
const hits = patchIntersects(intersect(filter), event); // Take care of unhover | ||
if (isPointerMove) cancelPointer(hits); | ||
handleIntersects(hits, event, data => { | ||
const eventObject = data.eventObject; | ||
const handlers = eventObject.__r3f.handlers; // Check presence of handlers | ||
if (!handlers) return; | ||
if (isPointerMove) { | ||
// Move event ... | ||
if (handlers.onPointerOver || handlers.onPointerEnter || handlers.onPointerOut || handlers.onPointerLeave) { | ||
// When enter or out is present take care of hover-state | ||
const id = makeId(data); | ||
const hoveredItem = hovered.get(id); | ||
if (!hoveredItem) { | ||
// If the object wasn't previously hovered, book it and call its handler | ||
hovered.set(id, data); | ||
handlers.onPointerOver == null ? void 0 : handlers.onPointerOver(data); | ||
handlers.onPointerEnter == null ? void 0 : handlers.onPointerEnter(data); | ||
} else if (hoveredItem.stopped) { | ||
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed | ||
data.stopPropagation(); | ||
} | ||
} // Call mouse move | ||
handlers.onPointerMove == null ? void 0 : handlers.onPointerMove(data); | ||
} else { | ||
// All other events ... | ||
const handler = handlers == null ? void 0 : handlers[name]; | ||
if (handler) { | ||
// Forward all events back to their respective handlers with the exception of click events, | ||
// which must use the initial target | ||
if (name !== 'onClick' && name !== 'onContextMenu' && name !== 'onDoubleClick' || internal.initialHits.includes(eventObject)) { | ||
handler(data); | ||
pointerMissed(event, internal.interaction.filter(object => object !== eventObject)); | ||
} | ||
} | ||
} | ||
}); // Save initial coordinates on pointer-down | ||
if (name === 'onPointerDown') { | ||
internal.initialClick = [event.offsetX, event.offsetY]; | ||
internal.initialHits = hits.map(hit => hit.eventObject); | ||
} // If a click yields no results, pass it back to the user as a miss | ||
if ((name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick') && !hits.length) { | ||
if (calculateDistance(event) <= 2) { | ||
pointerMissed(event, internal.interaction); | ||
if (onPointerMissed) onPointerMissed(); | ||
} | ||
} | ||
}; | ||
}; | ||
function pointerMissed(event, objects) { | ||
objects.forEach(object => { | ||
var _r3f$handlers2; | ||
return (_r3f$handlers2 = object.__r3f.handlers) == null ? void 0 : _r3f$handlers2.onPointerMissed == null ? void 0 : _r3f$handlers2.onPointerMissed(event); | ||
}); | ||
} | ||
return { | ||
handlePointer | ||
}; | ||
} | ||
function createPointerEvents(store) { | ||
@@ -1253,0 +1269,0 @@ const { |
{ | ||
"name": "@react-three/fiber", | ||
"version": "6.0.8", | ||
"version": "6.0.9", | ||
"description": "A React renderer for Threejs", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
213883
4817