@react-three/fiber
Version:
A React renderer for Threejs
1,290 lines (1,230 loc) • 78.4 kB
JavaScript
'use strict';
var THREE = require('three');
var React = require('react');
var constants = require('react-reconciler/constants');
var create = require('zustand');
var Reconciler = require('react-reconciler');
var scheduler = require('scheduler');
var suspendReact = require('suspend-react');
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return Object.freeze(n);
}
var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
var React__namespace = /*#__PURE__*/_interopNamespace(React);
var create__default = /*#__PURE__*/_interopDefault(create);
var Reconciler__default = /*#__PURE__*/_interopDefault(Reconciler);
var threeTypes = /*#__PURE__*/Object.freeze({
__proto__: null
});
var _window$document, _window$navigator;
const isOrthographicCamera = def => def && def.isOrthographicCamera;
const isRef = obj => obj && obj.hasOwnProperty('current');
/**
* An SSR-friendly useLayoutEffect.
*
* React currently throws a warning when using useLayoutEffect on the server.
* To get around it, we can conditionally useEffect on the server (no-op) and
* useLayoutEffect elsewhere.
*
* @see https://github.com/facebook/react/issues/14927
*/
const useIsomorphicLayoutEffect = typeof window !== 'undefined' && ((_window$document = window.document) != null && _window$document.createElement || ((_window$navigator = window.navigator) == null ? void 0 : _window$navigator.product) === 'ReactNative') ? React__namespace.useLayoutEffect : React__namespace.useEffect;
function useMutableCallback(fn) {
const ref = React__namespace.useRef(fn);
useIsomorphicLayoutEffect(() => void (ref.current = fn), [fn]);
return ref;
}
function Block({
set
}) {
useIsomorphicLayoutEffect(() => {
set(new Promise(() => null));
return () => set(false);
}, [set]);
return null;
}
class ErrorBoundary extends React__namespace.Component {
state = {
error: false
};
static getDerivedStateFromError = () => ({
error: true
});
componentDidCatch(err) {
this.props.set(err);
}
render() {
return this.state.error ? null : this.props.children;
}
}
const DEFAULT = '__default';
const isDiffSet = def => def && !!def.memoized && !!def.changes;
function calculateDpr(dpr) {
const target = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
return Array.isArray(dpr) ? Math.min(Math.max(dpr[0], target), dpr[1]) : dpr;
}
/**
* Returns instance root state
*/
const getRootState = obj => {
var _r3f;
return (_r3f = obj.__r3f) == null ? void 0 : _r3f.root.getState();
};
// A collection of compare functions
const is = {
obj: a => a === Object(a) && !is.arr(a) && typeof a !== 'function',
fun: a => typeof a === 'function',
str: a => typeof a === 'string',
num: a => typeof a === 'number',
boo: a => typeof a === 'boolean',
und: a => a === void 0,
arr: a => Array.isArray(a),
equ(a, b, {
arrays = 'shallow',
objects = 'reference',
strict = true
} = {}) {
// Wrong type or one of the two undefined, doesn't match
if (typeof a !== typeof b || !!a !== !!b) return false;
// Atomic, just compare a against b
if (is.str(a) || is.num(a)) return a === b;
const isObj = is.obj(a);
if (isObj && objects === 'reference') return a === b;
const isArr = is.arr(a);
if (isArr && arrays === 'reference') return a === b;
// Array or Object, shallow compare first to see if it's a match
if ((isArr || isObj) && a === b) return true;
// Last resort, go through keys
let i;
for (i in a) if (!(i in b)) return false;
for (i in strict ? b : a) if (a[i] !== b[i]) return false;
if (is.und(i)) {
if (isArr && a.length === 0 && b.length === 0) return true;
if (isObj && Object.keys(a).length === 0 && Object.keys(b).length === 0) return true;
if (a !== b) return false;
}
return true;
}
};
// Collects nodes and materials from a THREE.Object3D
function buildGraph(object) {
const data = {
nodes: {},
materials: {}
};
if (object) {
object.traverse(obj => {
if (obj.name) data.nodes[obj.name] = obj;
if (obj.material && !data.materials[obj.material.name]) data.materials[obj.material.name] = obj.material;
});
}
return data;
}
// Disposes an object and all its properties
function dispose(obj) {
if (obj.dispose && obj.type !== 'Scene') obj.dispose();
for (const p in obj) {
p.dispose == null ? void 0 : p.dispose();
delete obj[p];
}
}
// Each object in the scene carries a small LocalState descriptor
function prepare(object, state) {
const instance = object;
if (state != null && state.primitive || !instance.__r3f) {
instance.__r3f = {
type: '',
root: null,
previousAttach: null,
memoizedProps: {},
eventCount: 0,
handlers: {},
objects: [],
parent: null,
...state
};
}
return object;
}
function resolve(instance, key) {
let target = instance;
if (key.includes('-')) {
const entries = key.split('-');
const last = entries.pop();
target = entries.reduce((acc, key) => acc[key], instance);
return {
target,
key: last
};
} else return {
target,
key
};
}
// Checks if a dash-cased string ends with an integer
const INDEX_REGEX = /-\d+$/;
function attach(parent, child, type) {
if (is.str(type)) {
// If attaching into an array (foo-0), create one
if (INDEX_REGEX.test(type)) {
const root = type.replace(INDEX_REGEX, '');
const {
target,
key
} = resolve(parent, root);
if (!Array.isArray(target[key])) target[key] = [];
}
const {
target,
key
} = resolve(parent, type);
child.__r3f.previousAttach = target[key];
target[key] = child;
} else child.__r3f.previousAttach = type(parent, child);
}
function detach(parent, child, type) {
var _child$__r3f, _child$__r3f2;
if (is.str(type)) {
const {
target,
key
} = resolve(parent, type);
const previous = child.__r3f.previousAttach;
// When the previous value was undefined, it means the value was never set to begin with
if (previous === undefined) delete target[key];
// Otherwise set the previous value
else target[key] = previous;
} else (_child$__r3f = child.__r3f) == null ? void 0 : _child$__r3f.previousAttach == null ? void 0 : _child$__r3f.previousAttach(parent, child);
(_child$__r3f2 = child.__r3f) == null ? true : delete _child$__r3f2.previousAttach;
}
// This function prepares a set of changes to be applied to the instance
function diffProps(instance, {
children: cN,
key: kN,
ref: rN,
...props
}, {
children: cP,
key: kP,
ref: rP,
...previous
} = {}, remove = false) {
var _instance$__r3f;
const localState = (_instance$__r3f = instance == null ? void 0 : instance.__r3f) != null ? _instance$__r3f : {};
const entries = Object.entries(props);
const changes = [];
// Catch removed props, prepend them so they can be reset or removed
if (remove) {
const previousKeys = Object.keys(previous);
for (let i = 0; i < previousKeys.length; i++) {
if (!props.hasOwnProperty(previousKeys[i])) entries.unshift([previousKeys[i], DEFAULT + 'remove']);
}
}
entries.forEach(([key, value]) => {
var _instance$__r3f2;
// Bail out on primitive object
if ((_instance$__r3f2 = instance.__r3f) != null && _instance$__r3f2.primitive && key === 'object') return;
// When props match bail out
if (is.equ(value, previous[key])) return;
// Collect handlers and bail out
if (/^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/.test(key)) return changes.push([key, value, true, []]);
// Split dashed props
let entries = [];
if (key.includes('-')) entries = key.split('-');
changes.push([key, value, false, entries]);
// Reset pierced props
for (const prop in props) {
const value = props[prop];
if (prop.startsWith(`${key}-`)) changes.push([prop, value, false, prop.split('-')]);
}
});
const memoized = {
...props
};
if (localState.memoizedProps && localState.memoizedProps.args) memoized.args = localState.memoizedProps.args;
if (localState.memoizedProps && localState.memoizedProps.attach) memoized.attach = localState.memoizedProps.attach;
return {
memoized,
changes
};
}
// This function applies a set of changes to the instance
function applyProps$1(instance, data) {
var _instance$__r3f3, _root$getState, _instance$__r3f4;
// Filter equals, events and reserved props
const localState = (_instance$__r3f3 = instance.__r3f) != null ? _instance$__r3f3 : {};
const root = localState.root;
const rootState = (_root$getState = root == null ? void 0 : root.getState == null ? void 0 : root.getState()) != null ? _root$getState : {};
const {
memoized,
changes
} = isDiffSet(data) ? data : diffProps(instance, data);
const prevHandlers = localState.eventCount;
// Prepare memoized props
if (instance.__r3f) instance.__r3f.memoizedProps = memoized;
for (let i = 0; i < changes.length; i++) {
let [key, value, isEvent, keys] = changes[i];
let currentInstance = instance;
let targetProp = currentInstance[key];
// Revolve dashed props
if (keys.length) {
targetProp = keys.reduce((acc, key) => acc[key], instance);
// If the target is atomic, it forces us to switch the root
if (!(targetProp && targetProp.set)) {
const [name, ...reverseEntries] = keys.reverse();
currentInstance = reverseEntries.reverse().reduce((acc, key) => acc[key], instance);
key = name;
}
}
// https://github.com/mrdoob/three.js/issues/21209
// HMR/fast-refresh relies on the ability to cancel out props, but threejs
// has no means to do this. Hence we curate a small collection of value-classes
// with their respective constructor/set arguments
// For removed props, try to set default values, if possible
if (value === DEFAULT + 'remove') {
if (currentInstance.constructor) {
var _currentInstance$__r;
// create a blank slate of the instance and copy the particular parameter.
// @ts-ignore
const defaultClassCall = new currentInstance.constructor(...((_currentInstance$__r = currentInstance.__r3f.memoizedProps.args) != null ? _currentInstance$__r : []));
value = defaultClassCall[key];
// destroy the instance
if (defaultClassCall.dispose) defaultClassCall.dispose();
} else {
// instance does not have constructor, just set it to 0
value = 0;
}
}
// Deal with pointer events ...
if (isEvent) {
if (value) localState.handlers[key] = value;else delete localState.handlers[key];
localState.eventCount = Object.keys(localState.handlers).length;
}
// Special treatment for objects with support for set/copy, and layers
else if (targetProp && targetProp.set && (targetProp.copy || targetProp instanceof THREE__namespace.Layers)) {
// If value is an array
if (Array.isArray(value)) {
if (targetProp.fromArray) targetProp.fromArray(value);else targetProp.set(...value);
}
// Test again target.copy(class) next ...
else if (targetProp.copy && value && value.constructor && targetProp.constructor.name === value.constructor.name) {
targetProp.copy(value);
}
// If nothing else fits, just set the single value, ignore undefined
// https://github.com/pmndrs/react-three-fiber/issues/274
else if (value !== undefined) {
const isColor = targetProp instanceof THREE__namespace.Color;
// Allow setting array scalars
if (!isColor && targetProp.setScalar) targetProp.setScalar(value);
// Layers have no copy function, we must therefore copy the mask property
else if (targetProp instanceof THREE__namespace.Layers && value instanceof THREE__namespace.Layers) targetProp.mask = value.mask;
// Otherwise just set ...
else targetProp.set(value);
// For versions of three which don't support THREE.ColorManagement,
// Auto-convert sRGB colors
// https://github.com/pmndrs/react-three-fiber/issues/344
const supportsColorManagement = ('ColorManagement' in THREE__namespace);
if (!supportsColorManagement && !rootState.linear && isColor) targetProp.convertSRGBToLinear();
}
// Else, just overwrite the value
} else {
currentInstance[key] = value;
// Auto-convert sRGB textures, for now ...
// https://github.com/pmndrs/react-three-fiber/issues/344
if (!rootState.linear && currentInstance[key] instanceof THREE__namespace.Texture) {
currentInstance[key].encoding = THREE__namespace.sRGBEncoding;
}
}
invalidateInstance(instance);
}
if (localState.parent && rootState.internal && instance.raycast && prevHandlers !== localState.eventCount) {
// Pre-emptively remove the instance from the interaction manager
const index = rootState.internal.interaction.indexOf(instance);
if (index > -1) rootState.internal.interaction.splice(index, 1);
// Add the instance to the interaction manager only when it has handlers
if (localState.eventCount) rootState.internal.interaction.push(instance);
}
// Call the update lifecycle when it is being updated, but only when it is part of the scene
if (changes.length && (_instance$__r3f4 = instance.__r3f) != null && _instance$__r3f4.parent) updateInstance(instance);
return instance;
}
function invalidateInstance(instance) {
var _instance$__r3f5, _instance$__r3f5$root;
const state = (_instance$__r3f5 = instance.__r3f) == null ? void 0 : (_instance$__r3f5$root = _instance$__r3f5.root) == null ? void 0 : _instance$__r3f5$root.getState == null ? void 0 : _instance$__r3f5$root.getState();
if (state && state.internal.frames === 0) state.invalidate();
}
function updateInstance(instance) {
instance.onUpdate == null ? void 0 : instance.onUpdate(instance);
}
function updateCamera(camera, size) {
// https://github.com/pmndrs/react-three-fiber/issues/92
// Do not mess with the camera if it belongs to the user
if (!camera.manual) {
if (isOrthographicCamera(camera)) {
camera.left = size.width / -2;
camera.right = size.width / 2;
camera.top = size.height / 2;
camera.bottom = size.height / -2;
} else {
camera.aspect = size.width / size.height;
}
camera.updateProjectionMatrix();
// https://github.com/pmndrs/react-three-fiber/issues/178
// Update matrix world since the renderer is a frame late
camera.updateMatrixWorld();
}
}
/**
* Safely sets a deeply-nested value on an object.
*/
function setDeep(obj, value, keys) {
const key = keys.pop();
const target = keys.reduce((acc, key) => acc[key], obj);
return target[key] = value;
}
function makeId(event) {
return (event.eventObject || event.object).uuid + '/' + event.index + event.instanceId;
}
// https://github.com/facebook/react/tree/main/packages/react-reconciler#getcurrenteventpriority
// Gives React a clue as to how import the current interaction is
function getEventPriority() {
var _globalScope$event;
// Get a handle to the current global scope in window and worker contexts if able
// https://github.com/pmndrs/react-three-fiber/pull/2493
const globalScope = typeof self !== 'undefined' && self || typeof window !== 'undefined' && window;
if (!globalScope) return constants.DefaultEventPriority;
const name = (_globalScope$event = globalScope.event) == null ? void 0 : _globalScope$event.type;
switch (name) {
case 'click':
case 'contextmenu':
case 'dblclick':
case 'pointercancel':
case 'pointerdown':
case 'pointerup':
return constants.DiscreteEventPriority;
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'pointerenter':
case 'pointerleave':
case 'wheel':
return constants.ContinuousEventPriority;
default:
return constants.DefaultEventPriority;
}
}
/**
* Release pointer captures.
* This is called by releasePointerCapture in the API, and when an object is removed.
*/
function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
const captureData = captures.get(obj);
if (captureData) {
captures.delete(obj);
// If this was the last capturing object for this pointer
if (captures.size === 0) {
capturedMap.delete(pointerId);
captureData.target.releasePointerCapture(pointerId);
}
}
}
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) {
// Clear out intersects, they are outdated by now
internal.hovered.delete(key);
}
});
internal.capturedMap.forEach((captures, pointerId) => {
releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
});
}
function createEvents(store) {
/** 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;
return (_r3f = obj.__r3f) == null ? void 0 : _r3f.handlers['onPointer' + name];
}));
}
function intersect(event, filter) {
const state = store.getState();
const duplicates = new Set();
const intersections = [];
// Allow callers to eliminate event objects
const eventsObjects = filter ? filter(state.internal.interaction) : state.internal.interaction;
// Reset all raycaster cameras to undefined
for (let i = 0; i < eventsObjects.length; i++) {
const state = getRootState(eventsObjects[i]);
if (state) {
state.raycaster.camera = undefined;
}
}
if (!state.previousRoot) {
// Make sure root-level pointer and ray are set up
state.events.compute == null ? void 0 : state.events.compute(event, state);
}
function handleRaycast(obj) {
const state = getRootState(obj);
// Skip event handling when noEvents is set, or when the raycasters camera is null
if (!state || !state.events.enabled || state.raycaster.camera === null) return [];
// When the camera is undefined we have to call the event layers update function
if (state.raycaster.camera === undefined) {
var _state$previousRoot;
state.events.compute == null ? void 0 : state.events.compute(event, state, (_state$previousRoot = state.previousRoot) == null ? void 0 : _state$previousRoot.getState());
// If the camera is still undefined we have to skip this layer entirely
if (state.raycaster.camera === undefined) state.raycaster.camera = null;
}
// Intersect object by object
return state.raycaster.camera ? state.raycaster.intersectObject(obj, true) : [];
}
// Collect events
let hits = eventsObjects
// Intersect objects
.flatMap(handleRaycast)
// Sort by event priority and distance
.sort((a, b) => {
const aState = getRootState(a.object);
const bState = getRootState(b.object);
if (!aState || !bState) return a.distance - b.distance;
return bState.events.priority - aState.events.priority || a.distance - b.distance;
})
// Filter out duplicates
.filter(item => {
const id = makeId(item);
if (duplicates.has(id)) return false;
duplicates.add(id);
return true;
});
// https://github.com/mrdoob/three.js/issues/16031
// Allow custom userland intersect sort order, this likely only makes sense on the root filter
if (state.events.filter) hits = state.events.filter(hits, state);
// Bubble up the events, find the event source (eventObject)
for (const hit of hits) {
let eventObject = hit.object;
// Bubble event up
while (eventObject) {
var _r3f2;
if ((_r3f2 = eventObject.__r3f) != null && _r3f2.eventCount) intersections.push({
...hit,
eventObject
});
eventObject = eventObject.parent;
}
}
// If the interaction is captured, make all capturing targets part of the intersect.
if ('pointerId' in event && state.internal.capturedMap.has(event.pointerId)) {
for (let captureData of state.internal.capturedMap.get(event.pointerId).values()) {
if (!duplicates.has(makeId(captureData.intersection))) intersections.push(captureData.intersection);
}
}
return intersections;
}
/** Handles intersections by forwarding them to handlers */
function handleIntersects(intersections, event, delta, callback) {
const rootState = store.getState();
// If anything has been found, forward it to the event listeners
if (intersections.length) {
const localState = {
stopped: false
};
for (const hit of intersections) {
const state = getRootState(hit.object) || rootState;
const {
raycaster,
pointer,
camera,
internal
} = state;
const unprojectedPoint = new THREE__namespace.Vector3(pointer.x, pointer.y, 0).unproject(camera);
const hasPointerCapture = id => {
var _internal$capturedMap, _internal$capturedMap2;
return (_internal$capturedMap = (_internal$capturedMap2 = internal.capturedMap.get(id)) == null ? void 0 : _internal$capturedMap2.has(hit.eventObject)) != null ? _internal$capturedMap : false;
};
const setPointerCapture = id => {
const captureData = {
intersection: hit,
target: event.target
};
if (internal.capturedMap.has(id)) {
// if the pointerId was previously captured, we add the hit to the
// event capturedMap.
internal.capturedMap.get(id).set(hit.eventObject, captureData);
} else {
// if the pointerId was not previously captured, we create a map
// containing the hitObject, and the hit. hitObject is used for
// faster access.
internal.capturedMap.set(id, new Map([[hit.eventObject, captureData]]));
}
event.target.setPointerCapture(id);
};
const releasePointerCapture = id => {
const captures = internal.capturedMap.get(id);
if (captures) {
releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
}
};
// Add native event props
let extractEventProps = {};
// This iterates over the event's properties including the inherited ones. Native PointerEvents have most of their props as getters which are inherited, but polyfilled PointerEvents have them all as their own properties (i.e. not inherited). We can't use Object.keys() or Object.entries() as they only return "own" properties; nor Object.getPrototypeOf(event) as that *doesn't* return "own" properties, only inherited ones.
for (let prop in event) {
let property = event[prop];
// Only copy over atomics, leave functions alone as these should be
// called as event.nativeEvent.fn()
if (typeof property !== 'function') extractEventProps[prop] = property;
}
let raycastEvent = {
...hit,
...extractEventProps,
pointer,
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 capturesForPointer = 'pointerId' in event && internal.capturedMap.get(event.pointerId);
// We only authorize stopPropagation...
if (
// ...if this pointer hasn't been captured
!capturesForPointer ||
// ... or if the hit object is capturing the pointer
capturesForPointer.has(hit.eventObject)) {
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]);
}
}
},
// there should be a distinction between target and currentTarget
target: {
hasPointerCapture,
setPointerCapture,
releasePointerCapture
},
currentTarget: {
hasPointerCapture,
setPointerCapture,
releasePointerCapture
},
nativeEvent: event
};
// Call subscribers
callback(raycastEvent);
// Event bubbling may be interrupted by stopPropagation
if (localState.stopped === true) break;
}
}
return intersections;
}
function cancelPointer(intersections) {
const {
internal
} = store.getState();
for (const hoveredObj of internal.hovered.values()) {
// 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 (!intersections.length || !intersections.find(hit => hit.object === hoveredObj.object && hit.index === hoveredObj.index && hit.instanceId === hoveredObj.instanceId)) {
const eventObject = hoveredObj.eventObject;
const instance = eventObject.__r3f;
const handlers = instance == null ? void 0 : instance.handlers;
internal.hovered.delete(makeId(hoveredObj));
if (instance != null && instance.eventCount) {
// Clear out intersects, they are outdated by now
const data = {
...hoveredObj,
intersections
};
handlers.onPointerOut == null ? void 0 : handlers.onPointerOut(data);
handlers.onPointerLeave == null ? void 0 : handlers.onPointerLeave(data);
}
}
}
}
function pointerMissed(event, objects) {
for (let i = 0; i < objects.length; i++) {
const instance = objects[i].__r3f;
instance == null ? void 0 : instance.handlers.onPointerMissed == null ? void 0 : instance.handlers.onPointerMissed(event);
}
}
function handlePointer(name) {
// Deal with cancelation
switch (name) {
case 'onPointerLeave':
case 'onPointerCancel':
return () => cancelPointer([]);
case 'onLostPointerCapture':
return event => {
const {
internal
} = store.getState();
if ('pointerId' in event && internal.capturedMap.has(event.pointerId)) {
// If the object event interface had onLostPointerCapture, we'd call it here on every
// object that's getting removed.
internal.capturedMap.delete(event.pointerId);
cancelPointer([]);
}
};
}
// Any other pointer goes here ...
return function handleEvent(event) {
const {
onPointerMissed,
internal
} = store.getState();
// prepareRay(event)
internal.lastEvent.current = event;
// Get fresh intersects
const isPointerMove = name === 'onPointerMove';
const isClickEvent = name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick';
const filter = isPointerMove ? filterPointerEvents : undefined;
// const hits = patchIntersects(intersect(filter), event)
const hits = intersect(event, filter);
const delta = isClickEvent ? calculateDistance(event) : 0;
// 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
// Missed events have to come first in order to establish user-land side-effect clean up
if (isClickEvent && !hits.length) {
if (delta <= 2) {
pointerMissed(event, internal.interaction);
if (onPointerMissed) onPointerMissed(event);
}
}
// Take care of unhover
if (isPointerMove) cancelPointer(hits);
function onIntersect(data) {
const eventObject = data.eventObject;
const instance = eventObject.__r3f;
const handlers = instance == null ? void 0 : instance.handlers;
// Check presence of handlers
if (!(instance != null && instance.eventCount)) 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[name];
if (handler) {
// Forward all events back to their respective handlers with the exception of click events,
// which must use the initial target
if (!isClickEvent || internal.initialHits.includes(eventObject)) {
// Missed events have to come first
pointerMissed(event, internal.interaction.filter(object => !internal.initialHits.includes(object)));
// Now call the handler
handler(data);
}
} else {
// Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit
if (isClickEvent && internal.initialHits.includes(eventObject)) {
pointerMissed(event, internal.interaction.filter(object => !internal.initialHits.includes(object)));
}
}
}
}
handleIntersects(hits, event, delta, onIntersect);
};
}
return {
handlePointer
};
}
let catalogue = {};
let extend = objects => void (catalogue = {
...catalogue,
...objects
});
function createRenderer(_roots, _getEventPriority) {
function createInstance(type, {
args = [],
attach,
...props
}, root) {
let name = `${type[0].toUpperCase()}${type.slice(1)}`;
let instance;
if (type === 'primitive') {
if (props.object === undefined) throw new Error("R3F: Primitives without 'object' are invalid!");
const object = props.object;
instance = prepare(object, {
type,
root,
attach,
primitive: true
});
} else {
const target = catalogue[name];
if (!target) {
throw new Error(`R3F: ${name} is not part of the THREE namespace! Did you forget to extend? See: https://docs.pmnd.rs/react-three-fiber/api/objects#using-3rd-party-objects-declaratively`);
}
// Throw if an object or literal was passed for args
if (!Array.isArray(args)) throw new Error('R3F: The args prop must be an array!');
// Instanciate new object, link it to the root
// Append memoized props with args so it's not forgotten
instance = prepare(new target(...args), {
type,
root,
attach,
// Save args in case we need to reconstruct later for HMR
memoizedProps: {
args
}
});
}
// Auto-attach geometries and materials
if (instance.__r3f.attach === undefined) {
if (instance instanceof THREE__namespace.BufferGeometry) instance.__r3f.attach = 'geometry';else if (instance instanceof THREE__namespace.Material) instance.__r3f.attach = 'material';
}
// It should NOT call onUpdate on object instanciation, because it hasn't been added to the
// view yet. If the callback relies on references for instance, they won't be ready yet, this is
// why it passes "true" here
// There is no reason to apply props to injects
if (name !== 'inject') applyProps$1(instance, props);
return instance;
}
function appendChild(parentInstance, child) {
let added = false;
if (child) {
var _child$__r3f, _parentInstance$__r3f;
// The attach attribute implies that the object attaches itself on the parent
if ((_child$__r3f = child.__r3f) != null && _child$__r3f.attach) {
attach(parentInstance, child, child.__r3f.attach);
} else if (child.isObject3D && parentInstance.isObject3D) {
// add in the usual parent-child way
parentInstance.add(child);
added = true;
}
// This is for anything that used attach, and for non-Object3Ds that don't get attached to props;
// that is, anything that's a child in React but not a child in the scenegraph.
if (!added) (_parentInstance$__r3f = parentInstance.__r3f) == null ? void 0 : _parentInstance$__r3f.objects.push(child);
if (!child.__r3f) prepare(child, {});
child.__r3f.parent = parentInstance;
updateInstance(child);
invalidateInstance(child);
}
}
function insertBefore(parentInstance, child, beforeChild) {
let added = false;
if (child) {
var _child$__r3f2, _parentInstance$__r3f2;
if ((_child$__r3f2 = child.__r3f) != null && _child$__r3f2.attach) {
attach(parentInstance, child, child.__r3f.attach);
} else if (child.isObject3D && parentInstance.isObject3D) {
child.parent = parentInstance;
child.dispatchEvent({
type: 'added'
});
const restSiblings = parentInstance.children.filter(sibling => sibling !== child);
const index = restSiblings.indexOf(beforeChild);
parentInstance.children = [...restSiblings.slice(0, index), child, ...restSiblings.slice(index)];
added = true;
}
if (!added) (_parentInstance$__r3f2 = parentInstance.__r3f) == null ? void 0 : _parentInstance$__r3f2.objects.push(child);
if (!child.__r3f) prepare(child, {});
child.__r3f.parent = parentInstance;
updateInstance(child);
invalidateInstance(child);
}
}
function removeRecursive(array, parent, dispose = false) {
if (array) [...array].forEach(child => removeChild(parent, child, dispose));
}
function removeChild(parentInstance, child, dispose) {
if (child) {
var _parentInstance$__r3f3, _child$__r3f3, _child$__r3f5;
// Clear the parent reference
if (child.__r3f) child.__r3f.parent = null;
// Remove child from the parents objects
if ((_parentInstance$__r3f3 = parentInstance.__r3f) != null && _parentInstance$__r3f3.objects) parentInstance.__r3f.objects = parentInstance.__r3f.objects.filter(x => x !== child);
// Remove attachment
if ((_child$__r3f3 = child.__r3f) != null && _child$__r3f3.attach) {
detach(parentInstance, child, child.__r3f.attach);
} else if (child.isObject3D && parentInstance.isObject3D) {
var _child$__r3f4;
parentInstance.remove(child);
// Remove interactivity
if ((_child$__r3f4 = child.__r3f) != null && _child$__r3f4.root) {
removeInteractivity(child.__r3f.root, child);
}
}
// Allow objects to bail out of recursive dispose altogether 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 a <primitive 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
const isPrimitive = (_child$__r3f5 = child.__r3f) == null ? void 0 : _child$__r3f5.primitive;
const shouldDispose = dispose === undefined ? child.dispose !== null && !isPrimitive : dispose;
// Remove nested child objects. Primitives should not have objects and children that are
// attached to them declaratively ...
if (!isPrimitive) {
var _child$__r3f6;
removeRecursive((_child$__r3f6 = child.__r3f) == null ? void 0 : _child$__r3f6.objects, child, shouldDispose);
removeRecursive(child.children, child, shouldDispose);
}
// Remove references
if (child.__r3f) {
delete child.__r3f.root;
delete child.__r3f.objects;
delete child.__r3f.handlers;
delete child.__r3f.memoizedProps;
if (!isPrimitive) delete child.__r3f;
}
// Dispose item whenever the reconciler feels like it
if (shouldDispose && child.dispose && child.type !== 'Scene') {
scheduler.unstable_scheduleCallback(scheduler.unstable_IdlePriority, () => {
try {
child.dispose();
} catch (e) {
/* ... */
}
});
}
invalidateInstance(parentInstance);
}
}
function switchInstance(instance, type, newProps, fiber) {
var _instance$__r3f;
const parent = (_instance$__r3f = instance.__r3f) == null ? void 0 : _instance$__r3f.parent;
if (!parent) return;
const newInstance = createInstance(type, newProps, instance.__r3f.root);
// https://github.com/pmndrs/react-three-fiber/issues/1348
// When args change the instance has to be re-constructed, which then
// forces r3f to re-parent the children and non-scene objects
if (instance.children) {
for (const child of instance.children) {
if (child.__r3f) appendChild(newInstance, child);
}
instance.children = instance.children.filter(child => !child.__r3f);
}
instance.__r3f.objects.forEach(child => appendChild(newInstance, child));
instance.__r3f.objects = [];
if (!instance.__r3f.autoRemovedBeforeAppend) {
removeChild(parent, instance);
}
if (newInstance.parent) {
newInstance.__r3f.autoRemovedBeforeAppend = true;
}
appendChild(parent, newInstance);
// Re-bind event handlers
if (newInstance.raycast && newInstance.__r3f.eventCount) {
const rootState = newInstance.__r3f.root.getState();
rootState.internal.interaction.push(newInstance);
}
[fiber, fiber.alternate].forEach(fiber => {
if (fiber !== null) {
fiber.stateNode = newInstance;
if (fiber.ref) {
if (typeof fiber.ref === 'function') fiber.ref(newInstance);else fiber.ref.current = newInstance;
}
}
});
}
// Don't handle text instances, warn on undefined behavior
const handleTextInstance = () => console.warn('Text is not allowed in the R3F tree! This could be stray whitespace or characters.');
const reconciler = Reconciler__default["default"]({
createInstance,
removeChild,
appendChild,
appendInitialChild: appendChild,
insertBefore,
supportsMutation: true,
isPrimaryRenderer: false,
supportsPersistence: false,
supportsHydration: false,
noTimeout: -1,
appendChildToContainer: (container, child) => {
if (!child) return;
// Don't append to unmounted container
const scene = container.getState().scene;
if (!scene.__r3f) return;
// Link current root to the default scene
scene.__r3f.root = container;
appendChild(scene, child);
},
removeChildFromContainer: (container, child) => {
if (!child) return;
removeChild(container.getState().scene, child);
},
insertInContainerBefore: (container, child, beforeChild) => {
if (!child || !beforeChild) return;
// Don't append to unmounted container
const scene = container.getState().scene;
if (!scene.__r3f) return;
insertBefore(scene, child, beforeChild);
},
getRootHostContext: () => null,
getChildHostContext: parentHostContext => parentHostContext,
finalizeInitialChildren(instance) {
var _instance$__r3f2;
const localState = (_instance$__r3f2 = instance == null ? void 0 : instance.__r3f) != null ? _instance$__r3f2 : {};
// https://github.com/facebook/react/issues/20271
// Returning true will trigger commitMount
return Boolean(localState.handlers);
},
prepareUpdate(instance, _type, oldProps, newProps) {
// Create diff-sets
if (instance.__r3f.primitive && newProps.object && newProps.object !== instance) {
return [true];
} else {
// This is a data object, let's extract critical information about it
const {
args: argsNew = [],
children: cN,
...restNew
} = newProps;
const {
args: argsOld = [],
children: cO,
...restOld
} = oldProps;
// Throw if an object or literal was passed for args
if (!Array.isArray(argsNew)) throw new Error('R3F: the args prop must be an array!');
// If it has new props or arguments, then it needs to be re-instantiated
if (argsNew.some((value, index) => value !== argsOld[index])) return [true];
// Create a diff-set, flag if there are any changes
const diff = diffProps(instance, restNew, restOld, true);
if (diff.changes.length) return [false, diff];
// Otherwise do not touch the instance
return null;
}
},
commitUpdate(instance, [reconstruct, diff], type, _oldProps, newProps, fiber) {
// Reconstruct when args or <primitive object={...} have changes
if (reconstruct) switchInstance(instance, type, newProps, fiber);
// Otherwise just overwrite props
else applyProps$1(instance, diff);
},
commitMount(instance, _type, _props, _int) {
var _instance$__r3f3;
// https://github.com/facebook/react/issues/20271
// This will make sure events are only added once to the central container
const localState = (_instance$__r3f3 = instance.__r3f) != null ? _instance$__r3f3 : {};
if (instance.raycast && localState.handlers && localState.eventCount) {
instance.__r3f.root.getState().internal.interaction.push(instance);
}
},
getPublicInstance: instance => instance,
prepareForCommit: () => null,
preparePortalMount: container => prepare(container.getState().scene),
resetAfterCommit: () => {},
shouldSetTextContent: () => false,
clearContainer: () => false,
hideInstance(instance) {
var _instance$__r3f4;
// Detach while the instance is hidden
const {
attach: type,
parent
} = (_instance$__r3f4 = instance.__r3f) != null ? _instance$__r3f4 : {};
if (type && parent) detach(parent, instance, type);
if (instance.isObject3D) instance.visible = false;
invalidateInstance(instance);
},
unhideInstance(instance, props) {
var _instance$__r3f5;
// Re-attach when the instance is unhidden
const {
attach: type,
parent
} = (_instance$__r3f5 = instance.__r3f) != null ? _instance$__r3f5 : {};
if (type && parent) attach(parent, instance, type);
if (instance.isObject3D && props.visible == null || props.visible) instance.visible = true;
invalidateInstance(instance);
},
createTextInstance: handleTextInstance,
hideTextInstance: handleTextInstance,
unhideTextInstance: handleTextInstance,
// https://github.com/pmndrs/react-three-fiber/pull/2360#discussion_r916356874
// @ts-ignore
getCurrentEventPriority: () => _getEventPriority ? _getEventPriority() : constants.DefaultEventPriority,
beforeActiveInstanceBlur: () => {},
afterActiveInstanceBlur: () => {},
detachDeletedInstance: () => {},
now: typeof performance !== 'undefined' && is.fun(performance.now) ? performance.now : is.fun(Date.now) ? Date.now : () => 0,
// https://github.com/pmndrs/react-three-fiber/pull/2360#discussion_r920883503
scheduleTimeout: is.fun(setTimeout) ? setTimeout : undefined,
cancelTimeout: is.fun(clearTimeout) ? clearTimeout : undefined
});
return {
reconciler,
applyProps: applyProps$1
};
}
// Keys that shouldn't be copied between R3F stores
const privateKeys = ['set', 'get', 'setSize', 'setFrameloop', 'setDpr', 'events', 'invalidate', 'advance', 'size', 'viewport'];
const isRenderer = def => !!(def != null && def.render);
const context = /*#__PURE__*/React__namespace.createContext(null);
const createStore = (invalidate, advance) => {
const rootState = create__default["default"]((set, get) => {
const position = new THREE__namespace.Vector3();
const defaultTarget = new THREE__namespace.Vector3();
const tempTarget = new THREE__namespace.Vector3();
function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
const {
width,
height,
top,
left
} = size;
const aspect = width / height;
if (target instanceof THREE__namespace.Vector3) tempTarget.copy(target);else tempTarget.set(...target);
const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
if (isOrthographicCamera(camera)) {
return {
width: width / camera.zoom,
height: height / camera.zoom,
top,
left,
factor: 1,
distance,
aspect
};
} else {
const fov = camera.fov * Math.PI / 180; // convert vertical fov to radians
const h = 2 * Math.tan(fov / 2) * distance; // visible height
const w = h * (width / height);
return {
width: w,
height: h,
top,
left,
factor: width / w,
distance,
aspect
};
}
}
let performanceTimeout = undefined;
const setPerformanceCurrent = current => set(state => ({
performance: {
...state.performance,
current
}
}));
const pointer = new THREE__namespace.Vector2();
const rootState = {
set,
get,
// Mock objects that have to be configured
gl: null,
camera: null,
raycaster: null,
events: {
priority: 1,
enabled: true,
connected: false
},
xr: null,
invalidate: (frames = 1) => invalidate(get(), frames),
advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
legacy: false,
linear: false,
flat: false,
scene: prepare(new THREE__namespace.Scene()),
controls: null,
clock: new THREE__namespace.Clock(),
pointer,
mouse: pointer,
frameloop: 'always',
onPointerMissed: undefined,
performance: {
current: 1,
min: 0.5,
max: 1,
debounce: 200,
regress: () => {
const state = get();
// Clear timeout
if (performanceTimeout) clearTimeout(performanceTimeout);
// Set lower bound performance
if (state.performance.current !== state.performance.min) setPerformanceCurrent(state.performance.min);
// Go back to upper bound performance after a while unless something regresses meanwhile
performanceTimeout = setTimeout(() => setPerformanceCurrent(get().performance.max), state.performance.debounce);
}
},
size: {
width: 0,
height: 0,
top: 0,
left: 0,
updateStyle: false
},
viewport: {
initialDpr: 0,
dpr: 0,
width: 0,
height: 0,
top: 0,
left: 0,
aspect: 0,
distance: 0,
factor: 0,
getCurrentViewport
},
setEvents: events => set(state => ({
...state,
events: {
...state.events,
...events
}
})),
setSize: (width, height, updateStyle, top, left) => {