290 lines
8.7 KiB
JavaScript
290 lines
8.7 KiB
JavaScript
import _extends from '@babel/runtime/helpers/esm/extends';
|
|
import * as React from 'react';
|
|
import * as THREE from 'three';
|
|
import { context, useThree, createPortal, useFrame } from '@react-three/fiber';
|
|
import tunnel from 'tunnel-rat';
|
|
|
|
const isOrthographicCamera = def => def && def.isOrthographicCamera;
|
|
const col = /* @__PURE__ */new THREE.Color();
|
|
const tracked = /* @__PURE__ */tunnel();
|
|
|
|
/**
|
|
* In `@react-three/fiber` after `v8.0.0` but prior to `v8.1.0`, `state.size` contained only dimension
|
|
* information. After `v8.1.0`, position information (`top`, `left`) was added
|
|
*
|
|
* @todo remove this when drei supports v9 and up
|
|
*/
|
|
|
|
function isNonLegacyCanvasSize(size) {
|
|
return 'top' in size;
|
|
}
|
|
function computeContainerPosition(canvasSize, trackRect) {
|
|
const {
|
|
right,
|
|
top,
|
|
left: trackLeft,
|
|
bottom: trackBottom,
|
|
width,
|
|
height
|
|
} = trackRect;
|
|
const isOffscreen = trackRect.bottom < 0 || top > canvasSize.height || right < 0 || trackRect.left > canvasSize.width;
|
|
if (isNonLegacyCanvasSize(canvasSize)) {
|
|
const canvasBottom = canvasSize.top + canvasSize.height;
|
|
const bottom = canvasBottom - trackBottom;
|
|
const left = trackLeft - canvasSize.left;
|
|
return {
|
|
position: {
|
|
width,
|
|
height,
|
|
left,
|
|
top,
|
|
bottom,
|
|
right
|
|
},
|
|
isOffscreen
|
|
};
|
|
}
|
|
// Fall back on old behavior if r3f < 8.1.0
|
|
const bottom = canvasSize.height - trackBottom;
|
|
return {
|
|
position: {
|
|
width,
|
|
height,
|
|
top,
|
|
left: trackLeft,
|
|
bottom,
|
|
right
|
|
},
|
|
isOffscreen
|
|
};
|
|
}
|
|
function prepareSkissor(state, {
|
|
left,
|
|
bottom,
|
|
width,
|
|
height
|
|
}) {
|
|
let autoClear;
|
|
const aspect = width / height;
|
|
if (isOrthographicCamera(state.camera)) {
|
|
if (!state.camera.manual) {
|
|
if (state.camera.left !== width / -2 || state.camera.right !== width / 2 || state.camera.top !== height / 2 || state.camera.bottom !== height / -2) {
|
|
Object.assign(state.camera, {
|
|
left: width / -2,
|
|
right: width / 2,
|
|
top: height / 2,
|
|
bottom: height / -2
|
|
});
|
|
state.camera.updateProjectionMatrix();
|
|
}
|
|
} else {
|
|
state.camera.updateProjectionMatrix();
|
|
}
|
|
} else if (state.camera.aspect !== aspect) {
|
|
state.camera.aspect = aspect;
|
|
state.camera.updateProjectionMatrix();
|
|
}
|
|
autoClear = state.gl.autoClear;
|
|
state.gl.autoClear = false;
|
|
state.gl.setViewport(left, bottom, width, height);
|
|
state.gl.setScissor(left, bottom, width, height);
|
|
state.gl.setScissorTest(true);
|
|
return autoClear;
|
|
}
|
|
function finishSkissor(state, autoClear) {
|
|
// Restore the default state
|
|
state.gl.setScissorTest(false);
|
|
state.gl.autoClear = autoClear;
|
|
}
|
|
function clear(state) {
|
|
state.gl.getClearColor(col);
|
|
state.gl.setClearColor(col, state.gl.getClearAlpha());
|
|
state.gl.clear(true, true);
|
|
}
|
|
function Container({
|
|
visible = true,
|
|
canvasSize,
|
|
scene,
|
|
index,
|
|
children,
|
|
frames,
|
|
rect,
|
|
track
|
|
}) {
|
|
const rootState = useThree();
|
|
const [isOffscreen, setOffscreen] = React.useState(false);
|
|
let frameCount = 0;
|
|
useFrame(state => {
|
|
if (frames === Infinity || frameCount <= frames) {
|
|
var _track$current;
|
|
if (track) rect.current = (_track$current = track.current) == null ? void 0 : _track$current.getBoundingClientRect();
|
|
frameCount++;
|
|
}
|
|
if (rect.current) {
|
|
const {
|
|
position,
|
|
isOffscreen: _isOffscreen
|
|
} = computeContainerPosition(canvasSize, rect.current);
|
|
if (isOffscreen !== _isOffscreen) setOffscreen(_isOffscreen);
|
|
if (visible && !isOffscreen && rect.current) {
|
|
const autoClear = prepareSkissor(state, position);
|
|
// When children are present render the portalled scene, otherwise the default scene
|
|
state.gl.render(children ? state.scene : scene, state.camera);
|
|
finishSkissor(state, autoClear);
|
|
}
|
|
}
|
|
}, index);
|
|
React.useLayoutEffect(() => {
|
|
const curRect = rect.current;
|
|
if (curRect && (!visible || !isOffscreen)) {
|
|
// If the view is not visible clear it once, but stop rendering afterwards!
|
|
const {
|
|
position
|
|
} = computeContainerPosition(canvasSize, curRect);
|
|
const autoClear = prepareSkissor(rootState, position);
|
|
clear(rootState);
|
|
finishSkissor(rootState, autoClear);
|
|
}
|
|
}, [visible, isOffscreen]);
|
|
React.useEffect(() => {
|
|
if (!track) return;
|
|
const curRect = rect.current;
|
|
// Connect the event layer to the tracking element
|
|
const old = rootState.get().events.connected;
|
|
rootState.setEvents({
|
|
connected: track.current
|
|
});
|
|
return () => {
|
|
if (curRect) {
|
|
const {
|
|
position
|
|
} = computeContainerPosition(canvasSize, curRect);
|
|
const autoClear = prepareSkissor(rootState, position);
|
|
clear(rootState);
|
|
finishSkissor(rootState, autoClear);
|
|
}
|
|
rootState.setEvents({
|
|
connected: old
|
|
});
|
|
};
|
|
}, [track]);
|
|
React.useEffect(() => {
|
|
if (isNonLegacyCanvasSize(canvasSize)) return;
|
|
console.warn('Detected @react-three/fiber canvas size does not include position information. <View /> may not work as expected. ' + 'Upgrade to @react-three/fiber ^8.1.0 for support.\n See https://github.com/pmndrs/drei/issues/944');
|
|
}, []);
|
|
return /*#__PURE__*/React.createElement(React.Fragment, null, children, /*#__PURE__*/React.createElement("group", {
|
|
onPointerOver: () => null
|
|
}));
|
|
}
|
|
const CanvasView = /* @__PURE__ */React.forwardRef(({
|
|
track,
|
|
visible = true,
|
|
index = 1,
|
|
id,
|
|
style,
|
|
className,
|
|
frames = Infinity,
|
|
children,
|
|
...props
|
|
}, fref) => {
|
|
var _rect$current, _rect$current2, _rect$current3, _rect$current4;
|
|
const rect = React.useRef(null);
|
|
const {
|
|
size,
|
|
scene
|
|
} = useThree();
|
|
const [virtualScene] = React.useState(() => new THREE.Scene());
|
|
const [ready, toggle] = React.useReducer(() => true, false);
|
|
const compute = React.useCallback((event, state) => {
|
|
if (rect.current && track && track.current && event.target === track.current) {
|
|
const {
|
|
width,
|
|
height,
|
|
left,
|
|
top
|
|
} = rect.current;
|
|
const x = event.clientX - left;
|
|
const y = event.clientY - top;
|
|
state.pointer.set(x / width * 2 - 1, -(y / height) * 2 + 1);
|
|
state.raycaster.setFromCamera(state.pointer, state.camera);
|
|
}
|
|
}, [rect, track]);
|
|
React.useEffect(() => {
|
|
var _track$current2;
|
|
// We need the tracking elements bounds beforehand in order to inject it into the portal
|
|
if (track) rect.current = (_track$current2 = track.current) == null ? void 0 : _track$current2.getBoundingClientRect();
|
|
// And now we can proceed
|
|
toggle();
|
|
}, [track]);
|
|
return /*#__PURE__*/React.createElement("group", _extends({
|
|
ref: fref
|
|
}, props), ready && createPortal(/*#__PURE__*/React.createElement(Container, {
|
|
visible: visible,
|
|
canvasSize: size,
|
|
frames: frames,
|
|
scene: scene,
|
|
track: track,
|
|
rect: rect,
|
|
index: index
|
|
}, children), virtualScene, {
|
|
events: {
|
|
compute,
|
|
priority: index
|
|
},
|
|
size: {
|
|
width: (_rect$current = rect.current) == null ? void 0 : _rect$current.width,
|
|
height: (_rect$current2 = rect.current) == null ? void 0 : _rect$current2.height,
|
|
// @ts-ignore
|
|
top: (_rect$current3 = rect.current) == null ? void 0 : _rect$current3.top,
|
|
// @ts-ignore
|
|
left: (_rect$current4 = rect.current) == null ? void 0 : _rect$current4.left
|
|
}
|
|
}));
|
|
});
|
|
const HtmlView = /* @__PURE__ */React.forwardRef(({
|
|
as: El = 'div',
|
|
id,
|
|
visible,
|
|
className,
|
|
style,
|
|
index = 1,
|
|
track,
|
|
frames = Infinity,
|
|
children,
|
|
...props
|
|
}, fref) => {
|
|
const uuid = React.useId();
|
|
const ref = React.useRef(null);
|
|
React.useImperativeHandle(fref, () => ref.current);
|
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(El, _extends({
|
|
ref: ref,
|
|
id: id,
|
|
className: className,
|
|
style: style
|
|
}, props)), /*#__PURE__*/React.createElement(tracked.In, null, /*#__PURE__*/React.createElement(CanvasView, {
|
|
visible: visible,
|
|
key: uuid,
|
|
track: ref,
|
|
frames: frames,
|
|
index: index
|
|
}, children)));
|
|
});
|
|
const View = /* @__PURE__ */(() => {
|
|
const _View = /*#__PURE__*/React.forwardRef((props, fref) => {
|
|
// If we're inside a canvas we should be able to access the context store
|
|
const store = React.useContext(context);
|
|
// If that's not the case we render a tunnel
|
|
if (!store) return /*#__PURE__*/React.createElement(HtmlView, _extends({
|
|
ref: fref
|
|
}, props));
|
|
// Otherwise a plain canvas-view
|
|
else return /*#__PURE__*/React.createElement(CanvasView, _extends({
|
|
ref: fref
|
|
}, props));
|
|
});
|
|
_View.Port = () => /*#__PURE__*/React.createElement(tracked.Out, null);
|
|
return _View;
|
|
})();
|
|
|
|
export { View };
|