From 9de5145d77a48f65f811a82dc3b4c7e6bf76de92 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Fri, 13 Sep 2024 15:31:25 -0500 Subject: [PATCH 1/2] fix(native): make expo modules optional --- packages/fiber/src/native/Canvas.tsx | 11 ++- packages/fiber/src/native/polyfills.ts | 116 +++++++++++++------------ 2 files changed, 69 insertions(+), 58 deletions(-) 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) : './') From 6015419c9720a22617f994be980b0b9fc92a3664 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Fri, 13 Sep 2024 16:26:55 -0500 Subject: [PATCH 2/2] chore: cleanup --- packages/fiber/src/native/Canvas.tsx | 12 ++- packages/fiber/src/native/polyfills.ts | 122 ++++++++++++------------- 2 files changed, 69 insertions(+), 65 deletions(-) diff --git a/packages/fiber/src/native/Canvas.tsx b/packages/fiber/src/native/Canvas.tsx index 4b57795003..04be38124d 100644 --- a/packages/fiber/src/native/Canvas.tsx +++ b/packages/fiber/src/native/Canvas.tsx @@ -24,7 +24,12 @@ export interface CanvasProps extends Omit, 'size' export interface Props extends CanvasProps {} -let GLView: any | null = null // TODO: type reflection without importing +// Lazily load expo-gl, so it's only required when Canvas is used +try { + var GLView = require('expo-gl').GLView +} catch (_) { + // +} /** * A native canvas which accepts threejs elements as children. @@ -53,9 +58,6 @@ 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 @@ -243,6 +245,8 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef( * @see https://docs.pmnd.rs/react-three-fiber/api/canvas */ export const Canvas = React.forwardRef(function CanvasWrapper(props, ref) { + if (!GLView) throw new Error('expo-gl must be installed to use Canvas!') + return ( diff --git a/packages/fiber/src/native/polyfills.ts b/packages/fiber/src/native/polyfills.ts index b977fae805..99602ef790 100644 --- a/packages/fiber/src/native/polyfills.ts +++ b/packages/fiber/src/native/polyfills.ts @@ -3,6 +3,14 @@ import { Image, NativeModules, Platform } from 'react-native' import { fromByteArray } from 'base64-js' import { Buffer } from 'buffer' +// Polyfill asset loading if expo modules are available +try { + var Asset = require('expo-asset').Asset + var fs = require('expo-file-system') +} catch (_) { + // +} + // http://stackoverflow.com/questions/105034 function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { @@ -12,6 +20,59 @@ 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 @@ -59,67 +120,6 @@ 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) : './')