react-native-filament
Version:
A real-time physically based 3D rendering engine for React Native
316 lines (293 loc) • 13.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.FilamentView = void 0;
var _react = _interopRequireDefault(require("react"));
var _FilamentProxy = require("../native/FilamentProxy");
var _FilamentViewNativeComponent = _interopRequireDefault(require("../native/specs/FilamentViewNativeComponent"));
var _ErrorUtils = require("../ErrorUtils");
var _useFilamentContext = require("../hooks/useFilamentContext");
var _reactNative = require("react-native");
var _reactNativeWorkletsCore = require("react-native-worklets-core");
var _Logger = require("../utilities/logger/Logger");
var _TouchHandlerContext = require("./TouchHandlerContext");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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); }
const Logger = (0, _Logger.getLogger)();
let viewIds = 0;
/**
* The component that actually renders the native view and displays our content (think of it as canvas).
*/
class FilamentView extends _react.default.PureComponent {
// There is a race condition where the surface might be destroyed before the swapchain is created.
// For this we keep track of the surface state:
isSurfaceAlive = _reactNativeWorkletsCore.Worklets.createSharedValue(true);
isComponentMounted = false;
/**
* Uses the context in class.
* @note Not available in the constructor!
*/
static contextType = _useFilamentContext.FilamentContext;
// @ts-expect-error We can't use the declare keyword here as react-natives metro babel preset isn't able to handle it yet
constructor(props) {
super(props);
this.ref = /*#__PURE__*/_react.default.createRef();
this.viewId = viewIds++;
}
get handle() {
const nodeHandle = (0, _reactNative.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
Logger.debug('Setting render callback');
const listener = await workletContext.runAsync((0, _ErrorUtils.wrapWithErrorHandler)(() => {
'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';
if (!swapChain.isValid) {
// TODO: Supposedly fixed in https://github.com/margelo/react-native-filament/pull/210, remove this once proven
(0, _ErrorUtils.reportWorkletError)(new Error('[react-native-filament] SwapChain is invalid, cannot render frame.\nThis should never happen, please report an issue with reproduction steps.'));
return;
}
try {
callback(frameInfo);
if (renderer.beginFrame(swapChain, frameInfo.timestamp)) {
renderer.render(view);
renderer.endFrame();
}
} catch (error) {
(0, _ErrorUtils.reportWorkletError)(error);
}
});
}));
// It can happen that after the listener was set the surface got destroyed already:
if (!this.isComponentMounted || !this.isSurfaceAlive.value) {
Logger.debug('🚧 Component is already unmounted or surface is no longer alive, removing choreographer listener');
listener.remove();
return;
}
// 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;
Logger.debug('Render callback set!');
// Calling this here ensures that only after the latest successful call for attaching a listener, the choreographer is started.
Logger.debug('Starting choreographer');
choreographer.start();
};
getContext = () => {
if (this.context == null) {
throw new Error('Filament component must be used within a FilamentProvider!');
}
return this.context;
};
componentDidMount() {
Logger.debug('Mounting FilamentView', this.viewId);
this.isComponentMounted = true;
// 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;
Logger.debug('Cleaning up resources');
const {
choreographer
} = this.getContext();
choreographer.stop();
(_this$renderCallbackL2 = this.renderCallbackListener) === null || _this$renderCallbackL2 === void 0 || _this$renderCallbackL2.remove();
this.isSurfaceAlive.value = false;
(_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;
Logger.debug('Unmounting FilamentView', this.viewId);
this.isComponentMounted = false;
(_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;
Logger.debug('Finding FilamentView with handle', handle);
this.view = await _FilamentProxy.FilamentProxy.findFilamentView(handle);
if (this.view == null) {
throw new Error(`Failed to find FilamentView #${handle}!`);
}
if (!this.isComponentMounted) {
// It can happen that while the above async function executed the view was already removed
Logger.debug('➡️ Component already unmounted, skipping setup');
return;
}
Logger.debug('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();
const filamentDispatcher = _FilamentProxy.FilamentProxy.getCurrentDispatcher();
this.surfaceCreatedListener = surfaceProvider.addOnSurfaceCreatedListener(() => {
this.onSurfaceCreated(surfaceProvider);
}, filamentDispatcher);
this.surfaceDestroyedListener = surfaceProvider.addOnSurfaceDestroyedListener(() => {
this.onSurfaceDestroyed();
}, filamentDispatcher);
// Link the surface with the engine:
Logger.debug('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) {
Logger.debug('Surface already created!');
this.onSurfaceCreated(surfaceProvider);
}
};
// This will be called once the surface is created and ready to draw on:
onSurfaceCreated = async surfaceProvider => {
Logger.debug('Surface created!');
const isSurfaceAlive = this.isSurfaceAlive;
isSurfaceAlive.value = true;
const {
engine,
workletContext
} = this.getContext();
// Create a swap chain …
const enableTransparentRendering = this.props.enableTransparentRendering ?? true;
Logger.debug('Creating swap chain');
const swapChain = await workletContext.runAsync(() => {
'worklet';
if (!isSurfaceAlive.value) {
return null;
}
try {
return engine.createSwapChainForSurface(surfaceProvider, enableTransparentRendering);
} catch (error) {
// Report this error as none-fatal. We only throw in createSwapChainForSurface if the surface is already released.
// There is the chance of a race condition where the surface is destroyed but our JS onDestroy listener hasn't been called yet.
(0, _ErrorUtils.reportWorkletError)(error, false);
return null;
}
});
if (swapChain == null) {
isSurfaceAlive.value = false;
Logger.info('🚧 Swap chain is null, surface was already destroyed while we tried to create a swapchain from it.');
return;
}
this.swapChain = swapChain;
// Apply the swapchain to the engine …
Logger.debug('Setting swap chain');
engine.setSwapChain(this.swapChain);
// Set the render callback in the choreographer:
const {
renderCallback
} = this.props;
await this.updateRenderCallback(renderCallback, this.swapChain);
};
/**
* 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 = () => {
Logger.info('Surface destroyed!');
this.isSurfaceAlive.value = false;
this.cleanupResources();
};
/**
* Pauses the rendering of the Filament view.
*/
pause = () => {
Logger.info('Pausing rendering');
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 = () => {
Logger.info('Resuming rendering');
const {
choreographer
} = this.getContext();
choreographer.start();
};
onTouchStart = event => {
if (this.props.onTouchStart != null) {
this.props.onTouchStart(event);
}
// Gets the registered callbacks from the TouchHandlerContext
// This way we only have one real gesture responder event handler
const touchHandlers = (0, _TouchHandlerContext.getTouchHandlers)();
const callbacks = Object.values(touchHandlers);
Logger.debug('onTouchStart, handlers count:', callbacks.length);
for (const handler of callbacks) {
handler(event);
}
};
/** @internal */
render() {
return /*#__PURE__*/_react.default.createElement(_FilamentViewNativeComponent.default, _extends({
ref: this.ref,
onViewReady: this.onViewReady
}, this.props, {
onTouchStart: this.onTouchStart
}));
}
}
// @ts-expect-error Not in the types
exports.FilamentView = FilamentView;
FilamentView.defaultProps = {
enableTransparentRendering: true
};
//# sourceMappingURL=FilamentView.js.map