| @@ -1,6 +1,6 @@ | |||
| dist | |||
| docs | |||
| ./index.html | |||
| /index.html | |||
| examples/**/*.js | |||
| examples/**/*.js.map | |||
| @@ -0,0 +1,44 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Dropzone Plugin</title> | |||
| <!-- 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; | |||
| } | |||
| #prompt-div{ | |||
| position: absolute; | |||
| top: 50%; | |||
| left: 50%; | |||
| transform: translate(-50%, -50%); | |||
| color: #333; | |||
| font-size: 2em; | |||
| font-family: sans-serif; | |||
| } | |||
| </style> | |||
| <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 id="prompt-div">Drop any <span style="font-family: monospace">glb/hdr/png/jpg</span> files here</div> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,34 @@ | |||
| import {_testFinish, DropzonePlugin, ThreeViewer} from 'threepipe' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| dropzone: { // this can also be set to true and configured by getting a reference to the DropzonePlugin | |||
| allowedExtensions: ['gltf', 'glb', 'hdr', 'png', 'jpg', 'json', 'fbx', 'obj'], // only allow these file types. If undefined, all files are allowed. | |||
| addOptions: { | |||
| disposeSceneObjects: true, // auto dispose of old scene objects | |||
| autoSetEnvironment: true, // when hdr is dropped | |||
| autoSetBackground: true, // when any image is dropped | |||
| autoCenter: true, // auto center the object | |||
| autoScale: true, // auto scale according to radius | |||
| autoScaleRadius: 2, | |||
| license: 'Imported from dropzone', // Any license to set on imported objects | |||
| importConfig: true, // import config from file | |||
| }, | |||
| }, | |||
| }) | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| const dropzone = viewer.getPlugin(DropzonePlugin) | |||
| dropzone?.addEventListener('drop', (e: any) => { | |||
| if (!e.assets?.length) return // no assets imported | |||
| console.log('Dropped Event:', e) | |||
| const promptDiv = document.getElementById('prompt-div')! | |||
| promptDiv.style.display = 'none' | |||
| }) | |||
| } | |||
| init().then(_testFinish) | |||
| @@ -1,10 +1,23 @@ | |||
| import {_testFinish, ThreeViewer} from 'threepipe' | |||
| const viewer = new ThreeViewer({canvas: document.getElementById('mcanvas') as HTMLCanvasElement, msaa: true}) | |||
| async function init() { | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: true, | |||
| dropzone: { | |||
| allowedExtensions: ['gltf', 'glb', 'hdr'], | |||
| addOptions: { | |||
| disposeSceneObjects: true, | |||
| autoSetEnvironment: true, // when hdr is dropped | |||
| autoSetBackground: true, | |||
| }, | |||
| }, | |||
| }) | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', { | |||
| setBackground: true, | |||
| }) | |||
| const result = await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| @@ -203,6 +203,7 @@ | |||
| <ul> | |||
| <li><a href="./depth-buffer-plugin">Depth Buffer Plugin </a></li> | |||
| <li><a href="./render-target-preview">Render Target Preview </a></li> | |||
| <li><a href="./dropzone-plugin">Dropzone (Drag & Drop) </a></li> | |||
| </ul> | |||
| <h2 class="category">Import/Export</h2> | |||
| <ul> | |||
| @@ -583,4 +583,4 @@ class MaterialCreator { | |||
| } | |||
| export {MTLLoader2}; | |||
| export {MTLLoader2, type MaterialCreator}; | |||
| @@ -1,6 +1,6 @@ | |||
| export {JSONMaterialLoader} from './JSONMaterialLoader' | |||
| export {SimpleJSONLoader} from './SimpleJSONLoader' | |||
| export {MTLLoader2} from './MTLLoader2' | |||
| export {MTLLoader2, type MaterialCreator} from './MTLLoader2' | |||
| export {OBJLoader2} from './OBJLoader2' | |||
| export {ZipLoader} from './ZipLoader' | |||
| export {GLTFLoader2} from './GLTFLoader2' | |||
| @@ -6,14 +6,47 @@ import {Box3B} from '../three/math/Box3B' | |||
| import {ITexture} from './ITexture' | |||
| export interface AddObjectOptions { | |||
| addToRoot?: boolean // default = false | |||
| // TODO; add more options | |||
| /** | |||
| * Add directly to the {@link RootScene} object instead of {@link RootScene.modelRoot} | |||
| * @default false | |||
| */ | |||
| addToRoot?: boolean | |||
| /** | |||
| * Automatically center the object in the scene. | |||
| * @default false | |||
| */ | |||
| autoCenter?: boolean, | |||
| /** | |||
| * Add a license to the object | |||
| */ | |||
| license?: string, | |||
| /** | |||
| * Automatically scale the object according to its bounding box and the {@link autoScaleRadius} setting | |||
| * @default false | |||
| */ | |||
| autoScale?: boolean | |||
| autoScaleRadius?: number // default = 2 | |||
| /** | |||
| * Radius to use for {@link autoScale} | |||
| * @default 2 | |||
| */ | |||
| autoScaleRadius?: number | |||
| /** | |||
| * any attached viewer config will be ignored if this is set to true | |||
| * @default true | |||
| */ | |||
| importConfig?: boolean | |||
| /** | |||
| * Clear the viewer scene objects before the new object is added. Same as {@link disposeSceneObjects} but does not dispose the objects. | |||
| */ | |||
| clearSceneObjects?: boolean | |||
| /** | |||
| * Dispose all the scene objects before the new object is added. Same as {@link clearSceneObjects} but also disposes the objects. | |||
| */ | |||
| disposeSceneObjects?: boolean | |||
| importConfig?: boolean // any attached viewer config will be ignored if this is set to true | |||
| // TODO; add more options | |||
| } | |||
| // | string | |||
| @@ -30,6 +63,7 @@ export interface ISceneEvent<T extends string = ISceneEventTypes> extends IObjec | |||
| // change?: string | |||
| } | |||
| export type ISceneSetDirtyOptions = IObjectSetDirtyOptions & { | |||
| [key: string]: any | |||
| } | |||
| @@ -146,6 +146,9 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| * @param options | |||
| */ | |||
| addObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T { | |||
| if (options?.clearSceneObjects || options?.disposeSceneObjects) { | |||
| this.clearSceneModels(options.disposeSceneObjects) | |||
| } | |||
| if (!imported) return imported | |||
| if (!imported.isObject3D) { | |||
| console.error('Invalid object, cannot add to scene.', imported) | |||
| @@ -162,6 +165,9 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| * @param options | |||
| */ | |||
| loadModelRoot(obj: RootSceneImportResult, options?: AddObjectOptions) { | |||
| if (options?.clearSceneObjects || options?.disposeSceneObjects) { | |||
| this.clearSceneModels(options.disposeSceneObjects) | |||
| } | |||
| if (!obj.userData?.rootSceneModelRoot) { | |||
| console.error('Invalid model root scene object. Trying to add anyway.', obj) | |||
| } | |||
| @@ -221,11 +227,11 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| this.setDirty({refreshScene: true}) | |||
| } | |||
| clearSceneModels(dispose = false): void { | |||
| if (dispose) this.disposeSceneModels() | |||
| clearSceneModels(dispose = false, setDirty = true): void { | |||
| if (dispose) return this.disposeSceneModels(setDirty) | |||
| this.modelRoot.clear() | |||
| this.modelRoot.children = [] | |||
| this.setDirty({refreshScene: true}) | |||
| setDirty && this.setDirty({refreshScene: true}) | |||
| } | |||
| disposeSceneModels(setDirty = true) { | |||
| @@ -261,7 +267,7 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| public backgroundColor: Color | null = null // read in three.js WebGLBackground | |||
| /** | |||
| * @deprecated Use addSceneObject | |||
| * @deprecated Use {@link addObject} | |||
| */ | |||
| add(...object: Object3D[]): this { | |||
| super.add(...object) | |||
| @@ -2,3 +2,4 @@ export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin' | |||
| export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin' | |||
| export {PipelinePassPlugin} from './base/PipelinePassPlugin' | |||
| export {RenderTargetPreviewPlugin} from './ui/RenderTargetPreviewPlugin' | |||
| export {DropzonePlugin, type DropzonePluginOptions} from './interaction/DropzonePlugin' | |||
| @@ -0,0 +1,135 @@ | |||
| import {AViewerPluginSync} from '../../viewer/AViewerPlugin' | |||
| import {type ThreeViewer} from '../../viewer/' | |||
| import {Dropzone} from '../../utils' | |||
| import {uiButton, uiConfig, uiFolderContainer, uiToggle} from 'uiconfig.js' | |||
| import type {AddAssetOptions, ImportFilesOptions, ImportResult} from '../../assetmanager' | |||
| export interface DropzonePluginOptions { | |||
| domElement?: HTMLElement | |||
| allowedExtensions?: string[] | |||
| autoImport?: boolean | |||
| autoAdd?: boolean | |||
| importOptions?: ImportFilesOptions | |||
| addOptions?: AddAssetOptions | |||
| } | |||
| @uiFolderContainer('Dropzone') | |||
| export class DropzonePlugin extends AViewerPluginSync<'drop'> { | |||
| static readonly PluginType = 'Dropzone' | |||
| @uiToggle() enabled = true | |||
| private _inputEl?: HTMLInputElement | |||
| private _dropzone?: Dropzone | |||
| private _allowedExtensions: string[]|undefined = undefined // undefined and empty array is different. | |||
| /** | |||
| * Automatically import assets when dropped. | |||
| */ | |||
| autoImport = true | |||
| /** | |||
| * Automatically add dropped and imported assets to the scene. | |||
| * Works only if {@link autoImport} is true. | |||
| */ | |||
| @uiToggle() autoAdd = true | |||
| /** | |||
| * Import options for the {@link AssetImporter.importFiles} | |||
| */ | |||
| @uiConfig() importOptions: ImportFilesOptions = { | |||
| autoImportZipContents: true, | |||
| forceImporterReprocess: false, | |||
| } | |||
| /** | |||
| * Add options for the {@link RootScene.addObject} | |||
| */ | |||
| @uiConfig() addOptions: AddAssetOptions = { | |||
| autoCenter: true, | |||
| importConfig: true, | |||
| autoScale: true, | |||
| autoScaleRadius: 2, | |||
| license: '', | |||
| clearSceneObjects: false, | |||
| disposeSceneObjects: false, | |||
| autoSetBackground: false, | |||
| autoSetEnvironment: true, | |||
| } | |||
| /** | |||
| * Allowed file extensions. If undefined, all files are allowed. | |||
| */ | |||
| get allowedExtensions(): string[] | undefined { | |||
| return this._allowedExtensions | |||
| } | |||
| set allowedExtensions(value: string[] | undefined) { | |||
| this._allowedExtensions = value | |||
| if (this._inputEl) this._inputEl.accept = value ? value.map(v=>'.' + v).join(', ') : '' | |||
| } | |||
| /** | |||
| * Prompt for file selection using the browser file dialog. | |||
| */ | |||
| @uiButton('Select files') | |||
| public promptForFile(): void { | |||
| if (!this.enabled) return | |||
| this.allowedExtensions = this._allowedExtensions | |||
| this._inputEl?.click() | |||
| } | |||
| private _domElement?: HTMLElement | |||
| constructor(options?: DropzonePluginOptions) { | |||
| super() | |||
| if (!options) return | |||
| this._domElement = options.domElement | |||
| this.allowedExtensions = options.allowedExtensions | |||
| this.autoImport = options.autoImport ?? this.autoImport | |||
| this.autoAdd = options.autoAdd ?? this.autoAdd | |||
| this.importOptions = {...this.importOptions, ...options.importOptions} | |||
| this.addOptions = {...this.addOptions, ...options.addOptions} | |||
| } | |||
| onAdded(viewer: ThreeViewer) { | |||
| super.onAdded(viewer) | |||
| this._inputEl = document.createElement('input')! | |||
| this._inputEl.type = 'file' | |||
| this._dropzone = new Dropzone(this._domElement || viewer.canvas, this._inputEl, { | |||
| drop: this._onFileDrop.bind(this), | |||
| }) | |||
| this.allowedExtensions = this._allowedExtensions | |||
| } | |||
| onRemove(viewer: ThreeViewer) { | |||
| super.onRemove(viewer) | |||
| this._dropzone?.destroy() | |||
| this._dropzone = undefined | |||
| this._inputEl = undefined | |||
| } | |||
| private async _onFileDrop({files}: {files: Map<string, File>}&any) { | |||
| if (!files) return | |||
| if (!this.enabled) return | |||
| const viewer = this._viewer | |||
| if (!viewer) return | |||
| if (this._allowedExtensions !== undefined) { | |||
| for (const file of files.keys()) { | |||
| if (!this._allowedExtensions.includes(file.split('.').pop()?.toLowerCase() ?? '')) { | |||
| files.delete(file) | |||
| } | |||
| } | |||
| } | |||
| if (files.size < 1) return | |||
| const manager = viewer.assetManager | |||
| let imported: Map<string, (ImportResult | undefined)[]>|undefined | |||
| let assets: (ImportResult | undefined)[]|undefined | |||
| if (this.autoImport) { | |||
| imported = await manager.importer.importFiles(files, { | |||
| allowedExtensions: this.allowedExtensions, ...this.importOptions, | |||
| }) | |||
| if (this.autoAdd) { | |||
| const toAdd = [...imported?.values() ?? []].flat(2).filter(v=>!!v) ?? [] | |||
| assets = await manager.loadImported(toAdd, {...this.addOptions}) | |||
| } | |||
| } | |||
| this.dispatchEvent({type: 'drop', files, imported, assets}) | |||
| } | |||
| } | |||
| @@ -0,0 +1,242 @@ | |||
| /** | |||
| * Fork of: https://github.com/donmccurdy/simple-dropzone updated: Mar 2021 | |||
| * The MIT License (MIT) | |||
| * Copyright (c) 2018 Don McCurdy | |||
| * | |||
| * Changes: | |||
| * Convert to typescript. | |||
| * webkitRelativePath for file input select. | |||
| * Removed unzip and dependency(done in importer). | |||
| * | |||
| * Watches an element for file drops, parses to create a filemap hierarchy, | |||
| * and emits the result. | |||
| */ | |||
| export class Dropzone { | |||
| get inputEl(): HTMLInputElement|undefined { | |||
| return this._inputEl | |||
| } | |||
| get el(): HTMLElement|undefined { | |||
| return this._el | |||
| } | |||
| private _el?: HTMLElement | |||
| private _inputEl?: HTMLInputElement | |||
| private _listeners: Record<DropEventType, ListenerCallback[]> | |||
| constructor(el?: HTMLElement, inputEl?: HTMLInputElement, listeners?: Partial<Record<DropEventType, ListenerCallback>>) { | |||
| this._el = el | |||
| this._inputEl = inputEl | |||
| this._listeners = { | |||
| drop: [], | |||
| dropstart: [], | |||
| droperror: [], | |||
| } | |||
| this._onDragover = this._onDragover.bind(this) | |||
| this._onDrop = this._onDrop.bind(this) | |||
| this._onSelect = this._onSelect.bind(this) | |||
| el?.addEventListener('dragover', this._onDragover, false) | |||
| el?.addEventListener('drop', this._onDrop, false) | |||
| inputEl?.addEventListener('change', this._onSelect) | |||
| listeners && Object.entries(listeners).forEach(([e, c])=> c && this.on(e as DropEventType, c)) | |||
| } | |||
| on(type: DropEventType, callback: ListenerCallback): Dropzone { | |||
| this._listeners[type].push(callback) | |||
| return this | |||
| } | |||
| private _emit(type: DropEventType, data?: {[id:string]: any}) { | |||
| this._listeners[type] | |||
| .forEach((callback) => callback(data)) | |||
| return this | |||
| } | |||
| /** | |||
| * Destroys the instance. | |||
| */ | |||
| destroy(): void { | |||
| const el = this._el | |||
| const inputEl = this._inputEl | |||
| el?.removeEventListener('dragover', this._onDragover) | |||
| el?.removeEventListener('drop', this._onDrop) | |||
| inputEl?.removeEventListener('change', this._onSelect) | |||
| } | |||
| /** | |||
| * References (and horror): | |||
| * - https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/items | |||
| * - https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/files | |||
| * - https://code.flickr.net/2012/12/10/drag-n-drop/ | |||
| * - https://stackoverflow.com/q/44842247/1314762 | |||
| * | |||
| */ | |||
| private _onDrop(e: DragEvent) { | |||
| e.stopPropagation() | |||
| e.preventDefault() | |||
| this._emit('dropstart') | |||
| const files = Array.from(e.dataTransfer?.files || []) as DropFile[] | |||
| const items = Array.from(e.dataTransfer?.items || []) | |||
| if (files.length === 0 && items.length === 0) { | |||
| this._fail('Required drag-and-drop APIs are not supported in this browser.') | |||
| return | |||
| } | |||
| // Prefer .items, which allow folder traversal if necessary. | |||
| if (items.length > 0) { | |||
| const entries = items.map((item) => item.webkitGetAsEntry()) | |||
| // if (entries[0].name.match(/\.zip$/)) { | |||
| // this._loadZip(items[0].getAsFile()) | |||
| // } else { | |||
| this._loadNextEntry(new Map(), entries) | |||
| // } | |||
| return | |||
| } | |||
| // Fall back to .files, since folders can't be traversed. | |||
| // if (files.length === 1 && files[0].name.match(/\.zip$/)) { | |||
| // this._loadZip(files[0]) | |||
| // } | |||
| this._emit('drop', {files: new Map(files.map((file) => { | |||
| file.filePath = file.name | |||
| return [file.filePath, file] | |||
| }))}) | |||
| } | |||
| /** | |||
| * @param {Event} e | |||
| */ | |||
| private _onDragover(e: DragEvent) { | |||
| e.stopPropagation() | |||
| e.preventDefault() | |||
| e.dataTransfer && (e.dataTransfer.dropEffect = 'copy') // Explicitly show this is a copy. | |||
| } | |||
| private _onSelect(e: Event) { | |||
| if (!this._inputEl) { | |||
| console.warn('Invalid Dropzone event ', e) | |||
| return | |||
| } | |||
| this._emit('dropstart') | |||
| // HTML file inputs do not seem to support folders, so assume this is a flat file list. | |||
| const files: DropFile[] = [].slice.call(this._inputEl.files ?? new FileList()) | |||
| // Automatically decompress a zip archive if it is the only file given. | |||
| // if (files.length === 1 && this._isZip(files[0])) { | |||
| // this._loadZip(files[0]) | |||
| // return | |||
| // } | |||
| const fileMap = new Map() | |||
| files.forEach((file) => { | |||
| file.filePath = (file as any).webkitRelativePath || file.name | |||
| fileMap.set(file.filePath, file) | |||
| }) | |||
| this._emit('drop', {files: fileMap}) | |||
| } | |||
| /** | |||
| * Iterates through a list of FileSystemEntry objects, creates the fileMap | |||
| * tree, and emits the result. | |||
| * @param {Array<FileSystemEntry>} entries | |||
| */ | |||
| private _loadNextEntry(fileMap: Map<string, DropFile>, entries: any[]) { | |||
| const entry = entries.pop() | |||
| if (!entry) { | |||
| this._emit('drop', {files: fileMap}) | |||
| return | |||
| } | |||
| if (entry.isFile) { | |||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call | |||
| entry.file((file: DropFile) => { | |||
| file.filePath = entry.fullPath | |||
| fileMap.set(entry.fullPath, file) | |||
| this._loadNextEntry(fileMap, entries) | |||
| }, () => console.error('Could not load file: %s', entry.fullPath)) | |||
| } else if (entry.isDirectory) { | |||
| // readEntries() must be called repeatedly until it stops returning results. | |||
| // https://www.w3.org/TR/2012/WD-file-system-api-20120417/#the-directoryreader-interface | |||
| // https://bugs.chromium.org/p/chromium/issues/detail?id=378883 | |||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call | |||
| const reader = entry.createReader() | |||
| const readerCallback = (newEntries: any[]) => { | |||
| if (newEntries.length) { | |||
| entries = entries.concat(newEntries) | |||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call | |||
| reader.readEntries(readerCallback) | |||
| } else { | |||
| this._loadNextEntry(fileMap, entries) | |||
| } | |||
| } | |||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call | |||
| reader.readEntries(readerCallback) | |||
| } else { | |||
| console.warn('Unknown asset type: ' + entry.fullPath) | |||
| this._loadNextEntry(fileMap, entries) | |||
| } | |||
| } | |||
| // /** | |||
| // * Inflates a File in .ZIP format, creates the fileMap tree, and emits the | |||
| // * result. | |||
| // * @param {File} file | |||
| // */ | |||
| // _loadZip(file) { | |||
| // const pending = [] | |||
| // const fileMap = new Map() | |||
| // const archive = new fs.FS() | |||
| // | |||
| // const traverse = (node) => { | |||
| // if (node.directory) { | |||
| // node.children.forEach(traverse) | |||
| // } else if (node.name[0] !== '.') { | |||
| // pending.push(new Promise((resolve) => { | |||
| // node.getData(new zip.BlobWriter(), (blob) => { | |||
| // blob.name = node.name | |||
| // fileMap.set(node.getFullname(), blob) | |||
| // resolve() | |||
| // }) | |||
| // })) | |||
| // } | |||
| // } | |||
| // | |||
| // archive.importBlob(file, () => { | |||
| // traverse(archive.root) | |||
| // Promise.all(pending).then(() => { | |||
| // this._emit('drop', {files: fileMap, archive: file}) | |||
| // }) | |||
| // }) | |||
| // } | |||
| // /** | |||
| // * @param {File} file | |||
| // * @return {Boolean} | |||
| // */ | |||
| // _isZip(file) { | |||
| // return file.type === 'application/zip' || file.name.match(/\.zip$/) | |||
| // } | |||
| /** | |||
| * @throws | |||
| */ | |||
| private _fail(message: string) { | |||
| this._emit('droperror', {message: message}) | |||
| } | |||
| } | |||
| export type DropEventType = 'drop'|'dropstart'|'droperror' | |||
| export type ListenerCallback = ((data?:{files?:Map<string, DropFile>, message?:string})=>void) | |||
| export interface DropFile extends File{ | |||
| filePath: string | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| export * from './browser-helpers' | |||
| export {windowDialogWrapper, type IDialogWrapper} from './DialogWrapper' | |||
| export {GLStatsJS} from './GLStatsJS' | |||
| export {Dropzone, type DropFile, type ListenerCallback, type DropEventType} from './Dropzone' | |||
| export {ThreeSerialization, type SerializationMetaType, type SerializationResourcesType, MetaImporter} from './serialization' | |||
| export {shaderReplaceString} from './shader-helpers' | |||
| @@ -34,6 +34,7 @@ import { | |||
| } from '../assetmanager' | |||
| import {GLStatsJS, IDialogWrapper, windowDialogWrapper} from '../utils' | |||
| import {IViewerPlugin, IViewerPluginSync} from './IViewerPlugin' | |||
| import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin' | |||
| export type IViewerEvent = BaseEvent & { | |||
| type: 'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin' | |||
| @@ -66,7 +67,7 @@ export type IConsoleWrapper = Partial<Console> & Pick<Console, 'log'|'warn'|'err | |||
| * Options for the ThreeViewer creation. | |||
| * @category Viewer | |||
| */ | |||
| export interface ThreeViewerOptions extends AssetManagerOptions{ | |||
| export interface ThreeViewerOptions { | |||
| /** | |||
| * The canvas element to use for rendering. Only one of container and canvas must be specified. | |||
| */ | |||
| @@ -95,6 +96,15 @@ export interface ThreeViewerOptions extends AssetManagerOptions{ | |||
| debug?: boolean | |||
| assetManager?: AssetManagerOptions | |||
| /** | |||
| * Add the dropzone plugin to the viewer, allowing to drag and drop files into the viewer over the canvas/container. | |||
| * Set to true/false to enable/disable the plugin, or pass options to configure the plugin. Assuming true if options are passed. | |||
| * @default - false | |||
| */ | |||
| dropzone?: boolean|DropzonePluginOptions | |||
| /** | |||
| * @deprecated use {@link msaa} instead | |||
| */ | |||
| @@ -266,7 +276,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| this.setDirty(this.renderManager, e) | |||
| }) | |||
| this.assetManager = new AssetManager(this, options) | |||
| this.assetManager = new AssetManager(this, options.assetManager) | |||
| if (this.resizeObserver) this.resizeObserver.observe(this._canvas) | |||
| // sometimes resize observer is late, so extra check | |||
| @@ -275,7 +285,13 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| this._canvas.addEventListener('webglcontextrestored', this._onContextRestore, false) | |||
| this._canvas.addEventListener('webglcontextlost', this._onContextLost, false) | |||
| if (options.dropzone) { | |||
| this.addPluginSync(new DropzonePlugin(typeof options.dropzone === 'object' ? options.dropzone : undefined)) | |||
| } | |||
| this.console.log('ThreePipe Viewer instance initialized, version: ', ThreeViewer.VERSION) | |||
| } | |||
| private _objectProcessor: IObjectProcessor = { | |||
| @@ -347,19 +363,23 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| /** | |||
| * Set the environment map of the scene from url or an {@link IAsset} object. | |||
| * @param map | |||
| * @param options | |||
| * @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, options?: ImportAssetOptions): Promise<void> { | |||
| this._scene.environment = map ? await this.assetManager.importer.importSingle<ITexture>(map, options) || null : null | |||
| async setEnvironmentMap(map: string | IAsset | null | ITexture, {setBackground = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<void> { | |||
| this._scene.environment = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null | |||
| if (setBackground) return this.setBackgroundMap(this._scene.environment) | |||
| } | |||
| /** | |||
| * Set the background image of the scene from url or an {@link IAsset} object. | |||
| * @param map | |||
| * @param options | |||
| * @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, options?: ImportAssetOptions): Promise<void> { | |||
| this._scene.background = map ? await this.assetManager.importer.importSingle<ITexture>(map, options) || null : null | |||
| async setBackgroundMap(map: string | IAsset | null | ITexture, {setEnvironment = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<void> { | |||
| 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) | |||
| } | |||
| /** | |||
| @@ -757,7 +777,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| * Deserialize and import all the viewer and plugin settings, exported with {@link exportConfig}. | |||
| */ | |||
| async importConfig(json: ISerializedConfig|ISerializedViewerConfig) { | |||
| if (json.type !== this.type || <string>json.type !== 'ViewerApp') { | |||
| if (json.type !== this.type && <string>json.type !== 'ViewerApp') { | |||
| if (this.getPlugin(json.type)) { | |||
| return this.importPluginConfig(json) | |||
| } else { | |||