diff --git a/packages/fiber/src/native/Canvas.tsx b/packages/fiber/src/native/Canvas.tsx index 36b6994d7e..4b57795003 100644 --- a/packages/fiber/src/native/Canvas.tsx +++ b/packages/fiber/src/native/Canvas.tsx @@ -11,7 +11,6 @@ import { StyleSheet, PixelRatio, } from 'react-native' -import { ExpoWebGLRenderingContext, GLView } from 'expo-gl' import { useContextBridge, FiberProvider } from 'its-fine' import { SetBlock, Block, ErrorBoundary, useMutableCallback } from '../core/utils' import { extend, createRoot, unmountComponentAtNode, RenderProps, ReconcilerRoot } from '../core' @@ -25,6 +24,8 @@ export interface CanvasProps extends Omit, 'size' export interface Props extends CanvasProps {} +let GLView: any | null = null // TODO: type reflection without importing + /** * A native canvas which accepts threejs elements as children. * @see https://docs.pmnd.rs/react-three-fiber/api/canvas @@ -52,6 +53,9 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( }, forwardedRef, ) => { + // Lazily load expo-gl, so it's only required when Canvas is used + GLView ??= require('expo-gl').GLView + // Create a known catalogue of Threejs-native elements // This will include the entire THREE namespace by default, users can extend // their own elements by using the createRoot API instead @@ -85,7 +89,7 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( // Called on context create or swap // https://github.com/pmndrs/react-three-fiber/pull/2297 - const onContextCreate = React.useCallback((context: ExpoWebGLRenderingContext) => { + const onContextCreate = React.useCallback((context: WebGL2RenderingContext) => { const listeners = new Map() const canvas = { @@ -198,10 +202,11 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( // Overwrite onCreated to apply RN bindings onCreated: (state: RootState) => { // Bind render to RN bridge - const context = state.gl.getContext() as ExpoWebGLRenderingContext + const context = state.gl.getContext() const renderFrame = state.gl.render.bind(state.gl) state.gl.render = (scene: THREE.Scene, camera: THREE.Camera) => { renderFrame(scene, camera) + // @ts-ignore context.endFrameEXP() } diff --git a/packages/fiber/src/native/polyfills.ts b/packages/fiber/src/native/polyfills.ts index b61b2d9fcb..b977fae805 100644 --- a/packages/fiber/src/native/polyfills.ts +++ b/packages/fiber/src/native/polyfills.ts @@ -1,7 +1,5 @@ import * as THREE from 'three' import { Image, NativeModules, Platform } from 'react-native' -import { Asset } from 'expo-asset' -import * as fs from 'expo-file-system' import { fromByteArray } from 'base64-js' import { Buffer } from 'buffer' @@ -14,59 +12,6 @@ function uuidv4() { }) } -async function getAsset(input: string | number): Promise { - if (typeof input === 'string') { - // Don't process storage - if (input.startsWith('file:')) return input - - // Unpack Blobs from react-native BlobManager - // https://github.com/facebook/react-native/issues/22681#issuecomment-523258955 - if (input.startsWith('blob:') || input.startsWith(NativeModules.BlobModule?.BLOB_URI_SCHEME)) { - const blob = await new Promise((res, rej) => { - const xhr = new XMLHttpRequest() - xhr.open('GET', input as string) - xhr.responseType = 'blob' - xhr.onload = () => res(xhr.response) - xhr.onerror = rej - xhr.send() - }) - - const data = await new Promise((res, rej) => { - const reader = new FileReader() - reader.onload = () => res(reader.result as string) - reader.onerror = rej - reader.readAsText(blob) - }) - - input = `data:${blob.type};base64,${data}` - } - - // Create safe URI for JSI serialization - if (input.startsWith('data:')) { - const [header, data] = input.split(';base64,') - const [, type] = header.split('/') - - const uri = fs.cacheDirectory + uuidv4() + `.${type}` - await fs.writeAsStringAsync(uri, data, { encoding: fs.EncodingType.Base64 }) - - return uri - } - } - - // Download bundler module or external URL - const asset = await Asset.fromModule(input).downloadAsync() - let uri = asset.localUri || asset.uri - - // Unpack assets in Android Release Mode - if (!uri.includes(':')) { - const file = `${fs.cacheDirectory}ExponentAsset-${asset.hash}.${asset.type}` - await fs.copyAsync({ from: uri, to: file }) - uri = file - } - - return uri -} - export function polyfills() { // Patch Blob for ArrayBuffer and URL if unsupported // https://github.com/facebook/react-native/pull/39276 @@ -114,6 +59,67 @@ export function polyfills() { } } + // Polyfill asset loading if expo modules are available + try { + var Asset = require('expo-asset').Asset + var fs = require('expo-file-system') + } catch (_) { + return + } + + async function getAsset(input: string | number): Promise { + if (typeof input === 'string') { + // Don't process storage + if (input.startsWith('file:')) return input + + // Unpack Blobs from react-native BlobManager + // https://github.com/facebook/react-native/issues/22681#issuecomment-523258955 + if (input.startsWith('blob:') || input.startsWith(NativeModules.BlobModule?.BLOB_URI_SCHEME)) { + const blob = await new Promise((res, rej) => { + const xhr = new XMLHttpRequest() + xhr.open('GET', input as string) + xhr.responseType = 'blob' + xhr.onload = () => res(xhr.response) + xhr.onerror = rej + xhr.send() + }) + + const data = await new Promise((res, rej) => { + const reader = new FileReader() + reader.onload = () => res(reader.result as string) + reader.onerror = rej + reader.readAsText(blob) + }) + + input = `data:${blob.type};base64,${data}` + } + + // Create safe URI for JSI serialization + if (input.startsWith('data:')) { + const [header, data] = input.split(';base64,') + const [, type] = header.split('/') + + const uri = fs.cacheDirectory + uuidv4() + `.${type}` + await fs.writeAsStringAsync(uri, data, { encoding: fs.EncodingType.Base64 }) + + return uri + } + } + + // Download bundler module or external URL + const asset = await Asset.fromModule(input).downloadAsync() + let uri = asset.localUri || asset.uri + + // Unpack assets in Android Release Mode + if (!uri.includes(':')) { + const file = `${fs.cacheDirectory}ExponentAsset-${asset.hash}.${asset.type}` + await fs.copyAsync({ from: uri, to: file }) + uri = file + } + + return uri + } + // Don't pre-process urls, let expo-asset generate an absolute URL const extractUrlBase = THREE.LoaderUtils.extractUrlBase.bind(THREE.LoaderUtils) THREE.LoaderUtils.extractUrlBase = (url: string) => (typeof url === 'string' ? extractUrlBase(url) : './')