diff --git a/package.json b/package.json index 653cc4027c..a9ed12a92e 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.4.0", "expo-asset": "^8.4.6", + "expo-file-system": "^15.4.3", "expo-gl": "^11.1.2", "husky": "^7.0.4", "jest": "^29.3.1", diff --git a/packages/fiber/package.json b/packages/fiber/package.json index 4717d1083c..445d66eff0 100644 --- a/packages/fiber/package.json +++ b/packages/fiber/package.json @@ -55,6 +55,7 @@ "expo": ">=43.0", "expo-asset": ">=8.4", "expo-gl": ">=11.0", + "expo-file-system": ">=11.0", "react": ">=18.0", "react-dom": ">=18.0", "react-native": ">=0.64", @@ -73,6 +74,9 @@ "expo-asset": { "optional": true }, + "expo-file-system": { + "optional": true + }, "expo-gl": { "optional": true } diff --git a/packages/fiber/src/native/polyfills.ts b/packages/fiber/src/native/polyfills.ts index c0253cfbab..7d53b4401c 100644 --- a/packages/fiber/src/native/polyfills.ts +++ b/packages/fiber/src/native/polyfills.ts @@ -1,6 +1,7 @@ import * as THREE from 'three' import { Platform, NativeModules, Image } from 'react-native' -import type { Asset } from 'expo-asset' +import { Asset } from 'expo-asset' +import * as fs from 'expo-file-system' if (Platform.OS !== 'web') { const BlobManager = require('react-native/Libraries/Blob/BlobManager.js') @@ -73,20 +74,21 @@ if (Platform.OS !== 'web') { return `${BLOB_URL_PREFIX}${blob.data.blobId}?offset=${blob.data.offset}&size=${blob.size}` } - // Check if expo-asset is installed (available with expo modules) - let expAsset: typeof Asset | undefined - try { - expAsset = require('expo-asset')?.Asset - } catch (_) {} - /** * Generates an asset based on input type. */ async function getAsset(input: string | number): Promise { switch (typeof input) { case 'string': - if (input.startsWith('data:')) return { localUri: input } as Asset - if (input.startsWith('blob:')) { + if (input.startsWith('data:')) { + const [header, data] = input.split(',') + const [, type] = header.split('/') + + const localUri = fs.cacheDirectory + uuidv4() + `.${type}` + await fs.writeAsStringAsync(localUri, data, { encoding: fs.EncodingType.Base64 }) + + return { localUri } as Asset + } else if (input.startsWith('blob:')) { const blob = await new Promise((res, rej) => { const xhr = new XMLHttpRequest() xhr.open('GET', input) @@ -107,123 +109,121 @@ if (Platform.OS !== 'web') { return getAsset(localUri) } - return expAsset!.fromURI(input).downloadAsync() + return Asset.fromURI(input).downloadAsync() case 'number': - return expAsset!.fromModule(input).downloadAsync() + return Asset.fromModule(input).downloadAsync() default: throw new Error('R3F: Invalid asset! Must be a URI or module.') } } - if (expAsset) { - // 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) : './') - - // There's no Image in native, so create a data texture instead - THREE.TextureLoader.prototype.load = function load(url, onLoad, onProgress, onError) { - const texture = new THREE.Texture() - - getAsset(url) - .then(async (asset: Asset) => { - if (!asset.width || !asset.height) { - const { width, height } = await new Promise<{ width: number; height: number }>((res, rej) => - Image.getSize(asset.localUri!, (width, height) => res({ width, height }), rej), - ) - asset.width = width - asset.height = height - } - - texture.image = { - data: { localUri: asset.localUri }, - width: asset.width, - height: asset.height, - } - texture.flipY = true - // texture.unpackAlignment = 1 - texture.needsUpdate = true - - // @ts-ignore - texture.isDataTexture = true - - onLoad?.(texture) - }) - .catch(onError) - - return texture - } + // 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) : './') - // Fetches assets via XMLHttpRequest - THREE.FileLoader.prototype.load = function load(url, onLoad, onProgress, onError) { - if (this.path) url = this.path + url + // There's no Image in native, so create a data texture instead + THREE.TextureLoader.prototype.load = function load(url, onLoad, onProgress, onError) { + const texture = new THREE.Texture() - const request = new XMLHttpRequest() + getAsset(url) + .then(async (asset: Asset) => { + if (!asset.width || !asset.height) { + const { width, height } = await new Promise<{ width: number; height: number }>((res, rej) => + Image.getSize(asset.localUri!, (width, height) => res({ width, height }), rej), + ) + asset.width = width + asset.height = height + } - getAsset(url) - .then((asset) => { - request.open('GET', asset.uri, true) + texture.image = { + data: { localUri: asset.localUri }, + width: asset.width, + height: asset.height, + } + texture.flipY = true + // texture.unpackAlignment = 1 + texture.needsUpdate = true - request.addEventListener( - 'load', - (event) => { - if (request.status === 200) { - onLoad?.(request.response) + // @ts-ignore + texture.isDataTexture = true - this.manager.itemEnd(url) - } else { - onError?.(event as unknown as ErrorEvent) + onLoad?.(texture) + }) + .catch(onError) - this.manager.itemError(url) - this.manager.itemEnd(url) - } - }, - false, - ) + return texture + } - request.addEventListener( - 'progress', - (event) => { - onProgress?.(event) - }, - false, - ) + // Fetches assets via XMLHttpRequest + THREE.FileLoader.prototype.load = function load(url, onLoad, onProgress, onError) { + if (this.path) url = this.path + url - request.addEventListener( - 'error', - (event) => { - onError?.(event as unknown as ErrorEvent) + const request = new XMLHttpRequest() - this.manager.itemError(url) - this.manager.itemEnd(url) - }, - false, - ) + getAsset(url) + .then((asset) => { + request.open('GET', asset.uri, true) - request.addEventListener( - 'abort', - (event) => { + request.addEventListener( + 'load', + (event) => { + if (request.status === 200) { + onLoad?.(request.response) + + this.manager.itemEnd(url) + } else { onError?.(event as unknown as ErrorEvent) this.manager.itemError(url) this.manager.itemEnd(url) - }, - false, - ) - - if (this.responseType) request.responseType = this.responseType - if (this.withCredentials) request.withCredentials = this.withCredentials - - for (const header in this.requestHeader) { - request.setRequestHeader(header, this.requestHeader[header]) - } + } + }, + false, + ) + + request.addEventListener( + 'progress', + (event) => { + onProgress?.(event) + }, + false, + ) + + request.addEventListener( + 'error', + (event) => { + onError?.(event as unknown as ErrorEvent) + + this.manager.itemError(url) + this.manager.itemEnd(url) + }, + false, + ) + + request.addEventListener( + 'abort', + (event) => { + onError?.(event as unknown as ErrorEvent) + + this.manager.itemError(url) + this.manager.itemEnd(url) + }, + false, + ) + + if (this.responseType) request.responseType = this.responseType + if (this.withCredentials) request.withCredentials = this.withCredentials + + for (const header in this.requestHeader) { + request.setRequestHeader(header, this.requestHeader[header]) + } - request.send(null) + request.send(null) - this.manager.itemStart(url) - }) - .catch(onError) + this.manager.itemStart(url) + }) + .catch(onError) - return request - } + return request } } diff --git a/yarn.lock b/yarn.lock index 2954dfa779..16f4f7ea20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4800,6 +4800,13 @@ expo-asset@^8.4.6: path-browserify "^1.0.0" url-parse "^1.4.4" +expo-file-system@^15.4.3: + version "15.4.3" + resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-15.4.3.tgz#0cb2464c6e663ad8e8a742d5c538ed8ff1013b11" + integrity sha512-HaaCBTUATs2+i7T4jxIvoU9rViAHMvOD2eBaJ1H7xPHlwZlMORjQs7bsNKonR/TQoduxZBJLVZGawvaAJNCH8g== + dependencies: + uuid "^3.4.0" + expo-gl-cpp@~11.1.0: version "11.1.1" resolved "https://registry.yarnpkg.com/expo-gl-cpp/-/expo-gl-cpp-11.1.1.tgz#883781535658a3598f2262425b1d3527b0e72760" @@ -10078,7 +10085,7 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@^3.3.2: +uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==