UNPKG

@react-three/uikit

Version:

Build performant 3D user interfaces with react-three-fiber and yoga.

193 lines (192 loc) 8.54 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { Signal, computed, effect } from '@preact/signals-core'; import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import { HalfFloatType, LinearFilter, Scene, WebGLRenderTarget, PerspectiveCamera, Raycaster, Vector2, Vector3, } from 'three'; import { Image } from './image.js'; import { reconciler, useFrame, useStore, context } from '@react-three/fiber'; import { create } from 'zustand'; // Keys that shouldn't be copied between R3F stores export const privateKeys = [ 'set', 'get', 'setSize', 'setFrameloop', 'setDpr', 'events', 'invalidate', 'advance', 'size', 'viewport', ]; const isOrthographicCamera = (def) => def && def.isOrthographicCamera; export const Portal = forwardRef(({ children, dpr, frames = Infinity, renderPriority = 0, eventPriority = 0, ...props }, ref) => { const fbo = useMemo(() => new Signal(undefined), []); const imageRef = useRef(null); const previousRoot = useStore(); dpr ??= previousRoot.getState().viewport.dpr; useImperativeHandle(ref, () => imageRef.current, []); const texture = useMemo(() => computed(() => fbo.value?.texture), [fbo]); const usePortalStore = useMemo(() => { let previousState = previousRoot.getState(); // We have our own camera in here, separate from the main scene. const camera = new PerspectiveCamera(50, 1, 0.1, 1000); camera.position.set(0, 0, 5); const pointer = new Vector2(); let ownState = { events: { compute: uvCompute.bind(null, imageRef), priority: eventPriority }, size: { width: 1, height: 1, left: 0, top: 0 }, camera, scene: new Scene(), raycaster: new Raycaster(), pointer: pointer, mouse: pointer, previousRoot, }; //we now merge in order previousState, injectState, ownState const store = create((innerSet, get) => { const merge = () => { const result = {}; for (const key in previousState) { if (privateKeys.includes(key)) { continue; } result[key] = previousState[key]; } return Object.assign(result, ownState, { events: { ...previousState.events, ...ownState.events }, viewport: Object.assign({}, previousState.viewport, previousState.viewport.getCurrentViewport(camera, new Vector3(), ownState.size)), }); }; const update = () => innerSet(merge()); return { ...previousState, // Set and get refer to this root-state set(newOwnState) { if (typeof newOwnState === 'function') { newOwnState = newOwnState(get()); } Object.assign(ownState, newOwnState); update(); }, setPreviousState(prevState) { previousState = prevState; update(); }, get, // Layers are allowed to override events setEvents(events) { Object.assign(ownState.events, events); update(); }, ...merge(), }; }); return Object.assign(store, { setState(state) { store.getState().set(state); }, }); }, [eventPriority, previousRoot]); //syncing up previous store with the current store useEffect(() => previousRoot.subscribe(usePortalStore.getState().setPreviousState), [previousRoot, usePortalStore]); useEffect(() => { if (imageRef.current == null) { return; } const renderTarget = (fbo.value = new WebGLRenderTarget(1, 1, { minFilter: LinearFilter, magFilter: LinearFilter, type: HalfFloatType, })); const { size } = imageRef.current; const unsubscribeSetSize = effect(() => { if (size.value == null) { return; } const [width, height] = size.value; renderTarget.setSize(width * dpr, height * dpr); usePortalStore.setState({ size: { width, height, top: 0, left: 0 }, viewport: { ...previousRoot.getState().viewport, width, height, aspect: width / height }, }); //we invalidate because we need to re-render the image's framebuffer now because it's size was changed usePortalStore.getState().invalidate(); }); return () => { unsubscribeSetSize(); renderTarget.dispose(); }; }, [fbo, previousRoot, usePortalStore, dpr]); return (_jsxs(_Fragment, { children: [reconciler.createPortal(_jsx(context.Provider, { value: usePortalStore, children: _jsxs(ChildrenToFBO, { renderPriority: renderPriority, frames: frames, fbo: fbo, imageRef: imageRef, children: [children, _jsx("group", { onPointerOver: () => null })] }) }), usePortalStore, null), _jsx(Image, { src: texture, objectFit: "fill", keepAspectRatio: false, ...props, ref: imageRef })] })); }); function uvCompute({ current }, event, state, previous) { if (current == null || previous == null) { return false; } // First we call the previous state-onion-layers compute, this is what makes it possible to nest portals if (!previous.raycaster.camera) previous.events.compute?.(event, previous, previous.previousRoot?.getState()); // We run a quick check against the parent, if it isn't hit there's no need to raycast at all const [intersection] = previous.raycaster.intersectObject(current.interactionPanel); if (!intersection) return false; // We take that hits uv coords, set up this layers raycaster, et voilà, we have raycasting on arbitrary surfaces const uv = intersection.uv; if (!uv) return false; state.raycaster.setFromCamera(state.pointer.set(uv.x * 2 - 1, uv.y * 2 - 1), state.camera); } function ChildrenToFBO({ frames, renderPriority, children, fbo, imageRef, }) { const store = useStore(); useEffect(() => { return store.subscribe((state, prevState) => { const { size, camera } = state; if (size) { 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; } if (size !== prevState.size || camera !== prevState.camera) { camera.updateProjectionMatrix(); // https://github.com/pmndrs/react-three-fiber/issues/178 // Update matrix world since the renderer is a frame late camera.updateMatrixWorld(); } } }); }, [store]); let count = 0; let oldAutoClear; let oldXrEnabled; let oldIsPresenting; let oldRenderTarget; useFrame((state) => { const currentFBO = fbo.peek(); //we only render if we have a framebuffer to write to and if the portal is not clipped if (currentFBO == null || imageRef.current?.isVisible?.peek() != true) { return; } if (frames === Infinity || count < frames) { oldAutoClear = state.gl.autoClear; oldXrEnabled = state.gl.xr.enabled; oldIsPresenting = state.gl.xr.isPresenting; oldRenderTarget = state.gl.getRenderTarget(); state.gl.autoClear = true; state.gl.xr.enabled = false; state.gl.xr.isPresenting = false; state.gl.setRenderTarget(currentFBO); state.gl.render(state.scene, state.camera); state.gl.setRenderTarget(oldRenderTarget); state.gl.autoClear = oldAutoClear; state.gl.xr.enabled = oldXrEnabled; state.gl.xr.isPresenting = oldIsPresenting; count++; } }, renderPriority); return _jsx(_Fragment, { children: children }); }