From a8c7fe25b71cc08c07fe947c8641b0b2674b6dc6 Mon Sep 17 00:00:00 2001 From: Brian Zinn Date: Thu, 18 Feb 2021 11:20:39 -0800 Subject: [PATCH] fix: model loading progress callback (was in v2) #122 --- src/customComponents/Model.tsx | 21 +- src/hooks/loaders/useSceneLoader.tsx | 347 ++++++++++++++------------- 2 files changed, 200 insertions(+), 168 deletions(-) diff --git a/src/customComponents/Model.tsx b/src/customComponents/Model.tsx index d6ad5b1f..b9cb2912 100644 --- a/src/customComponents/Model.tsx +++ b/src/customComponents/Model.tsx @@ -6,8 +6,9 @@ import { ILoadedModel } from "../hooks/loaders/loadedModel"; import { SceneLoaderOptions, useSceneLoader } from "../hooks/loaders/useSceneLoader"; export type ModelProps = { - /** + /** * Only used on init. Will not update dynamically (scaling will update dynamically and override this) + * An array of mesh names, a single mesh name, or empty string for all meshes that filter what meshes are imported */ meshNames?: any receiveShadows?: boolean @@ -24,13 +25,21 @@ export type ModelProps = { } & FiberAbstractMeshProps & FiberAbstractMeshPropsCtor; const Model: React.FC = (props: ModelProps) => { - const { alwaysSelectAsActiveMesh, onModelLoaded, pluginExtension, rootUrl, receiveShadows, reportProgress, scaleToDimension, sceneFilename, ...rest } = props; + const { + alwaysSelectAsActiveMesh, meshNames, onLoadProgress, onModelError, onModelLoaded, receiveShadows, reportProgress, scaleToDimension, // SceneLoaderOptions + rootUrl, sceneFilename, pluginExtension, // other parameters + ...rest // passed on to "rootMesh" + } = props; + const options: SceneLoaderOptions = { - receiveShadows, - scaleToDimension, alwaysSelectAsActiveMesh, + meshNames, + onLoadProgress, + onModelError, + onModelLoaded, + receiveShadows, reportProgress, - onModelLoaded + scaleToDimension, } const sceneLoaderResults = useSceneLoader(rootUrl, sceneFilename, pluginExtension, options); @@ -41,7 +50,7 @@ const Model: React.FC = (props: ModelProps) => { } }, []); - return ; + return ; } export default Model; diff --git a/src/hooks/loaders/useSceneLoader.tsx b/src/hooks/loaders/useSceneLoader.tsx index f1fe052f..638f7a52 100644 --- a/src/hooks/loaders/useSceneLoader.tsx +++ b/src/hooks/loaders/useSceneLoader.tsx @@ -5,198 +5,221 @@ import { useScene } from '../scene'; import { ILoadedModel, LoadedModel, LoaderStatus } from './loadedModel'; export type SceneLoaderContextType = { - updateProgress: (progress: ISceneLoaderProgressEvent) => void - lastProgress?: Nullable + updateProgress: (progress: ISceneLoaderProgressEvent) => void + lastProgress?: Nullable } | undefined; export const SceneLoaderContext = React.createContext(undefined); export type SceneLoaderContextProviderProps = { - startProgress?: ISceneLoaderProgressEvent, - children: React.ReactNode, + startProgress?: ISceneLoaderProgressEvent, + children: React.ReactNode, } export const SceneLoaderContextProvider: React.FC = (props: SceneLoaderContextProviderProps) => { - const [progress, setProgress] = useState>(null); + const [progress, setProgress] = useState>(null); - return ( - {props.children} - ); + return ( + {props.children} + ); } export type SceneLoaderOptions = { - /** - * set that all meshes receive shadows. - * Defaults to false. - */ - receiveShadows?: boolean - - /** - * Scale entire model within these square bounds - * Defaults to no scaling. - */ - scaleToDimension?: number - - /** - * Always select root mesh as active. - * Defaults to false. - */ - alwaysSelectAsActiveMesh?: boolean - - /** - * SceneLoader progress events are set on context provider (when available). - * Defaults to false. - */ - reportProgress?: boolean - - scene?: Scene - - /** - * Access to loaded model as soon as it is loaded, so it provides - * a way to hide or scale the meshes before the first render. - */ - onModelLoaded?: (loadedModel: ILoadedModel) => void + /** + * An array of mesh names, a single mesh name, or empty string for all meshes that filter what meshes are imported + */ + meshNames: any + + /** + * set that all meshes receive shadows. + * Defaults to false. + */ + receiveShadows?: boolean + + /** + * Scale entire model within these square bounds + * Defaults to no scaling. + */ + scaleToDimension?: number + + /** + * Always select root mesh as active. + * Defaults to false. + */ + alwaysSelectAsActiveMesh?: boolean + + /** + * SceneLoader progress events are set on context provider (when available). + * Defaults to false. + */ + reportProgress?: boolean + + /** + * Not needed if you are within a SceneContext. + */ + scene?: Scene + + /** + * Access to loaded model as soon as it is loaded, so it provides a way to hide or scale the meshes before the first render. + */ + onModelLoaded?: (loadedModel: ILoadedModel) => void + + /** + * Raw progress event for SceneLoader + */ + onLoadProgress?: (event: ISceneLoaderProgressEvent) => void + + /** + * Called if SceneLoader returns an error. + */ + onModelError?: (model: ILoadedModel) => void } /** * A cached version of SceneLoader with a Suspense cache. */ const useSceneLoaderWithCache = (): (rootUrl: string, sceneFilename: string, pluginExtension?: string, options?: SceneLoaderOptions) => LoadedModel => { - // we need our own memoized cache. useMemo, useState, etc. fail miserably - throwing a promise forces the component to remount. - let suspenseCache: Record LoadedModel)> = {}; - let suspenseScene: Nullable = null; + // we need our own memoized cache. useMemo, useState, etc. fail miserably - throwing a promise forces the component to remount. + let suspenseCache: Record LoadedModel)> = {}; + let suspenseScene: Nullable = null; - // let tasksCompletedCache: Record = {}; + // let tasksCompletedCache: Record = {}; - return (rootUrl: string, sceneFilename: string, pluginExtension?: string, options?: SceneLoaderOptions): LoadedModel => { - const opts: SceneLoaderOptions = options || {}; + return (rootUrl: string, sceneFilename: string, pluginExtension?: string, options?: SceneLoaderOptions): LoadedModel => { + const opts: SceneLoaderOptions = options || {}; - const hookScene = useScene(); + const hookScene = useScene(); - if (opts.scene === undefined && hookScene === null) { - throw new Error('useSceneLoader can only be used inside a Scene component (or pass scene as an option)') - } + if (opts.scene === undefined && hookScene === null) { + throw new Error('useSceneLoader can only be used inside a Scene component (or pass scene as an option)') + } - const scene: Scene = opts.scene || hookScene! + const scene: Scene = opts.scene || hookScene! + + if (suspenseScene == null) { + suspenseScene = scene; + } else { + if (suspenseScene !== scene) { + // console.log('new scene detected - clearing useAssetManager cache'); + suspenseCache = {}; + // NOTE: could keep meshes with mesh.serialize and Mesh.Parse + // Need to research how to do with textures, etc. + // browser cache should make the load fast in most cases + // tasksCompletedCache = {}; + suspenseScene = scene; + } + } - if (suspenseScene == null) { - suspenseScene = scene; - } else { - if (suspenseScene !== scene) { - // console.log('new scene detected - clearing useAssetManager cache'); - suspenseCache = {}; - // NOTE: could keep meshes with mesh.serialize and Mesh.Parse - // Need to research how to do with textures, etc. - // browser cache should make the load fast in most cases - // tasksCompletedCache = {}; - suspenseScene = scene; + const suspenseKey = `${rootUrl}/${sceneFilename}`; + const sceneLoaderContext = useContext(SceneLoaderContext); + + const createSceneLoader = (): () => LoadedModel => { + const taskPromise = new Promise((resolve, reject) => { + let loadedModel = new LoadedModel() + + loadedModel.status = LoaderStatus.Loading + + let loader: Nullable = SceneLoader.ImportMesh( + meshNames: opts.meshNames, + rootUrl, + sceneFilename, + scene, + (meshes: AbstractMesh[], particleSystems: IParticleSystem[], skeletons: Skeleton[], animationGroups: AnimationGroup[]): void => { + loadedModel.rootMesh = new AbstractMesh(sceneFilename + "-root-model", scene); + if (opts.alwaysSelectAsActiveMesh === true) { + loadedModel.rootMesh.alwaysSelectAsActiveMesh = true; } - } - const suspenseKey = `${rootUrl}/${sceneFilename}`; - const sceneLoaderContext = useContext(SceneLoaderContext); - - const createSceneLoader = (): () => LoadedModel => { - const taskPromise = new Promise((resolve, reject) => { - let loadedModel = new LoadedModel() - - loadedModel.status = LoaderStatus.Loading - - let loader: Nullable = SceneLoader.ImportMesh( - undefined, - rootUrl, - sceneFilename, - scene, - (meshes: AbstractMesh[], particleSystems: IParticleSystem[], skeletons: Skeleton[], animationGroups: AnimationGroup[]): void => { - loadedModel.rootMesh = new AbstractMesh(sceneFilename + "-root-model", scene); - if (opts.alwaysSelectAsActiveMesh === true) { - loadedModel.rootMesh.alwaysSelectAsActiveMesh = true; - } - - loadedModel.meshes = []; - meshes.forEach(mesh => { - loadedModel.meshes!.push(mesh); - - // leave meshes already parented to maintain model hierarchy: - if (!mesh.parent) { - mesh.parent = loadedModel.rootMesh!; - } - - if (opts.receiveShadows === true) { - mesh.receiveShadows = true; - } - }) - loadedModel.particleSystems = particleSystems; - loadedModel.skeletons = skeletons; - loadedModel.animationGroups = animationGroups; - - loadedModel.status = LoaderStatus.Loaded; - - if (opts.scaleToDimension) { - loadedModel.scaleTo(opts.scaleToDimension); - } - if (options?.onModelLoaded) { - options.onModelLoaded(loadedModel); - } - - const originalDispose = loadedModel.dispose; - loadedModel.dispose = () => { - // console.log('Clearing cache (cannot re-use meshes).'); - suspenseCache[suspenseKey] = undefined; - originalDispose.call(loadedModel); - } - - resolve(loadedModel); - }, - (event: ISceneLoaderProgressEvent): void => { - if (opts.reportProgress === true && sceneLoaderContext !== undefined) { - sceneLoaderContext!.updateProgress(event); - } - }, - (_: Scene, message: string, exception?: any): void => { - reject(exception ?? message); - }, - pluginExtension - ) - - if (loader) { - loadedModel.loaderName = loader.name - } else { - loadedModel.loaderName = "no loader found" - } - }); - - let result: LoadedModel; - let error: Nullable = null; - let suspender: Nullable> = (async () => { - try { - result = await taskPromise; - } catch (e) { - error = e; - } finally { - suspender = null; - } - })(); - - const getAssets = () => { - if (suspender) { - throw suspender - }; - if (error !== null) { - throw error; - } - - return result; - }; - return getAssets; - } + loadedModel.meshes = []; + meshes.forEach(mesh => { + loadedModel.meshes!.push(mesh); + + // leave meshes already parented to maintain model hierarchy: + if (!mesh.parent) { + mesh.parent = loadedModel.rootMesh!; + } + + if (opts.receiveShadows === true) { + mesh.receiveShadows = true; + } + }) + loadedModel.particleSystems = particleSystems; + loadedModel.skeletons = skeletons; + loadedModel.animationGroups = animationGroups; - if (suspenseCache[suspenseKey] === undefined) { - suspenseCache[suspenseKey] = createSceneLoader(); + loadedModel.status = LoaderStatus.Loaded; + + if (opts.scaleToDimension) { + loadedModel.scaleTo(opts.scaleToDimension); + } + if (options?.onModelLoaded) { + options.onModelLoaded(loadedModel); + } + + const originalDispose = loadedModel.dispose; + loadedModel.dispose = () => { + // console.log('Clearing cache (cannot re-use meshes).'); + suspenseCache[suspenseKey] = undefined; + originalDispose.call(loadedModel); + } + + resolve(loadedModel); + }, + (event: ISceneLoaderProgressEvent): void => { + if (opts.reportProgress === true && sceneLoaderContext !== undefined) { + sceneLoaderContext!.updateProgress(event); + } + if (opts.onLoadProgress) { + opts.onLoadProgress(event); + } + }, + (_: Scene, message: string, exception?: any): void => { + if (opts.onModelError) { + opts.onModelError(loadedModel); + } + reject(exception ?? message); + }, + pluginExtension + ) + + if (loader) { + loadedModel.loaderName = loader.name + } else { + loadedModel.loaderName = "no loader found" + } + }); + + let result: LoadedModel; + let error: Nullable = null; + let suspender: Nullable> = (async () => { + try { + result = await taskPromise; + } catch (e) { + error = e; + } finally { + suspender = null; + } + })(); + + const getAssets = () => { + if (suspender) { + throw suspender + }; + if (error !== null) { + throw error; } - return suspenseCache[suspenseKey]!(); + return result; + }; + return getAssets; } + + if (suspenseCache[suspenseKey] === undefined) { + suspenseCache[suspenseKey] = createSceneLoader(); + } + + return suspenseCache[suspenseKey]!(); + } } export const useSceneLoader = useSceneLoaderWithCache();