UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

224 lines (207 loc) 7.99 kB
import {AViewerPluginEventMap, type ThreeViewer} from '../../viewer/' // noinspection ES6PreferShortImport import {AViewerPluginSync} from '../../viewer/AViewerPlugin' import {Dropzone} from '../../utils' import {uiButton, uiConfig, uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js' import {AddAssetOptions, ImportFilesOptions, ImportResult, ImportAddOptions, GLTFLoader2} from '../../assetmanager' import {parseFileExtension, serialize} from 'ts-browser-helpers' export interface DropzonePluginOptions { /** * The DOM element to attach the dropzone to. */ domElement?: HTMLElement /** * Allowed file extensions. If undefined, all files are allowed. */ allowedExtensions?: string[] /** * Automatically import assets when dropped. * @default true */ autoImport?: boolean /** * Automatically add dropped and imported assets to the scene. * Works only if {@link autoImport} is true. * @default true */ autoAdd?: boolean /** * Import options for the {@link AssetImporter.importFiles}, used when importing files. */ importOptions?: ImportFilesOptions /** * Add options for the {@link RootScene.addObject}, used when adding assets to the scene. */ addOptions?: AddAssetOptions } export interface DropzonePluginEventMap extends AViewerPluginEventMap{ drop: { files: Map<string, File> imported?: Map<string, (ImportResult | undefined)[]> assets?: (ImportResult | undefined)[] nativeEvent: DragEvent } } /** * Dropzone Plugin * * Adds a dropzone to the viewer for importing assets. * * Automatically imports and adds assets to the scene, the behavior can be configured. * @category Plugins */ @uiFolderContainer('Dropzone') export class DropzonePlugin extends AViewerPluginSync<DropzonePluginEventMap> { static readonly PluginType = 'Dropzone' static readonly OldPluginType = 'DropzonePlugin' // todo swap declare uiConfig: UiObjectConfig @uiToggle() @serialize() enabled = true private _inputEl?: HTMLInputElement private _dropzone?: Dropzone private _allowedExtensions: string[]|undefined = undefined // undefined and empty array is different. /** * Automatically import assets when dropped. */ @serialize() autoImport = true /** * Automatically add dropped and imported assets to the scene. * Works only if {@link autoImport} is true. */ @uiToggle() @serialize() autoAdd = true /** * Import options for the {@link AssetImporter.importFiles} */ @uiConfig() @serialize() importOptions: ImportFilesOptions = { autoImportZipContents: true, forceImporterReprocess: false, useMeshLines: GLTFLoader2.UseMeshLines, createUniqueNames: GLTFLoader2.CreateUniqueNames, } /** * Add options for the {@link RootScene.addObject} */ @uiConfig() @serialize() addOptions: AddAssetOptions = { autoCenter: true, importConfig: true, autoScale: true, autoScaleRadius: 2, centerGeometries: false, // in the whole hierarchy centerGeometriesKeepPosition: true, // this centers while keeping world position 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 Local files') public promptForFile(): void { if (this.isDisabled()) return this.allowedExtensions = this._allowedExtensions this._inputEl?.click() } /** * Prompt for file url. */ @uiButton('Import from URL') public async promptForUrl(): Promise<void> { if (this.isDisabled() || !this._viewer) return const res = await this._viewer.dialog.prompt('Enter URL: Enter a public URL for a 3d file with extension', '', true) if (!res || !res.length) return await this.load(res, {}, true) } async load(res: string, options?: ImportAddOptions, dialog = false) { if (!this._viewer) { console.warn('DropzonePlugin: viewer not set') return } if (this.autoImport) { const manager = this._viewer.assetManager const ext = parseFileExtension(res) if (this._allowedExtensions && !this._allowedExtensions.includes(ext)) { dialog && await this._viewer.dialog.alert(`DropzonePlugin: file extension ${ext} not allowed`) return } const imported = await manager.importer.import(res, { ...this.importOptions, ...options ?? {}, }) const toAdd = [...imported ?? []].flat(2).filter(v => !!v) ?? [] if (this.autoAdd) { return await manager.loadImported(toAdd, { ...this.addOptions, ...options ?? {}, }) } return toAdd } else { dialog && await this._viewer.dialog.alert('DropzonePlugin: autoImport is disabled, file was not imported') } } 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' if (!this._domElement) this._domElement = viewer.canvas this._dropzone = new Dropzone(this._domElement, this._inputEl, { drop: this._onFileDrop.bind(this), }) this.allowedExtensions = this._allowedExtensions } onRemove(viewer: ThreeViewer) { this._dropzone?.destroy() this._dropzone = undefined this._inputEl = undefined super.onRemove(viewer) } private async _onFileDrop({files, nativeEvent}: {files: Map<string, File>, nativeEvent: DragEvent}) { if (!files) return if (this.isDisabled()) 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, nativeEvent}) } }