@react-three/uikit
Version:
Build performant 3D user interfaces with react-three-fiber and yoga.
193 lines (192 loc) • 8.54 kB
JavaScript
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 });
}