@react-three/fiber
Version:
A React renderer for Threejs
1,452 lines (1,199 loc) • 60.1 kB
JavaScript
import * as THREE from 'three';
import * as React from 'react';
import create from 'zustand';
import shallow from 'zustand/shallow';
import Reconciler from 'react-reconciler';
import { unstable_now, unstable_runWithPriority, unstable_IdlePriority } from 'scheduler';
import { useAsset } from 'use-asset';
import mergeRefs from 'react-merge-refs';
import useMeasure from 'react-use-measure';
var threeTypes = /*#__PURE__*/Object.freeze({
__proto__: null
});
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',
und: a => a === void 0,
arr: a => Array.isArray(a),
equ(a, b) {
// 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) || is.obj(a)) return a === b; // Array, shallow compare first to see if it's a match
if (is.arr(a) && a == b) return true; // Last resort, go through keys
let i;
for (i in a) if (!(i in b)) return false;
for (i in b) if (a[i] !== b[i]) return false;
return is.und(i) ? a === b : true;
}
};
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, make all capturing targets part of the
// intersect.
if ('pointerId' in event && internal.capturedMap.has(event.pointerId)) {
intersections.push(...internal.capturedMap.get(event.pointerId).values());
}
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
};
for (const hit of intersections) {
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 => {
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, hit);
} 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, hit]]));
} // Call the original event now
event.target.setPointerCapture(id);
}; // Add native event props
let extractEventProps = {};
for (let prop in Object.getPrototypeOf(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,
spaceX: mouse.x,
spaceY: mouse.y,
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
},
sourceEvent: event,
// deprecated
nativeEvent: 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 event => {
if ('pointerId' in event) {
// this will be a problem if one target releases the pointerId
// and another one is still keeping it, as the line below
// indifferently deletes all capturing references.
store.getState().internal.capturedMap.delete(event.pointerId);
}
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(event);
}
}
};
};
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
const isStore = def => def && !!def.getState;
const getContainer = (container, child) => {
var _container$__r3f$root, _container$__r3f;
return {
// If the container is not a root-store then it must be a THREE.Object3D into which part of the
// scene is portalled into. Now there can be two variants of this, either that object is part of
// the regular jsx tree, in which case it already has __r3f with a valid root attached, or it lies
// outside react, in which case we must take the root of the child that is about to be attached to it.
root: isStore(container) ? container : (_container$__r3f$root = (_container$__r3f = container.__r3f) == null ? void 0 : _container$__r3f.root) != null ? _container$__r3f$root : child.__r3f.root,
// The container is the eventual target into which objects are mounted, it has to be a THREE.Object3D
container: isStore(container) ? container.getState().scene : container
};
};
const DEFAULT = '__default';
const EMPTY = {};
const FILTER = ['children', 'key', 'ref'];
let catalogue = {};
let extend = objects => void (catalogue = { ...catalogue,
...objects
}); // Each object in the scene carries a small LocalState descriptor
function prepare(object, state) {
const instance = object;
if (state != null && state.instance || !instance.__r3f) {
instance.__r3f = {
root: null,
memoizedProps: {},
objects: [],
...state
};
}
return object;
}
function createRenderer(roots) {
function applyProps(instance, newProps, oldProps = {}, accumulative = false) {
var _instance$__r3f, _root$getState, _instance$__r3f2;
// Filter equals, events and reserved props
const localState = (_instance$__r3f = instance == null ? void 0 : instance.__r3f) != null ? _instance$__r3f : {};
const root = localState.root;
const rootState = (_root$getState = root == null ? void 0 : root.getState == null ? void 0 : root.getState()) != null ? _root$getState : {};
const sameProps = [];
const handlers = [];
const newMemoizedProps = {};
let i = 0;
Object.entries(newProps).forEach(([key, entry]) => {
// we don't want children, ref or key in the memoized props
if (FILTER.indexOf(key) === -1) {
newMemoizedProps[key] = entry;
}
});
if (localState.memoizedProps && localState.memoizedProps.args) {
newMemoizedProps.args = localState.memoizedProps.args;
}
if (localState.memoizedProps && localState.memoizedProps.attach) {
newMemoizedProps.attach = localState.memoizedProps.attach;
}
if (instance.__r3f) {
instance.__r3f.memoizedProps = newMemoizedProps;
}
let objectKeys = Object.keys(newProps);
for (i = 0; i < objectKeys.length; i++) {
if (is.equ(newProps[objectKeys[i]], oldProps[objectKeys[i]])) {
sameProps.push(objectKeys[i]);
} // Event-handlers ...
// are functions, that
// start with "on", and
// contain the name "Pointer", "Click", "DoubleClick", "ContextMenu", or "Wheel"
if (is.fun(newProps[objectKeys[i]]) && /^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/.test(objectKeys[i])) {
handlers.push(objectKeys[i]);
}
} // Catch props that existed, but now exist no more ...
const leftOvers = [];
if (accumulative) {
objectKeys = Object.keys(oldProps);
for (i = 0; i < objectKeys.length; i++) {
if (!newProps.hasOwnProperty(objectKeys[i])) {
leftOvers.push(objectKeys[i]);
}
}
}
const toFilter = [...sameProps, ...FILTER]; // Instances use "object" as a reserved identifier
if ((_instance$__r3f2 = instance.__r3f) != null && _instance$__r3f2.instance) toFilter.push('object');
const filteredProps = { ...newProps
}; // Removes sameProps and reserved props from newProps
objectKeys = Object.keys(filteredProps);
for (i = 0; i < objectKeys.length; i++) {
if (toFilter.indexOf(objectKeys[i]) > -1) {
delete filteredProps[objectKeys[i]];
}
} // Collect all new props
const filteredPropsEntries = Object.entries(filteredProps); // Prepend left-overs so they can be reset or removed
// Left-overs must come first!
for (i = 0; i < leftOvers.length; i++) {
if (leftOvers[i] !== 'children') {
filteredPropsEntries.unshift([leftOvers[i], DEFAULT + 'remove']);
}
}
if (filteredPropsEntries.length > 0) {
filteredPropsEntries.forEach(([key, value]) => {
if (!handlers.includes(key)) {
let currentInstance = instance;
let targetProp = currentInstance[key];
if (key.includes('-')) {
const entries = key.split('-');
targetProp = entries.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] = entries.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 (targetProp && targetProp.constructor) {
// use the prop constructor to find the default it should be
value = new targetProp.constructor(newMemoizedProps.args);
} else if (currentInstance.constructor) {
// create a blank slate of the instance and copy the particular parameter.
// @ts-ignore
const defaultClassCall = new currentInstance.constructor(currentInstance.__r3f.memoizedProps.args);
value = defaultClassCall[targetProp]; // destory the instance
if (defaultClassCall.dispose) {
defaultClassCall.dispose();
}
} else {
// instance does not have constructor, just set it to 0
value = 0;
}
} // Special treatment for objects with support for set/copy, and layers
if (targetProp && targetProp.set && (targetProp.copy || targetProp instanceof THREE.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/react-spring/react-three-fiber/issues/274
else if (value !== undefined) {
const isColor = targetProp instanceof THREE.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.Layers && value instanceof THREE.Layers) targetProp.mask = value.mask; // Otherwise just set ...
else targetProp.set(value); // Auto-convert sRGB colors, for now ...
// https://github.com/react-spring/react-three-fiber/issues/344
if (!rootState.linear && isColor) targetProp.convertSRGBToLinear();
} // Else, just overwrite the value
} else {
currentInstance[key] = value; // Auto-convert sRGB textures, for now ...
// https://github.com/react-spring/react-three-fiber/issues/344
if (!rootState.linear && currentInstance[key] instanceof THREE.Texture) currentInstance[key].encoding = THREE.sRGBEncoding;
}
invalidateInstance(instance);
}
}); // Preemptively delete the instance from the containers interaction
if (accumulative && root && instance.raycast && localState.handlers) {
localState.handlers = undefined;
const index = rootState.internal.interaction.indexOf(instance);
if (index > -1) rootState.internal.interaction.splice(index, 1);
} // Prep interaction handlers
if (handlers.length) {
if (accumulative && root && instance.raycast) {
rootState.internal.interaction.push(instance);
} // Add handlers to the instances handler-map
localState.handlers = handlers.reduce((acc, key) => ({ ...acc,
[key]: newProps[key]
}), {});
} // Call the update lifecycle when it is being updated, but only when it is part of the scene
if (instance.parent) updateInstance(instance);
}
}
function invalidateInstance(instance) {
var _instance$__r3f3, _instance$__r3f3$root;
const state = (_instance$__r3f3 = instance.__r3f) == null ? void 0 : (_instance$__r3f3$root = _instance$__r3f3.root) == null ? void 0 : _instance$__r3f3$root.getState == null ? void 0 : _instance$__r3f3$root.getState();
if (state && state.internal.frames === 0) state.invalidate();
}
function updateInstance(instance) {
instance.onUpdate == null ? void 0 : instance.onUpdate(instance);
}
function createInstance(type, {
args = [],
...props
}, root, hostContext, internalInstanceHandle) {
let name = `${type[0].toUpperCase()}${type.slice(1)}`;
let instance; // https://github.com/facebook/react/issues/17147
// Portals do not give us a root, they are themselves treated as a root by the reconciler
// In order to figure out the actual root we have to climb through fiber internals :(
if (!isStore(root) && internalInstanceHandle) {
const fn = node => {
if (!node.return) return node.stateNode && node.stateNode.containerInfo;else return fn(node.return);
};
root = fn(internalInstanceHandle);
} // Assert that by now we have a valid root
if (!root || !isStore(root)) throw `No valid root for ${name}!`;
if (type === 'primitive') {
if (props.object === undefined) throw `Primitives without 'object' are invalid!`;
const object = props.object;
instance = prepare(object, {
root,
instance: true
});
} else {
const target = catalogue[name] || THREE[name];
if (!target) throw `${name} is not part of the THREE namespace! Did you forget to extend? See: https://github.com/pmndrs/react-three-fiber/blob/master/markdown/api.md#using-3rd-party-objects-declaratively`;
const isArgsArr = is.arr(args); // Instanciate new object, link it to the root
instance = prepare(isArgsArr ? new target(...args) : new target(args), {
root,
// append memoized props with args so it's not forgotten
memoizedProps: {
args: isArgsArr && args.length === 0 ? null : args
}
});
} // Auto-attach geometries and materials
if (!('attachFns' in props)) {
if (name.endsWith('Geometry')) {
props = {
attach: 'geometry',
...props
};
} else if (name.endsWith('Material')) {
props = {
attach: 'material',
...props
};
}
} // 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
applyProps(instance, props, {});
return instance;
}
function appendChild(parentInstance, child) {
let addedAsChild = false;
if (child) {
// The attach attribute implies that the object attaches itself on the parent
if (child.attachArray) {
if (!is.arr(parentInstance[child.attachArray])) parentInstance[child.attachArray] = [];
parentInstance[child.attachArray].push(child);
} else if (child.attachObject) {
if (!is.obj(parentInstance[child.attachObject[0]])) parentInstance[child.attachObject[0]] = {};
parentInstance[child.attachObject[0]][child.attachObject[1]] = child;
} else if (child.attach && !is.fun(child.attach)) {
parentInstance[child.attach] = child;
} else if (is.arr(child.attachFns)) {
const [attachFn] = child.attachFns;
if (is.str(attachFn) && is.fun(parentInstance[attachFn])) {
parentInstance[attachFn](child);
} else if (is.fun(attachFn)) {
attachFn(child, parentInstance);
}
} else if (child.isObject3D) {
// add in the usual parent-child way
parentInstance.add(child);
addedAsChild = true;
}
if (!addedAsChild) {
// 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.
parentInstance.__r3f.objects.push(child);
child.parent = parentInstance;
}
updateInstance(child);
invalidateInstance(child);
}
}
function insertBefore(parentInstance, child, beforeChild) {
let added = false;
if (child) {
if (child.attachArray) {
const array = parentInstance[child.attachArray];
if (!is.arr(array)) parentInstance[child.attachArray] = [];
array.splice(array.indexOf(beforeChild), 0, child);
} else if (child.attachObject || child.attach && !is.fun(child.attach)) {
// attach and attachObject don't have an order anyway, so just append
return appendChild(parentInstance, child);
} else if (child.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.__r3f.objects.push(child);
child.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 _child$__r3f2;
if (parentInstance.__r3f.objects) {
const oldLength = parentInstance.__r3f.objects.length;
parentInstance.__r3f.objects = parentInstance.__r3f.objects.filter(x => x !== child);
const newLength = parentInstance.__r3f.objects.length; // was it in the list?
if (newLength < oldLength) {
// we had also set this, so we must clear it now
child.parent = null;
}
} // Remove attachment
if (child.attachArray) {
parentInstance[child.attachArray] = parentInstance[child.attachArray].filter(x => x !== child);
} else if (child.attachObject) {
delete parentInstance[child.attachObject[0]][child.attachObject[1]];
} else if (child.attach && !is.fun(child.attach)) {
parentInstance[child.attach] = null;
} else if (is.arr(child.attachFns)) {
const [, detachFn] = child.attachFns;
if (is.str(detachFn) && is.fun(parentInstance[detachFn])) {
parentInstance[detachFn](child);
} else if (is.fun(detachFn)) {
detachFn(child, parentInstance);
}
} else if (child.isObject3D) {
var _child$__r3f;
parentInstance.remove(child); // Remove interactivity
if ((_child$__r3f = child.__r3f) != null && _child$__r3f.root) {
removeInteractivity(child.__r3f.root, child);
}
} // 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
const isInstance = (_child$__r3f2 = child.__r3f) == null ? void 0 : _child$__r3f2.instance;
const shouldDispose = dispose === undefined ? child.dispose !== null && !isInstance : dispose; // Remove nested child objects. Primitives should not have objects and children that are
// attached to them declaratively ...
if (!isInstance) {
var _child$__r3f3;
removeRecursive((_child$__r3f3 = child.__r3f) == null ? void 0 : _child$__r3f3.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 (!isInstance) delete child.__r3f;
} // Dispose item whenever the reconciler feels like it
if (shouldDispose && child.dispose && child.type !== 'Scene') {
unstable_runWithPriority(unstable_IdlePriority, () => child.dispose());
}
invalidateInstance(parentInstance);
}
}
function switchInstance(instance, type, newProps, fiber) {
const parent = instance.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) {
instance.children.forEach(child => appendChild(newInstance, child));
instance.children = [];
}
instance.__r3f.objects.forEach(child => appendChild(newInstance, child));
instance.__r3f.objects = [];
removeChild(parent, instance);
appendChild(parent, newInstance) // This evil hack switches the react-internal fiber node
// https://github.com/facebook/react/issues/14983
// https://github.com/facebook/react/pull/15021
;
[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;
}
}
});
}
const reconciler = Reconciler({
now: unstable_now,
createInstance,
removeChild,
appendChild,
appendInitialChild: appendChild,
insertBefore,
warnsIfNotActing: true,
supportsMutation: true,
isPrimaryRenderer: false,
// @ts-ignore
scheduleTimeout: is.fun(setTimeout) ? setTimeout : undefined,
// @ts-ignore
cancelTimeout: is.fun(clearTimeout) ? clearTimeout : undefined,
// @ts-ignore
setTimeout: is.fun(setTimeout) ? setTimeout : undefined,
// @ts-ignore
clearTimeout: is.fun(clearTimeout) ? clearTimeout : undefined,
noTimeout: -1,
appendChildToContainer: (parentInstance, child) => {
const {
container,
root
} = getContainer(parentInstance, child); // Link current root to the default scene
container.__r3f.root = root;
appendChild(container, child);
},
removeChildFromContainer: (parentInstance, child) => {
const {
container
} = getContainer(parentInstance, child);
removeChild(container, child);
},
insertInContainerBefore: (parentInstance, child, beforeChild) => {
const {
container
} = getContainer(parentInstance, child);
insertBefore(container, child, beforeChild);
},
commitUpdate(instance, updatePayload, type, oldProps, newProps, fiber) {
if (instance.__r3f.instance && newProps.object && newProps.object !== instance) {
// <instance object={...} /> where the object reference has changed
switchInstance(instance, type, newProps, fiber);
} else {
// This is a data object, let's extract critical information about it
const {
args: argsNew = [],
...restNew
} = newProps;
const {
args: argsOld = [],
...restOld
} = oldProps; // If it has new props or arguments, then it needs to be re-instanciated
const hasNewArgs = argsNew.some((value, index) => is.obj(value) ? Object.entries(value).some(([key, val]) => val !== argsOld[index][key]) : value !== argsOld[index]);
if (hasNewArgs) {
// Next we create a new instance and append it again
switchInstance(instance, type, newProps, fiber);
} else {
// Otherwise just overwrite props
applyProps(instance, restNew, restOld, true);
}
}
},
hideInstance(instance) {
if (instance.isObject3D) {
instance.visible = false;
invalidateInstance(instance);
}
},
unhideInstance(instance, props) {
if (instance.isObject3D && props.visible == null || props.visible) {
instance.visible = true;
invalidateInstance(instance);
}
},
hideTextInstance() {
throw new Error('Text is not allowed in the R3F tree.');
},
getPublicInstance(instance) {
// TODO: might fix switchInstance (?)
return instance;
},
getRootHostContext(rootContainer) {
return EMPTY;
},
getChildHostContext(parentHostContext) {
return EMPTY;
},
createTextInstance() {},
finalizeInitialChildren(instance) {
// https://github.com/facebook/react/issues/20271
// Returning true will trigger commitMount
return !!instance.__r3f.handlers;
},
commitMount(instance)
/*, type, props*/
{
// https://github.com/facebook/react/issues/20271
// This will make sure events are only added once to the central container
if (instance.raycast && instance.__r3f.handlers) instance.__r3f.root.getState().internal.interaction.push(instance);
},
prepareUpdate() {
return EMPTY;
},
shouldDeprioritizeSubtree() {
return false;
},
prepareForCommit() {
return null;
},
preparePortalMount(...args) {// noop
},
resetAfterCommit() {// noop
},
shouldSetTextContent() {
return false;
},
clearContainer() {
return false;
}
});
return {
reconciler,
applyProps
};
}
const isRenderer = def => def && !!def.render;
const isOrthographicCamera = def => def && def.isOrthographicCamera;
const context = /*#__PURE__*/React.createContext(null);
const createStore = (applyProps, invalidate, advance, props) => {
const {
gl,
size,
shadows = false,
linear = false,
flat = false,
vr = false,
orthographic = false,
frameloop = 'always',
dpr = 1,
performance,
clock = new THREE.Clock(),
raycaster: raycastOptions,
camera: cameraOptions,
onPointerMissed
} = props; // Set shadowmap
if (shadows) {
gl.shadowMap.enabled = true;
if (typeof shadows === 'object') Object.assign(gl.shadowMap, shadows);else gl.shadowMap.type = THREE.PCFSoftShadowMap;
} // Set color management
if (!linear) {
if (!flat) gl.toneMapping = THREE.ACESFilmicToneMapping;
gl.outputEncoding = THREE.sRGBEncoding;
} // clock.elapsedTime is updated using advance(timestamp)
if (frameloop === 'never') {
clock.stop();
clock.elapsedTime = 0;
}
const rootState = create((set, get) => {
// Create custom raycaster
const raycaster = new THREE.Raycaster();
const {
params,
...options
} = raycastOptions || {};
applyProps(raycaster, {
enabled: true,
...options,
params: { ...raycaster.params,
...params
}
}, {}); // Create default camera
const isCamera = cameraOptions instanceof THREE.Camera;
const camera = isCamera ? cameraOptions : orthographic ? new THREE.OrthographicCamera(0, 0, 0, 0, 0.1, 1000) : new THREE.PerspectiveCamera(75, 0, 0.1, 1000);
if (!isCamera) {
camera.position.z = 5;
if (cameraOptions) applyProps(camera, cameraOptions, {}); // Always look at center by default
camera.lookAt(0, 0, 0);
}
function setDpr(dpr) {
return Array.isArray(dpr) ? Math.min(Math.max(dpr[0], window.devicePixelRatio), dpr[1]) : dpr;
}
const initialDpr = setDpr(dpr);
const position = new THREE.Vector3();
const defaultTarget = new THREE.Vector3();
function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
const {
width,
height
} = size;
const aspect = width / height;
const distance = camera.getWorldPosition(position).distanceTo(target);
if (isOrthographicCamera(camera)) {
return {
width: width / camera.zoom,
height: height / camera.zoom,
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,
factor: width / w,
distance,
aspect
};
}
}
let performanceTimeout = undefined;
const setPerformanceCurrent = current => set(state => ({
performance: { ...state.performance,
current
}
}));
return {
gl,
set,
get,
invalidate: () => invalidate(get()),
advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
linear,
flat,
scene: prepare(new THREE.Scene()),
camera,
controls: null,
raycaster,
clock,
mouse: new THREE.Vector2(),
vr,
frameloop,
onPointerMissed,
performance: {
current: 1,
min: 0.5,
max: 1,
debounce: 200,
...performance,
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
},
viewport: {
initialDpr,
dpr: initialDpr,
width: 0,
height: 0,
aspect: 0,
distance: 0,
factor: 0,
getCurrentViewport
},
setSize: (width, height) => {
const size = {
width,
height
};
set(state => ({
size,
viewport: { ...state.viewport,
...getCurrentViewport(camera, defaultTarget, size)
}
}));
},
setDpr: dpr => set(state => ({
viewport: { ...state.viewport,
dpr: setDpr(dpr)
}
})),
events: {
connected: false
},
internal: {
active: false,
priority: 0,
frames: 0,
lastProps: props,
interaction: [],
hovered: new Map(),
subscribers: [],
initialClick: [0, 0],
initialHits: [],
capturedMap: new Map(),
subscribe: (ref, priority = 0) => {
set(({
internal
}) => ({
internal: { ...internal,
// If this subscription was given a priority, it takes rendering into its own hands
// For that reason we switch off automatic rendering and increase the manual flag
// As long as this flag is positive (there could be multiple render subscription)
// ..there can be no internal rendering at all
priority: internal.priority + (priority > 0 ? 1 : 0),
// Register subscriber and sort layers from lowest to highest, meaning,
// highest priority renders last (on top of the other frames)
subscribers: [...internal.subscribers, {
ref,
priority
}].sort((a, b) => a.priority - b.priority)
}
}));
return () => {
set(({
internal
}) => ({
internal: { ...internal,
// Decrease manual flag if this subscription had a priority
priority: internal.priority - (priority > 0 ? 1 : 0),
// Remove subscriber from list
subscribers: internal.subscribers.filter(s => s.ref !== ref)
}
}));
};
}
}
};
}); // Resize camera and renderer on changes to size and pixelratio
rootState.subscribe(() => {
const {
camera,
size,
viewport,
internal
} = rootState.getState(); // https://github.com/pmndrs/react-three-fiber/issues/92
// Do not mess with the camera if it belongs to the user
if (!(internal.lastProps.camera instanceof THREE.Camera)) {
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();
} // Update renderer
gl.setPixelRatio(viewport.dpr);
gl.setSize(size.width, size.height);
}, state => [state.viewport.dpr, state.size], shallow);
const state = rootState.getState(); // Update size
if (size) state.setSize(size.width, size.height); // Invalidate on any change
rootState.subscribe(state => invalidate(state)); // Return root state
return rootState;
};
function createSubs(callback, subs) {
const index = subs.length;
subs.push(callback);
return () => void subs.splice(index, 1);
}
let i;
let globalEffects = [];
let globalAfterEffects = [];
let globalTailEffects = [];
const addEffect = callback => createSubs(callback, globalEffects);
const addAfterEffect = callback => createSubs(callback, globalAfterEffects);
const addTail = callback => createSubs(callback, globalTailEffects);
function run(effects, timestamp) {
for (i = 0; i < effects.length; i++) effects[i](timestamp);
}
function render$1(timestamp, state) {
// Run local effects
let delta = state.clock.getDelta(); // In frameloop='never' mode, clock times are updated using the provided timestamp
if (state.frameloop === 'never' && typeof timestamp === 'number') {
delta = timestamp - state.clock.elapsedTime;
state.clock.oldTime = state.clock.elapsedTime;
state.clock.elapsedTime = timestamp;
} // Call subscribers (useFrame)
for (i = 0; i < state.internal.subscribers.length; i++) state.internal.subscribers[i].ref.current(state, delta); // Render content
if (!state.internal.priority && state.gl.render) state.gl.render(state.scene, state.camera); // Decrease frame count
state.internal.frames = Math.max(0, state.internal.frames - 1);
return state.frameloop === 'always' ? 1 : state.internal.frames;
}
function createLoop(roots) {
let running = false;
let repeat;
function loop(timestamp) {
running = true;
repeat = 0; // Run effects
run(globalEffects, timestamp); // Render all roots
roots.forEach(root => {
const state = root.store.getState(); // If the frameloop is invalidated, do not run another frame
if (state.internal.active && (state.frameloop === 'always' || state.internal.frames > 0)) repeat += render$1(timestamp, state);
}); // Run after-effects
run(globalAfterEffects, timestamp); // Keep on looping if anything invalidates the frameloop
if (repeat > 0) return requestAnimationFrame(loop); // Tail call effects, they are called when rendering stops
else run(globalTailEffects, timestamp); // Flag end of operation
running = false;
}
function invalidate(state) {
if (!state) return roots.forEach(root => invalidate(root.store.getState()));
if (state.vr || !state.internal.active || state.frameloop === 'never') return; // Increase frames, do not go higher than 60
state.internal.frames = Math.min(60, state.internal.frames + 1); // If the render-loop isn't active, start it
if (!running) {
running = true;
requestAnimationFrame(loop);
}
}
function advance(timestamp, runGlobalEffects = true, state) {
if (runGlobalEffects) run(globalEffects, timestamp);
if (!state) roots.forEach(root => render$1(timestamp, root.store.getState()));else render$1(timestamp, state);
if (runGlobalEffects) run(globalAfterEffects, timestamp);
}
return {
loop,
invalidate,
advance
};
}
function createPointerEvents(store) {
const {
handlePointer
} = createEvents(store);
const names = {
onClick: ['click', false],
onContextMenu: ['contextmenu', false],
onDoubleClick: ['dblclick', false],
onWheel: ['wheel', true],
onPointerDown: ['pointerdown', true],
onPointerUp: ['pointerup', true],
onPointerLeave: ['pointerleave', true],
onPointerMove: ['pointermove', true],
onPointerCancel: ['pointercancel', true],
onLostPointerCapture: ['lostpointercapture', true]
};
return {
connected: false,
handlers: Object.keys(names).reduce((acc, key) => ({ ...acc,
[key]: handlePointer(key)
}), {}),
connect: target => {
var _events$handlers;
const {
set,
events
} = store.getState();
events.disconnect == null ? void 0 : events.disconnect();
set(state => ({
events: { ...state.events,
connected: target
}
}));
Object.entries((_events$handlers = events == null ? void 0 : events.handlers) != null ? _events$handlers : []).forEach(([name, event]) => {
const [eventName, passive] = names[name];
target.addEventListener(eventName, event, {
passive
});
});
},
disconnect: () => {
const {
set,
events
} = store.getState();
if (events.connected) {
var _events$handlers2;
Object.entries((_events$handlers2 = events.handlers) != null ? _events$handlers2 : []).forEach(([name, event]) => {
if (events && events.connected instanceof HTMLElement) {
const [eventName] = names[name];
events.connected.removeEventListener(eventName, event);
}
});
set(state => ({
events: { ...state.events,
connected: false
}
}));
}
}
};
}
// 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 in the browser.
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
function Block({
set
}) {
useIsomorphicLayoutEffect(() =>