react-native-filament
Version:
A real-time physically based 3D rendering engine for React Native
236 lines (217 loc) • 9.28 kB
JavaScript
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import React from 'react';
import { FilamentProxy } from '../native/FilamentProxy';
import FilamentNativeView from '../native/specs/FilamentViewNativeComponent';
import { reportWorkletError } from '../ErrorUtils';
import { Context } from './Context';
import { findNodeHandle } from 'react-native';
/**
* The component that wraps the native view.
* @private
*/
export class FilamentView extends React.PureComponent {
/**
* Uses the context in class.
* @note Not available in the constructor!
*/
static contextType = Context;
// @ts-ignore
constructor(props) {
super(props);
this.ref = /*#__PURE__*/React.createRef();
}
get handle() {
const nodeHandle = findNodeHandle(this.ref.current);
if (nodeHandle == null || nodeHandle === -1) {
throw new Error("Could not get the FilamentView's native view tag! Does the FilamentView exist in the native view-tree?");
}
return nodeHandle;
}
updateTransparentRendering = enable => {
const {
renderer
} = this.getContext();
renderer.setClearContent(enable);
};
latestToken = 0;
updateRenderCallback = async (callback, swapChain) => {
var _this$renderCallbackL;
const currentToken = ++this.latestToken;
const {
renderer,
view,
workletContext,
_choreographer
} = this.getContext();
// When requesting to update the render callback we have to assume that the previous one is not valid anymore
// ie. its pointing to already released resources from useDisposableResource:
(_this$renderCallbackL = this.renderCallbackListener) === null || _this$renderCallbackL === void 0 || _this$renderCallbackL.remove();
// Adding a new render callback listener is an async operation
const listener = await workletContext.runAsync(() => {
'worklet';
// We need to create the function we pass to addFrameCallbackListener on the worklet thread, so that the
// underlying JSI function is owned by that thread. Only then can we call it on the worklet thread when
// the choreographer is calling its listeners.
return _choreographer.addFrameCallbackListener(frameInfo => {
'worklet';
try {
callback(frameInfo);
// TODO: investigate the root cause of this. This should never happen, but we've seen sentry reports of it.
if (!swapChain.isValid) {
console.warn('[react-native-filament] SwapChain is invalid, cannot render frame.\nThis should never happen, please report an issue with reproduction steps.');
return;
}
if (renderer.beginFrame(swapChain, frameInfo.timestamp)) {
renderer.render(view);
renderer.endFrame();
}
} catch (e) {
reportWorkletError(e);
}
});
});
// As setting the listener is async, we have to check updateRenderCallback was called meanwhile.
// In that case we have to assume that the listener we just set is not valid anymore:
if (currentToken !== this.latestToken) {
listener.remove();
return;
}
this.renderCallbackListener = listener;
};
getContext = () => {
if (this.context == null) {
throw new Error('Filament component must be used within a FilamentProvider!');
}
return this.context;
};
componentDidMount() {
// Setup transparency mode:
if (!this.props.enableTransparentRendering) {
this.updateTransparentRendering(false);
}
}
componentDidUpdate(prevProps) {
if (prevProps.enableTransparentRendering !== this.props.enableTransparentRendering) {
this.updateTransparentRendering(this.props.enableTransparentRendering ?? true);
}
if (prevProps.renderCallback !== this.props.renderCallback && this.swapChain != null) {
// Note: if swapChain was null, the renderCallback will be set/updated in onSurfaceCreated, which uses the latest renderCallback prop
this.updateRenderCallback(this.props.renderCallback, this.swapChain);
}
}
/**
* Calling this signals that this FilamentView will be removed, and it should release all its resources and listeners.
*/
cleanupResources() {
var _this$renderCallbackL2, _this$swapChain, _this$view;
const {
_choreographer
} = this.getContext();
_choreographer.stop();
(_this$renderCallbackL2 = this.renderCallbackListener) === null || _this$renderCallbackL2 === void 0 || _this$renderCallbackL2.remove();
(_this$swapChain = this.swapChain) === null || _this$swapChain === void 0 || _this$swapChain.release();
this.swapChain = undefined; // Note: important to set it to undefined, as this might be called twice (onSurfaceDestroyed and componentWillUnmount), and we can only release once
// Unlink the view from the choreographer. The native view might be destroyed later, after another FilamentView is created using the same choreographer (and then it would stop the rendering)
(_this$view = this.view) === null || _this$view === void 0 || _this$view.setChoreographer(undefined);
}
componentWillUnmount() {
var _this$surfaceCreatedL, _this$surfaceDestroye;
(_this$surfaceCreatedL = this.surfaceCreatedListener) === null || _this$surfaceCreatedL === void 0 || _this$surfaceCreatedL.remove();
(_this$surfaceDestroye = this.surfaceDestroyedListener) === null || _this$surfaceDestroye === void 0 || _this$surfaceDestroye.remove();
this.cleanupResources();
}
// This registers the surface provider, which will be notified when the surface is ready to draw on:
onViewReady = async () => {
const context = this.getContext();
const handle = this.handle;
console.log('Finding FilamentView with handle', handle);
this.view = await FilamentProxy.findFilamentView(handle);
if (this.view == null) {
throw new Error(`Failed to find FilamentView #${handle}!`);
}
console.log('Found FilamentView!');
// Link the view with the choreographer.
// When the view gets destroyed, the choreographer will be stopped.
this.view.setChoreographer(context._choreographer);
if (this.ref.current == null) {
throw new Error('Ref is not set!');
}
const surfaceProvider = this.view.getSurfaceProvider();
this.surfaceCreatedListener = surfaceProvider.addOnSurfaceCreatedListener(() => {
this.onSurfaceCreated(surfaceProvider);
}, FilamentProxy.getCurrentDispatcher());
this.surfaceDestroyedListener = surfaceProvider.addOnSurfaceDestroyedListener(() => {
this.onSurfaceDestroyed();
}, FilamentProxy.getCurrentDispatcher());
// Link the surface with the engine:
console.log('Setting surface provider');
context.engine.setSurfaceProvider(surfaceProvider);
// Its possible that the surface is already created, then our callback wouldn't be called
// (we still keep the callback as on android a surface can be destroyed and recreated, while the view stays alive)
if (surfaceProvider.getSurface() != null) {
console.log('Surface already created!');
this.onSurfaceCreated(surfaceProvider);
}
};
// This will be called once the surface is created and ready to draw on:
onSurfaceCreated = async surfaceProvider => {
const {
engine,
workletContext,
_choreographer
} = this.getContext();
// Create a swap chain …
const enableTransparentRendering = this.props.enableTransparentRendering ?? true;
this.swapChain = await workletContext.runAsync(() => {
'worklet';
return engine.createSwapChainForSurface(surfaceProvider, enableTransparentRendering);
});
// Apply the swapchain to the engine …
engine.setSwapChain(this.swapChain);
// Set the render callback in the choreographer:
const {
renderCallback
} = this.props;
await this.updateRenderCallback(renderCallback, this.swapChain);
// Start the choreographer …
_choreographer.start();
};
/**
* On surface destroyed might be called multiple times for the same native view (FilamentView).
* On android if a surface is destroyed, it can be recreated, while the view stays alive.
*/
onSurfaceDestroyed = () => {
this.cleanupResources();
};
/**
* Pauses the rendering of the Filament view.
*/
pause = () => {
const {
_choreographer
} = this.getContext();
_choreographer.stop();
};
/**
* Resumes the rendering of the Filament view.
* It's a no-op if the rendering is already running.
*/
resume = () => {
const {
_choreographer
} = this.getContext();
_choreographer.start();
};
/** @internal */
render() {
return /*#__PURE__*/React.createElement(FilamentNativeView, _extends({
ref: this.ref,
onViewReady: this.onViewReady
}, this.props));
}
}
// @ts-expect-error Not in the types
FilamentView.defaultProps = {
enableTransparentRendering: true
};
//# sourceMappingURL=FilamentView.js.map