Skip to content

Commit

Permalink
fix(native): make expo modules optional
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyJasonBennett committed Sep 13, 2024
1 parent 4c154d9 commit 9de5145
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 58 deletions.
11 changes: 8 additions & 3 deletions packages/fiber/src/native/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -25,6 +24,8 @@ export interface CanvasProps extends Omit<RenderProps<HTMLCanvasElement>, '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
Expand Down Expand Up @@ -52,6 +53,9 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef<View, Props>(
},
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
Expand Down Expand Up @@ -85,7 +89,7 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef<View, Props>(

// 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<string, EventListener[]>()

const canvas = {
Expand Down Expand Up @@ -198,10 +202,11 @@ const CanvasImpl = /*#__PURE__*/ React.forwardRef<View, Props>(
// 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()
}

Expand Down
116 changes: 61 additions & 55 deletions packages/fiber/src/native/polyfills.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -14,59 +12,6 @@ function uuidv4() {
})
}

async function getAsset(input: string | number): Promise<string> {
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<Blob>((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<string>((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
Expand Down Expand Up @@ -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<string> {
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<Blob>((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<string>((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) : './')
Expand Down

0 comments on commit 9de5145

Please sign in to comment.