UNPKG

react-three-fiber

Version:
1,180 lines (982 loc) 43.2 kB
import _extends from '@babel/runtime/helpers/esm/extends'; import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/esm/objectWithoutPropertiesLoose'; import * as THREE from 'three'; import { Layers, Color, Texture, sRGBEncoding, Vector2, Raycaster, Scene, OrthographicCamera, PerspectiveCamera, Clock, Vector3, PCFSoftShadowMap, ACESFilmicToneMapping, WebGLRenderer } from 'three'; import Reconciler from 'react-reconciler'; import { unstable_now, unstable_runWithPriority, unstable_IdlePriority } from 'scheduler'; import React__default, { createContext, useState, useRef, useMemo, useCallback, useLayoutEffect, useEffect, createElement, useContext as useContext$1 } from 'react'; import { TinyEmitter } from 'tiny-emitter'; import usePromise from 'react-promise-suspense'; import useMeasure from 'react-use-measure'; import { ResizeObserver } from '@juggle/resize-observer'; import mergeRefs from 'react-merge-refs'; var version = "4.2.11"; function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const roots = new Map(); const emptyObject = {}; const is = { obj: a => a === Object(a) && !is.arr(a), 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 createSubs(callback, subs) { const index = subs.length; subs.push(callback); return () => void subs.splice(index, 1); } let globalEffects = []; let globalTailEffects = []; const addEffect = callback => createSubs(callback, globalEffects); const addTail = callback => createSubs(callback, globalTailEffects); function renderGl(state, timestamp, repeat = 0, runGlobalEffects = false) { // Run global effects if (runGlobalEffects) globalEffects.forEach(effect => effect(timestamp) && repeat++); // Run local effects const delta = state.current.clock.getDelta(); state.current.subscribers.forEach(sub => sub.ref.current(state.current, delta)); // Decrease frame count state.current.frames = Math.max(0, state.current.frames - 1); repeat += !state.current.invalidateFrameloop ? 1 : state.current.frames; // Render content if (!state.current.manual) state.current.gl.render(state.current.scene, state.current.camera); return repeat; } let running = false; function renderLoop(timestamp) { running = true; let repeat = 0; // Run global effects globalEffects.forEach(effect => effect(timestamp) && repeat++); roots.forEach(root => { const state = root.containerInfo.__state; // If the frameloop is invalidated, do not run another frame if (state.current.active && state.current.ready && (!state.current.invalidateFrameloop || state.current.frames > 0)) repeat = renderGl(state, timestamp, repeat); }); if (repeat !== 0) return requestAnimationFrame(renderLoop);else { // Tail call effects, they are called when rendering stops globalTailEffects.forEach(effect => effect(timestamp)); } // Flag end of operation running = false; } function invalidate(state = true, frames = 2) { if (state === true) roots.forEach(root => root.containerInfo.__state.current.frames = frames);else if (state && state.current) { if (state.current.vr) return; state.current.frames = frames; } if (!running) { running = true; requestAnimationFrame(renderLoop); } } let catalogue = {}; const extend = objects => void (catalogue = _extends({}, catalogue, objects)); function applyProps(instance, newProps, oldProps = {}, accumulative = false) { // Filter equals, events and reserved props const container = instance.__container; const sameProps = Object.keys(newProps).filter(key => is.equ(newProps[key], oldProps[key])); const handlers = Object.keys(newProps).filter(key => { // Event-handlers ... // are functions, that // start with "on", and // contain the name "Pointer", "Click", "ContextMenu", or "Wheel" if (is.fun(newProps[key]) && key.startsWith('on')) { return key.includes('Pointer') || key.includes('Click') || key.includes('ContextMenu') || key.includes('Wheel'); } }); const leftOvers = accumulative ? Object.keys(oldProps).filter(key => newProps[key] === void 0) : []; const filteredProps = [...sameProps, 'children', 'key', 'ref'].reduce((acc, prop) => { let rest = _objectWithoutPropertiesLoose(acc, [prop].map(_toPropertyKey)); return rest; }, newProps); // Add left-overs as undefined props so they can be removed leftOvers.forEach(key => key !== 'children' && (filteredProps[key] = undefined)); if (Object.keys(filteredProps).length > 0) { Object.entries(filteredProps).forEach(([key, value]) => { if (!handlers.includes(key)) { var _instance$__container; let root = instance; let target = root[key]; if (key.includes('-')) { const entries = key.split('-'); target = entries.reduce((acc, key) => acc[key], instance); // If the target is atomic, it forces us to switch the root if (!(target && target.set)) { const [name, ...reverseEntries] = entries.reverse(); root = reverseEntries.reverse().reduce((acc, key) => acc[key], instance); key = name; } } // Special treatment for objects with support for set/copy const isColorManagement = (_instance$__container = instance.__container) == null ? void 0 : _instance$__container.__state.current.colorManagement; if (target && target.set && (target.copy || target instanceof Layers)) { // If value is an array it has got to be the set function if (Array.isArray(value)) target.set(...value); // Test again target.copy(class) next ... else if (target.copy && value && value.constructor && target.constructor.name === value.constructor.name) target.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) { target.set(value); // Auto-convert sRGB colors, for now ... // https://github.com/react-spring/react-three-fiber/issues/344 if (isColorManagement && target instanceof Color) { target.convertSRGBToLinear(); } } // Else, just overwrite the value } else { root[key] = value; // Auto-convert sRGB textures, for now ... // https://github.com/react-spring/react-three-fiber/issues/344 if (isColorManagement && root[key] instanceof Texture) { root[key].encoding = sRGBEncoding; } } invalidateInstance(instance); } }); // Preemptively delete the instance from the containers interaction if (accumulative && container && instance.raycast && instance.__handlers) { instance.__handlers = undefined; const index = container.__interaction.indexOf(instance); if (index > -1) container.__interaction.splice(index, 1); } // Prep interaction handlers if (handlers.length) { // Add interactive object to central container if (container && instance.raycast) container.__interaction.push(instance); // Add handlers to the instances handler-map instance.__handlers = handlers.reduce((acc, key) => _extends({}, acc, { [key.charAt(2).toLowerCase() + key.substr(3)]: 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) { if (instance.__container && instance.__container.__state) invalidate(instance.__container.__state); } function updateInstance(instance) { if (instance.onUpdate) instance.onUpdate(instance); } function createInstance(type, _ref, container, hostContext, internalInstanceHandle) { let { args = [] } = _ref, props = _objectWithoutPropertiesLoose(_ref, ["args"]); let name = "" + type[0].toUpperCase() + type.slice(1); let instance; if (type === 'primitive') { instance = props.object; instance.__instance = true; } else if (type === 'new') { instance = new props.object(args); } else { const target = catalogue[name] || THREE[name]; if (!target) { throw "\"" + name + "\" is not part of the THREE namespace! Did you forget to extend it? See: https://github.com/react-spring/react-three-fiber#using-3rd-party-non-three-namespaced-objects-in-the-scene-graph"; } instance = is.arr(args) ? new target(...args) : new target(args); } // Bind to the root container in case portals are being used // This is perhaps better for event management as we can keep them on a single instance while (container.__container) { container = container.__container; } // TODO: https://github.com/facebook/react/issues/17147 // If it's still not there it means the portal was created on a virtual node outside of react if (!roots.has(container)) { const fn = node => { if (!node.return) return node.stateNode && node.stateNode.containerInfo;else return fn(node.return); }; container = fn(internalInstanceHandle); } // Apply initial props instance.__objects = []; instance.__container = container; // 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 "false" here applyProps(instance, props, {}); return instance; } function appendChild(parentInstance, child) { if (child) { if (child.isObject3D) parentInstance.add(child);else { parentInstance.__objects.push(child); child.parent = parentInstance; // The attach attribute implies that the object attaches itself on the parent if (child.attach) parentInstance[child.attach] = child;else 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; } } updateInstance(child); invalidateInstance(child); } } function insertBefore(parentInstance, child, beforeChild) { if (child) { if (child.isObject3D) { child.parent = parentInstance; child.dispatchEvent({ type: 'added' }); const restSiblings = parentInstance.children.filter(sibling => sibling !== child); // TODO: the order is out of whack if data objects are present, has to be recalculated const index = restSiblings.indexOf(beforeChild); parentInstance.children = [...restSiblings.slice(0, index), child, ...restSiblings.slice(index)]; updateInstance(child); } else appendChild(parentInstance, child); // TODO: order!!! invalidateInstance(child); } } 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 removeChild(parentInstance, child) { if (child) { if (child.isObject3D) { parentInstance.remove(child); } else { child.parent = null; if (parentInstance.__objects) parentInstance.__objects = parentInstance.__objects.filter(x => x !== child); // Remove attachment if (child.attach) parentInstance[child.attach] = null;else 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]]; } } invalidateInstance(child); // Allow objects to bail out of recursive dispose alltogether by passing dispose={null} if (child.dispose !== null) { unstable_runWithPriority(unstable_IdlePriority, () => { // Remove interactivity if (child.__container) child.__container.__interaction = child.__container.__interaction.filter(x => x !== child); // Remove nested child objects removeRecursive(child.__objects, child); removeRecursive(child.children, child, true); // Dispose item if (child.dispose) child.dispose(); // Remove references delete child.__container; delete child.__objects; }); } } } function switchInstance(instance, type, newProps, fiber) { const parent = instance.parent; const newInstance = createInstance(type, newProps, instance.__container, null, fiber); 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 (is.fun(fiber.ref)) fiber.ref(newInstance);else fiber.ref.current = newInstance; } } }); } const Renderer = Reconciler({ now: unstable_now, createInstance, removeChild, appendChild, insertBefore, // @ts-ignore warnsIfNotActing: true, supportsMutation: true, isPrimaryRenderer: false, scheduleTimeout: is.fun(setTimeout) ? setTimeout : undefined, cancelTimeout: is.fun(clearTimeout) ? clearTimeout : undefined, // @ts-ignore setTimeout: is.fun(setTimeout) ? setTimeout : undefined, // @ts-ignore clearTimeout: is.fun(clearTimeout) ? clearTimeout : undefined, noTimeout: -1, appendInitialChild: appendChild, appendChildToContainer: appendChild, removeChildFromContainer: removeChild, insertInContainerBefore: insertBefore, commitUpdate(instance, updatePayload, type, oldProps, newProps, fiber) { if (instance.__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 = [] } = newProps, restNew = _objectWithoutPropertiesLoose(newProps, ["args"]); const { args: argsOld = [] } = oldProps, restOld = _objectWithoutPropertiesLoose(oldProps, ["args"]); // 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 react-three-fibre tree. You may have extraneous whitespace between components.'); }, getPublicInstance(instance) { return instance; }, getRootHostContext() { return emptyObject; }, getChildHostContext() { return emptyObject; }, createTextInstance() {}, finalizeInitialChildren() { return false; }, prepareUpdate() { return emptyObject; }, shouldDeprioritizeSubtree() { return false; }, prepareForCommit() {}, resetAfterCommit() {}, shouldSetTextContent() { return false; } }); const LegacyRoot = 0; const ConcurrentRoot = 2; const hasSymbol = is.fun(Symbol) && Symbol.for; const REACT_PORTAL_TYPE = hasSymbol ? Symbol.for('react.portal') : 0xeaca; function render(element, container, state) { let root = roots.get(container); if (!root) { container.__state = state; let newRoot = root = Renderer.createContainer(container, state !== undefined && state.current.concurrent ? ConcurrentRoot : LegacyRoot, false); roots.set(container, newRoot); } Renderer.updateContainer(element, root, null, () => undefined); return Renderer.getPublicRootInstance(root); } function unmountComponentAtNode(container) { const root = roots.get(container); if (root) Renderer.updateContainer(null, root, null, () => void roots.delete(container)); } function createPortal(children, containerInfo, implementation, key = null) { if (!containerInfo.__objects) containerInfo.__objects = []; return { $$typeof: REACT_PORTAL_TYPE, key: key == null ? null : '' + key, children, containerInfo, implementation }; } Renderer.injectIntoDevTools({ bundleType: process.env.NODE_ENV === 'production' ? 0 : 1, version: version, rendererPackageName: 'react-three-fiber', findHostInstanceByFiber: Renderer.findHostInstance }); function isOrthographicCamera(def) { return def.isOrthographicCamera; } function makeId(event) { return (event.eventObject || event.object).uuid + '/' + event.index; } const stateContext = createContext({}); const useCanvas = props => { const { children, gl, camera, orthographic, raycaster, size, pixelRatio, vr = false, concurrent = false, shadowMap = false, sRGB = false, colorManagement = false, invalidateFrameloop = false, updateDefaultCamera = true, noEvents = false, onCreated, onPointerMissed } = props; // Local, reactive state const [ready, setReady] = useState(false); const [mouse] = useState(() => new Vector2()); const [defaultRaycaster] = useState(() => { const ray = new Raycaster(); if (raycaster) { const raycasterProps = _objectWithoutPropertiesLoose(raycaster, ["filter"]); applyProps(ray, raycasterProps, {}); } return ray; }); const [defaultScene] = useState(() => { const scene = new Scene(); scene.__interaction = []; scene.__objects = []; return scene; }); const [defaultCam, _setDefaultCamera] = useState(() => { const cam = orthographic ? new OrthographicCamera(0, 0, 0, 0, 0.1, 1000) : new PerspectiveCamera(75, 0, 0.1, 1000); cam.position.z = 5; if (camera) applyProps(cam, camera, {}); // Always look at [0, 0, 0] cam.lookAt(0, 0, 0); return cam; }); const [clock] = useState(() => new Clock()); // Public state const state = useRef({ ready: false, active: true, manual: 0, colorManagement, sRGB, vr, concurrent, noEvents, invalidateFrameloop: false, frames: 0, aspect: 0, subscribers: [], camera: defaultCam, scene: defaultScene, raycaster: defaultRaycaster, mouse, clock, gl, size, viewport: { width: 0, height: 0, factor: 0 }, initialClick: [0, 0], initialHits: [], pointer: new TinyEmitter(), captured: undefined, events: undefined, subscribe: (ref, priority = 0) => { // 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 if (priority) state.current.manual++; state.current.subscribers.push({ ref, priority: priority }); // Sort layers from lowest to highest, meaning, highest priority renders last (on top of the other frames) state.current.subscribers = state.current.subscribers.sort((a, b) => a.priority - b.priority); return () => { // Decrease manual flag if this subscription had a priority if (priority) state.current.manual--; state.current.subscribers = state.current.subscribers.filter(s => s.ref !== ref); }; }, setDefaultCamera: camera => _setDefaultCamera(camera), invalidate: () => invalidate(state), intersect: (event = {}, prepare = true) => handlePointerMove(event, prepare) }); // Writes locals into public state for distribution among subscribers, context, etc useMemo(() => { state.current.ready = ready; state.current.size = size; state.current.camera = defaultCam; state.current.invalidateFrameloop = invalidateFrameloop; state.current.vr = vr; state.current.gl = gl; state.current.concurrent = concurrent; state.current.noEvents = noEvents; }, [invalidateFrameloop, vr, concurrent, noEvents, ready, size, defaultCam, gl]); // Adjusts default camera useMemo(() => { state.current.aspect = size.width / size.height; if (isOrthographicCamera(defaultCam)) { state.current.viewport = { width: size.width, height: size.height, factor: 1 }; } else { const target = new Vector3(0, 0, 0); const distance = defaultCam.position.distanceTo(target); const fov = defaultCam.fov * Math.PI / 180; // convert vertical fov to radians const height = 2 * Math.tan(fov / 2) * distance; // visible height const width = height * state.current.aspect; state.current.viewport = { width, height, factor: size.width / width }; } // #92 (https://github.com/drcmda/react-three-fiber/issues/92) // Sometimes automatic default camera adjustment isn't wanted behaviour if (updateDefaultCamera) { if (isOrthographicCamera(defaultCam)) { defaultCam.left = size.width / -2; defaultCam.right = size.width / 2; defaultCam.top = size.height / 2; defaultCam.bottom = size.height / -2; } else { defaultCam.aspect = state.current.aspect; } defaultCam.updateProjectionMatrix(); // #178: https://github.com/react-spring/react-three-fiber/issues/178 // Update matrix world since the renderer is a frame late defaultCam.updateMatrixWorld(); } gl.setSize(size.width, size.height); if (ready) invalidate(state); }, [defaultCam, size, updateDefaultCamera]); /** Events ------------------------------------------------------------------------------------------------ */ /** Sets up defaultRaycaster */ const prepareRay = useCallback(({ clientX, clientY }) => { if (clientX !== void 0) { const { left, right, top, bottom } = state.current.size; mouse.set((clientX - left) / (right - left) * 2 - 1, -((clientY - top) / (bottom - top)) * 2 + 1); defaultRaycaster.setFromCamera(mouse, state.current.camera); } }, []); /** Intersects interaction objects using the event input */ const intersect = useCallback((event, filter) => { // Skip event handling when noEvents is set if (state.current.noEvents) return []; const seen = new Set(); const hits = []; // Allow callers to eliminate event objects const eventsObjects = filter ? filter(state.current.scene.__interaction) : state.current.scene.__interaction; // Intersect known handler objects and filter against duplicates let intersects = defaultRaycaster.intersectObjects(eventsObjects, true).filter(item => { const id = makeId(item); if (seen.has(id)) return false; seen.add(id); return true; }); // #16031: (https://github.com/mrdoob/three.js/issues/16031) // Allow custom userland intersect sort order if (raycaster && raycaster.filter && sharedState.current) intersects = raycaster.filter(intersects, sharedState.current); for (let intersect of intersects) { let eventObject = intersect.object; // Bubble event up while (eventObject) { const handlers = eventObject.__handlers; if (handlers) hits.push(_extends({}, intersect, { eventObject })); eventObject = eventObject.parent; } } return hits; }, []); /** Calculates click deltas */ const calculateDistance = useCallback(event => { let dx = event.clientX - state.current.initialClick[0]; let dy = event.clientY - state.current.initialClick[1]; return Math.round(Math.sqrt(dx * dx + dy * dy)); }, []); const hovered = useMemo(() => new Map(), []); /** Handles intersections by forwarding them to handlers */ const temp = new Vector3(); const handleIntersects = useCallback((event, fn, filter) => { // Get fresh intersects let intersections = intersect(event, filter); // If the interaction is captured take that into account, the captured event has to be part of the intersects if (state.current.captured && event.type !== 'click' && event.type !== 'wheel') { state.current.captured.forEach(captured => { if (!intersections.find(hit => hit.eventObject === captured.eventObject)) intersections.push(captured); }); } // If anything has been found, forward it to the event listeners if (intersections.length) { const unprojectedPoint = temp.set(mouse.x, mouse.y, 0).unproject(state.current.camera); const delta = event.type === 'click' ? calculateDistance(event) : 0; const releasePointerCapture = id => event.target.releasePointerCapture(id); const localState = { stopped: false, captured: false }; for (let 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 state.current.captured = []; } // Push hits to the array if (state.current.captured) state.current.captured.push(hit) // Call the original event now ; event.target.setPointerCapture(id); }; let raycastEvent = _extends({}, event, hit, { intersections, stopped: localState.stopped, delta, unprojectedPoint, ray: defaultRaycaster.ray, camera: state.current.camera, // Hijack stopPropagation, which just sets a flag stopPropagation: () => raycastEvent.stopped = localState.stopped = true, // Pointer-capture needs the hit, on which the user may call stopPropagation() // This makes it harder to use the actual event, because then we loose the connection // to the actual hit, which would mean it's picking up all intersects ... target: _extends({}, event.target, { setPointerCapture, releasePointerCapture }), currentTarget: _extends({}, event.currentTarget, { setPointerCapture, releasePointerCapture }), sourceEvent: event }); fn(raycastEvent); if (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.object === hit.object)) { handlePointerCancel(raycastEvent, [hit]); } break; } } } return intersections; }, []); const handlePointerMove = useCallback((event, prepare = true) => { state.current.pointer.emit('pointerMove', event); if (prepare) prepareRay(event); const hits = handleIntersects(event, data => { const eventObject = data.eventObject; const handlers = eventObject.__handlers; // Check presence of handlers if (!handlers) return; // Call mouse move if (handlers.pointerMove) handlers.pointerMove(data); // Check if mouse enter or out is present if (handlers.pointerOver || handlers.pointerEnter || handlers.pointerOut || handlers.pointerLeave) { 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); if (handlers.pointerOver) handlers.pointerOver(_extends({}, data, { type: 'pointerover' })); if (handlers.pointerEnter) handlers.pointerEnter(_extends({}, data, { type: 'pointerenter' })); } else if (hoveredItem.stopped) { // If the object was previously hovered and stopped, we shouldn't allow other items to proceed data.stopPropagation(); } } }, // This is onPointerMove, we're only interested in events that exhibit this particular event objects => objects.filter(obj => ['Move', 'Over', 'Enter', 'Out', 'Leave'].some(name => obj.__handlers['pointer' + name]))); // Take care of unhover handlePointerCancel(event, hits, prepare); return hits; }, []); const handlePointerCancel = useCallback((event, hits, prepare = true) => { state.current.pointer.emit('pointerCancel', event); if (prepare) prepareRay(event); if (!hits) hits = handleIntersects(event, () => null); Array.from(hovered.values()).forEach(data => { // 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 && (!hits.length || !hits.find(i => i.eventObject === data.eventObject))) { const eventObject = data.eventObject; const handlers = eventObject.__handlers; if (handlers) { if (handlers.pointerOut) handlers.pointerOut(_extends({}, data, { type: 'pointerout' })); if (handlers.pointerLeave) handlers.pointerLeave(_extends({}, data, { type: 'pointerleave' })); } hovered.delete(makeId(data)); } }); }, []); const handlePointer = useCallback(name => (event, prepare = true) => { state.current.pointer.emit(name, event); if (prepare) prepareRay(event); const hits = handleIntersects(event, data => { const eventObject = data.eventObject; const handlers = eventObject.__handlers; if (handlers && handlers[name]) { // Forward all events back to their respective handlers with the exception of click events, // which must use the initial target if (name !== 'click' && name !== 'contextMenu' && name !== 'doubleClick' || state.current.initialHits.includes(eventObject)) { handlers[name](data); } } }); // If a click yields no results, pass it back to the user as a miss if (name === 'pointerDown') { state.current.initialClick = [event.clientX, event.clientY]; state.current.initialHits = hits.map(hit => hit.eventObject); } if ((name === 'click' || name === 'contextMenu' || name === 'doubleClick') && !hits.length && onPointerMissed) { if (calculateDistance(event) <= 2) onPointerMissed(); } }, [onPointerMissed]); useMemo(() => { state.current.events = { onClick: handlePointer('click'), onContextMenu: handlePointer('contextMenu'), onDoubleClick: handlePointer('doubleClick'), onWheel: handlePointer('wheel'), onPointerDown: handlePointer('pointerDown'), onPointerUp: handlePointer('pointerUp'), onPointerLeave: e => handlePointerCancel(e, []), onPointerMove: handlePointerMove, // onGotPointerCapture is not needed any longer because the behaviour is hacked into // the event itself (see handleIntersects). But in order for non-web targets to simulate // it we keep the legacy event, which simply flags all current intersects as captured onGotPointerCaptureLegacy: e => state.current.captured = intersect(e), onLostPointerCapture: e => (state.current.captured = undefined, handlePointerCancel(e)) }; }, [onPointerMissed]); /** Events ------------------------------------------------------------------------------------------------- */ // Only trigger the context provider when necessary const sharedState = useRef(); useMemo(() => { const _state$current = state.current, props = _objectWithoutPropertiesLoose(_state$current, ["ready", "manual", "vr", "noEvents", "invalidateFrameloop", "frames", "subscribers", "captured", "initialClick", "initialHits"]); sharedState.current = props; }, [size, defaultCam]); // Update pixel ratio useLayoutEffect(() => void (pixelRatio && gl.setPixelRatio(pixelRatio)), [pixelRatio]); // Update shadow map useLayoutEffect(() => { if (shadowMap) { gl.shadowMap.enabled = true; if (typeof shadowMap === 'object') Object.assign(gl, shadowMap);else gl.shadowMap.type = PCFSoftShadowMap; } if (sRGB || colorManagement) { gl.toneMapping = ACESFilmicToneMapping; gl.outputEncoding = sRGBEncoding; } }, [shadowMap, sRGB, colorManagement]); // This component is a bridge into the three render context, when it gets rendered // we know we are ready to compile shaders, call subscribers, etc const Canvas = useCallback(function Canvas(props) { const activate = () => setReady(true); // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { const result = onCreated && onCreated(state.current); return void (result && result.then ? result.then(activate) : activate()); }, []); return props.children; }, []); // Render v-dom into scene useLayoutEffect(() => { render( /*#__PURE__*/createElement(Canvas, null, /*#__PURE__*/createElement(stateContext.Provider, { value: sharedState.current }, typeof children === 'function' ? children(state.current) : children)), defaultScene, state); }, [ready, children, sharedState.current]); useLayoutEffect(() => { if (ready) { // Start render-loop, either via RAF or setAnimationLoop for VR if (!state.current.vr) { invalidate(state); } else if ((gl.xr || gl.vr) && gl.setAnimationLoop) { (gl.xr || gl.vr).enabled = true; gl.setAnimationLoop(t => renderGl(state, t, 0, true)); } else console.warn('the gl instance does not support VR!'); } }, [ready, invalidateFrameloop]); // Dispose renderer on unmount useEffect(() => () => { if (state.current.gl) { if (state.current.gl.forceContextLoss) state.current.gl.forceContextLoss(); if (state.current.gl.dispose) state.current.gl.dispose(); state.current.gl = undefined; unmountComponentAtNode(state.current.scene); state.current.active = false; } }, []); return state.current.events; }; function useContext(context) { let result = useContext$1(context); if (!result) { console.warn('hooks can only be used within the canvas! https://github.com/react-spring/react-three-fiber#hooks'); } return result; } function useFrame(callback, renderPriority = 0) { const { subscribe } = useContext(stateContext); // Update ref const ref = useRef(callback); useLayoutEffect(() => void (ref.current = callback), [callback]); // Subscribe/unsub useEffect(() => { const unsubscribe = subscribe(ref, renderPriority); return () => unsubscribe(); }, [renderPriority]); return null; } function useThree() { return useContext(stateContext); } function useUpdate(callback, dependents, optionalRef) { const { invalidate } = useContext(stateContext); const localRef = useRef(); const ref = optionalRef ? optionalRef : localRef; useLayoutEffect(() => { if (ref.current) { callback(ref.current); invalidate(); } }, dependents); return ref; } function useResource(optionalRef) { const [_, forceUpdate] = useState(false); const localRef = useRef(undefined); const ref = optionalRef ? optionalRef : localRef; useLayoutEffect(() => void forceUpdate(i => !i), [ref.current]); return [ref, ref.current]; } const blackList = ['id', 'uuid', 'type', 'children', 'parent', 'matrix', 'matrixWorld', 'matrixWorldNeedsUpdate', 'modelViewMatrix', 'normalMatrix']; function prune(props) { const reducedProps = _extends({}, props); // Remove black listed props blackList.forEach(name => delete reducedProps[name]); // Remove functions Object.keys(reducedProps).forEach(name => typeof reducedProps[name] === 'function' && delete reducedProps[name]); // Prune materials and geometries if (reducedProps.material) reducedProps.material = prune(reducedProps.material); if (reducedProps.geometry) reducedProps.geometry = prune(reducedProps.geometry); // Return cleansed object return reducedProps; } function useLoader(Proto, url, extensions) { const loader = useMemo(() => { // Construct new loader const temp = new Proto(); // Run loader extensions if (extensions) extensions(temp); return temp; }, [Proto]); // Use suspense to load async assets const results = usePromise((Proto, url) => { const urlArray = Array.isArray(url) ? url : [url]; return Promise.all(urlArray.map(url => new Promise(res => loader.load(url, data => { if (data.scene) { // This has to be deprecated at some point! data.__$ = []; // Nodes and materials are better data.nodes = {}; data.materials = {}; data.scene.traverse(obj => { data.__$.push(prune(obj)); if (obj.name) data.nodes = _extends({}, data.nodes, { [obj.name]: obj }); if (obj.material && !data.materials[obj.material.name]) data.materials[obj.material.name] = obj.material; }); } res(data); })))); }, [Proto, url]); // Return the object/s return Array.isArray(url) ? results : results[0]; } const useCamera = () => { console.warn("The useCamera hook was moved to: https://github.com/react-spring/drei"); return null; }; const defaultStyles = { position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }; function Content(_ref) { let { children, setEvents, container, renderer, effects } = _ref, props = _objectWithoutPropertiesLoose(_ref, ["children", "setEvents", "container", "renderer", "effects"]); // Create renderer const [gl] = useState(renderer); if (!gl) console.warn('No renderer created!'); // Mount and unmount managemenbt useEffect(() => effects && effects(gl, container), []); // Init canvas, fetch events, hand them back to the wrapping div const events = useCanvas(_extends({}, props, { children, gl: gl })); useEffect(() => void setEvents(events), [events]); return null; } const ResizeContainer = /*#__PURE__*/React__default.memo(function ResizeContainer(props) { const { preRender, resize, style } = props, restSpread = _objectWithoutPropertiesLoose(props, ["renderer", "effects", "children", "vr", "gl2", "concurrent", "shadowMap", "sRGB", "colorManagement", "orthographic", "invalidateFrameloop", "updateDefaultCamera", "noEvents", "gl", "camera", "raycaster", "pixelRatio", "onCreated", "onPointerMissed", "preRender", "resize", "style"]); const containerRef = useRef(); // onGotPointerCaptureLegacy is a fake event used by non-web targets to simulate poinzter capture const [_ref2, setEvents] = useState({}); const events = _objectWithoutPropertiesLoose(_ref2, ["onGotPointerCaptureLegacy"]); const [bind, size] = useMeasure(resize || { scroll: true, debounce: { scroll: 50, resize: 0 }, polyfill: typeof window === 'undefined' || !window.ResizeObserver ? ResizeObserver : undefined }); // Flag view ready once it's been measured out const readyFlag = useRef(false); const ready = useMemo(() => readyFlag.current = readyFlag.current || !!size.width && !!size.height, [size]); const state = useMemo(() => ({ size, setEvents, container: containerRef.current }), [size]); // Allow Gatsby, Next and other server side apps to run. Will output styles to reduce flickering. if (typeof window === 'undefined') return /*#__PURE__*/React__default.createElement("div", _extends({ style: _extends({}, defaultStyles, style) }, restSpread), preRender); // Render the canvas into the dom return /*#__PURE__*/React__default.createElement("div", _extends({ ref: mergeRefs([bind, containerRef]), style: _extends({}, defaultStyles, style) }, events, restSpread), preRender, ready && /*#__PURE__*/React__default.createElement(Content, _extends({}, props, state))); }); const Canvas = /*#__PURE__*/React__default.memo(function Canvas(_ref) { let { children } = _ref, props = _objectWithoutPropertiesLoose(_ref, ["children"]); const canvasRef = useRef(); return /*#__PURE__*/React__default.createElement(ResizeContainer, _extends({}, props, { renderer: () => { if (canvasRef.current) { const params = _extends({ antialias: true, alpha: true }, props.gl); const temp = new WebGLRenderer(_extends({ canvas: canvasRef.current, context: props.gl2 ? canvasRef.current.getContext('webgl2', params) : undefined }, params)); return temp; } }, preRender: /*#__PURE__*/React__default.createElement("canvas", { ref: canvasRef, style: { display: 'block' } }) }), children); }); const Dom = () => { console.warn("The experimental <Dom> component was renamed to <HTML> and moved to: https://github.com/react-spring/drei"); return null; }; export { Canvas, Dom, Renderer, addEffect, addTail, applyProps, createPortal, extend, invalidate, isOrthographicCamera, render, renderGl, stateContext, unmountComponentAtNode, useCamera, useCanvas, useFrame, useLoader, useResource, useThree, useUpdate };