| @@ -31,31 +31,33 @@ | |||
| const viewer = new ThreeViewer({canvas: document.getElementById('mcanvas')}) | |||
| async function init() { | |||
| async function init(loadAsFile = true) { | |||
| const url = 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf' | |||
| let file | |||
| if(loadAsFile){ | |||
| file = new File([await (await fetch(url)).blob()], url) | |||
| } | |||
| const load = async ()=> { | |||
| return viewer.load(file ?? url, { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| } | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| viewer.scene.background = viewer.scene.environment | |||
| const model = await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| const model = await load() | |||
| console.log(model.uuid) | |||
| await timeout(500) | |||
| viewer.scene.clearSceneModels(false) | |||
| await timeout(500) | |||
| const model2 = await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| const model2 = await load() | |||
| console.log(model2.uuid) | |||
| if(model !== model2) throw new Error('Error in Test - Models should be the same after clearing scene models'); | |||
| await timeout(500) | |||
| viewer.scene.disposeSceneModels() | |||
| await timeout(500) | |||
| const model3 = await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| const model3 = await load() | |||
| console.log(model3.uuid) | |||
| if(model2 === model3) throw new Error('Error in Test - Models should not be the same after disposing scene models'); | |||
| await timeout(500) | |||
| @@ -64,10 +66,12 @@ | |||
| viewer.scene.addObject(model.translateX(-0.5)) // add back cleared model | |||
| viewer.scene.addObject(model2.translateX(-0.5)) // again, shouldn't change anything | |||
| viewer.scene.addObject(model3.translateX(1)) // add back disposed model | |||
| await timeout(1000) | |||
| viewer.scene.disposeSceneModels() | |||
| } | |||
| _testStart() | |||
| init().finally(_testFinish) | |||
| init(false).then(()=>init(true)).finally(_testFinish) | |||
| </script> | |||
| </head> | |||
| <body> | |||
| @@ -0,0 +1,36 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>File/Blob Load</title> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <!-- Import maps polyfill --> | |||
| <!-- Remove this when import maps will be widely supported --> | |||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||
| <script type="importmap"> | |||
| { | |||
| "imports": { | |||
| "threepipe": "./../../dist/index.mjs" | |||
| } | |||
| } | |||
| </script> | |||
| <style id="example-style"> | |||
| html, body, #canvas-container, #mcanvas { | |||
| width: 100%; | |||
| height: 100%; | |||
| margin: 0; | |||
| overflow: hidden; | |||
| } | |||
| </style> | |||
| <script type="module" src="../examples-utils/global-loading.mjs"></script> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,41 @@ | |||
| import {_testFinish, _testStart, LoadingScreenPlugin, ThreeViewer} from 'threepipe' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: true, | |||
| dropzone: { | |||
| addOptions: { | |||
| disposeSceneObjects: true, | |||
| autoSetEnvironment: true, // when hdr is dropped | |||
| autoSetBackground: true, | |||
| }, | |||
| }, | |||
| }) | |||
| viewer.addPluginSync(LoadingScreenPlugin) | |||
| const env = 'https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr' | |||
| const url = 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf' | |||
| const responses = await Promise.all([ | |||
| fetch(env), | |||
| fetch(url), | |||
| ]) | |||
| const envFile = new File([await responses[0].blob()], 'venice_sunset_1k.hdr') | |||
| await viewer.setEnvironmentMap(envFile, { | |||
| setBackground: true, | |||
| }) | |||
| const blob = await responses[1].blob() | |||
| const file = new File([blob], url) // Set the file name to the URL, so that internal textures can be resolved correctly from the base path | |||
| const result = await viewer.load(file, { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| console.log(result) | |||
| } | |||
| _testStart() | |||
| init().finally(_testFinish) | |||
| @@ -454,6 +454,7 @@ | |||
| <li><a href="./slippy-map-tiles/">Slippy Map Tiles Load<br/>(OpenStreetMap ZYX) </a></li> | |||
| <li><a href="./ogc-tiles-google-maps/">Google Maps Globe (OGC Tiles)</a></li> | |||
| <li><a href="./ogc-tiles-google-maps-3d/">Google Maps 3D (OGC Tiles)</a></li> | |||
| <li><a href="./file-load/">File/Blob load</a></li> | |||
| </ul> | |||
| <h2 class="category">Export</h2> | |||
| <ul> | |||
| @@ -546,7 +547,7 @@ | |||
| <li><a href="./uint8-rgbm-hdr-test/">Uint8 RGBM HDR Test </a></li> | |||
| <li><a href="./half-float-hdr-test/">Half-float HDR Test </a></li> | |||
| <li><a href="./sphere-rgbm-test/">RGBM Test </a></li> | |||
| <li><a href="./sphere-half-float-test/">Half Float Test </a></li> | |||
| <li><a href="./sphere-half-float-test/">Half-float Test </a></li> | |||
| <li><a href="./sphere-msaa-test/">MSAA Test </a></li> | |||
| <li><a href="./z-prepass/">Z-Prepass Test </a></li> | |||
| <li><a href="./import-test/">Import Test</a></li> | |||
| @@ -6,14 +6,14 @@ import { | |||
| ImportFilesOptions, | |||
| ImportResult, | |||
| LoadFileOptions, | |||
| ProcessRawOptions, RootSceneImportResult, | |||
| ProcessRawOptions, | |||
| RootSceneImportResult, | |||
| } from './IAssetImporter' | |||
| import {IAsset, IFile} from './IAsset' | |||
| import {IImporter, ILoader} from './IImporter' | |||
| import {Importer} from './Importer' | |||
| import {SimpleJSONLoader} from './import' | |||
| import {parseFileExtension} from 'ts-browser-helpers' | |||
| import {IObject3D} from '../core' | |||
| // export type IAssetImporterEvent = Event&{ | |||
| // type: IAssetImporterEventTypes, | |||
| @@ -126,7 +126,7 @@ export class AssetImporter extends EventDispatcher<IAssetImporterEventMap> imple | |||
| console.error('AssetImporter: Invalid asset or path', assetOrPath) | |||
| return [] | |||
| } | |||
| async importSingle<T extends ImportResult|undefined = ImportResult>(asset?: IAsset | string, options?: ImportAssetOptions): Promise<T|undefined> { | |||
| async importSingle<T extends ImportResult|undefined = ImportResult>(asset?: string | IAsset | File, options?: ImportAssetOptions): Promise<T|undefined> { | |||
| return (await this.import<T>(asset, options))?.[0] | |||
| } | |||
| @@ -283,7 +283,7 @@ export class AssetImporter extends EventDispatcher<IAssetImporterEventMap> imple | |||
| // load a single file | |||
| private async _loadFile(path: string, file?: IFile, options: LoadFileOptions = {}, onDownloadProgress?: (e: ProgressEvent)=>void): Promise<ImportResult | ImportResult[] | undefined> { | |||
| if (file?.__loadedAsset) return file.__loadedAsset | |||
| // if (file?.__loadedAsset) return file.__loadedAsset | |||
| this.dispatchEvent({type: 'importFile', path, state:'downloading', progress: 0}) | |||
| let res: ImportResult | ImportResult[] | undefined | |||
| @@ -343,21 +343,21 @@ export class AssetImporter extends EventDispatcher<IAssetImporterEventMap> imple | |||
| return [] | |||
| } | |||
| this.dispatchEvent({type: 'importFile', path, state: 'done'}) // todo: do this after processing? | |||
| if (file) { | |||
| file.__loadedAsset = res | |||
| // todo: recheck below code after dispose logic change | |||
| // Clear the reference __loadedAsset when any one asset is disposed. | |||
| // it's a bit hacky to do this here, but it works for now. todo: move to a better place | |||
| let ress: any[] = [] | |||
| if (Array.isArray(res)) ress = res.flat(2) | |||
| else if ((<RootSceneImportResult>res)?.userData?.rootSceneModelRoot) ress.push(...(<IObject3D>res).children) | |||
| else ress.push(res) | |||
| for (const r of ress) r?.addEventListener?.('dispose', () => file.__loadedAsset = undefined) | |||
| } | |||
| // if (file) { | |||
| // file.__loadedAsset = res | |||
| // | |||
| // | |||
| // // todo: recheck below code after dispose logic change | |||
| // | |||
| // // Clear the reference __loadedAsset when any one asset is disposed. | |||
| // // it's a bit hacky to do this here, but it works for now. todo: move to a better place | |||
| // let ress: any[] = [] | |||
| // if (Array.isArray(res)) ress = res.flat(2) | |||
| // else if ((<RootSceneImportResult>res)?.userData?.rootSceneModelRoot) ress.push(...(<IObject3D>res).children) | |||
| // else ress.push(res) | |||
| // for (const r of ress) r?.addEventListener?.('dispose', () => file.__loadedAsset = undefined) | |||
| // | |||
| // } | |||
| if (res && typeof res === 'object' && !Array.isArray(res)) { | |||
| res.__rootPath = path | |||
| const f = file || this._fileDatabase.get(path) | |||
| @@ -513,7 +513,7 @@ export class ThreeViewer extends EventDispatcher<Record<IViewerEventTypes, IView | |||
| * @param obj | |||
| * @param options | |||
| */ | |||
| async import<T extends ImportResult = ImportResult>(obj: string | IAsset | null, options?: ImportAddOptions) { | |||
| async import<T extends ImportResult = ImportResult>(obj: string | IAsset | File | null, options?: ImportAddOptions) { | |||
| if (!obj) return | |||
| return await this.assetManager.importer.importSingle<T>(obj, options) | |||
| } | |||
| @@ -524,8 +524,8 @@ export class ThreeViewer extends EventDispatcher<Record<IViewerEventTypes, IView | |||
| * @param setBackground - Set the background image of the scene from the same map. | |||
| * @param options - Options for importing the asset. See {@link ImportAssetOptions} | |||
| */ | |||
| async setEnvironmentMap(map: string | IAsset | null | ITexture | undefined, {setBackground = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||
| this._scene.environment = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null | |||
| async setEnvironmentMap(map: string | IAsset | null | ITexture | File | undefined, {setBackground = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||
| this._scene.environment = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset|File, options) || null : <ITexture>map || null | |||
| if (setBackground) return this.setBackgroundMap(this._scene.environment) | |||
| return this._scene.environment | |||
| } | |||
| @@ -536,7 +536,7 @@ export class ThreeViewer extends EventDispatcher<Record<IViewerEventTypes, IView | |||
| * @param setEnvironment - Set the environment map of the scene from the same map. | |||
| * @param options - Options for importing the asset. See {@link ImportAssetOptions} | |||
| */ | |||
| async setBackgroundMap(map: string | IAsset | null | ITexture | undefined, {setEnvironment = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||
| async setBackgroundMap(map: string | IAsset | null | ITexture | File | undefined, {setEnvironment = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||
| this._scene.background = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null | |||
| if (setEnvironment) return this.setEnvironmentMap(this._scene.background) | |||
| return this._scene.background | |||
| @@ -14,8 +14,12 @@ ThreePipe uses the [AssetManager](https://threepipe.org/docs/classes/AssetManage | |||
| The AssetManager has support for loading files from URLs, local files and data URLs. | |||
| The AssetManager also adds support for loading files from a zip archive. The zip files are automatically unzipped, and the files are loaded from the zip archive. | |||
| [`viewer.load()`](https://threepipe.org/docs/classes/ThreeViewer.html#load) is a simple wrapper for loading files from the AssetManager. | |||
| It automatically adds the loaded object to the scene(if possible) and returns a promise that resolves to the loaded object, the materials are also automatically registered to the material manager. | |||
| [`viewer.load()`](https://threepipe.org/docs/classes/ThreeViewer.html#load) is a simple helper for loading files from the AssetManager. It accepts urls, local `File`/`Blob`, data URLs, zip files, `IAsset`. | |||
| It automatically adds the loaded object to the scene(if possible) and returns a promise that resolves to the loaded object/texture/material/etc, the materials are also automatically registered to the material manager. | |||
| ```typescript | |||
| const object = await viewer.load<IObject3D>('https://example.com/file.glb') | |||
| ``` | |||
| ::: details AssetManager | |||
| AssetManager internally uses [AssetImporter](https://threepipe.org/docs/classes/AssetImporter.html), which provides an API for managing three.js [LoadingManager](https://threejs.org/docs/#api/en/loaders/LoadingManager) and adding and registering loaders for different file types. | |||
| @@ -185,7 +189,8 @@ Local files can be loaded using the `viewer.load` method by passing a [IAsset](h | |||
| ```typescript | |||
| const file: File|Blob = fileObject // create a new file, blob or get from input element | |||
| const text = await viewer.load<IObject>({ | |||
| const res = await viewer.load(file) | |||
| const res2 = await viewer.load<IObject>({ | |||
| // a path/name is required to determine the proper importer by extension. `file.name` can also be used if available | |||
| path: 'file.glb', | |||
| file | |||
| @@ -195,6 +200,22 @@ The same can be done for any file type. | |||
| To load a `Map` of files(like when multiple files are dragged and dropped on the webpage) with internal references to other files, use `viewer.assetManager.importer.importFiles` method. Check the source for [DropzonePlugin](../plugin/DropzonePlugin) for an example. | |||
| ## File/Blob with URL | |||
| Files with references can be loaded with path, by setting the path as the name of the `File` object, or by specifying the `path` parameter. | |||
| ```typescript | |||
| const url = 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf' | |||
| const blob = await fetch(url).then(res => res.blob()) | |||
| const file = new File([blob], url) // Set the file name to the URL, so that internal textures can be resolved correctly from the base path | |||
| const result = await viewer.load(file, { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| ``` | |||
| Check the complete example - [file-load](https://threepipe.org/examples/#file-load/) | |||
| ## Background, Environment maps | |||
| The background and environment maps can be set using the `viewer.setBackgroundMap` and `viewer.setEnvironmentMap` methods respectively. These accept both loaded textures from `viewer.load` and direct URLs. Files can be of any image format including hdr, exr. | |||