| if (!this.importer || !this.viewer) return [] | if (!this.importer || !this.viewer) return [] | ||||
| const imported = await this.importer.import<T>(assetOrPath, options) | const imported = await this.importer.import<T>(assetOrPath, options) | ||||
| if (!imported) { | if (!imported) { | ||||
| console.warn('Unable to import', assetOrPath, imported) | |||||
| const path = typeof assetOrPath === 'string' ? assetOrPath : (assetOrPath as IAsset)?.path | |||||
| if (path && !path.split('?')[0].endsWith('.vjson')) | |||||
| console.warn('Threepipe AssetManager - Unable to import', assetOrPath, imported) | |||||
| return [] | return [] | ||||
| } | } | ||||
| return this.loadImported<(T | undefined)[]>(imported, options) | return this.loadImported<(T | undefined)[]>(imported, options) |
| } | } | ||||
| applyMaterial(material: IMaterial, nameRegexOrUuid: string, regex = true): boolean { | applyMaterial(material: IMaterial, nameRegexOrUuid: string, regex = true): boolean { | ||||
| const mType = Object.getPrototypeOf(material).constructor.TYPE | |||||
| let currentMats = this.findMaterialsByName(nameRegexOrUuid, regex) | let currentMats = this.findMaterialsByName(nameRegexOrUuid, regex) | ||||
| if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameRegexOrUuid) as any] | if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameRegexOrUuid) as any] | ||||
| let applied = false | let applied = false | ||||
| if (!c) continue | if (!c) continue | ||||
| if (c === material) continue | if (c === material) continue | ||||
| if (c.userData.__isVariation) continue | if (c.userData.__isVariation) continue | ||||
| const cType = Object.getPrototypeOf(c).constructor.TYPE | |||||
| // console.log(cType, mType) | |||||
| if (cType === mType) { | |||||
| const n = c.name | |||||
| c.setValues(material) | |||||
| c.name = n | |||||
| applied = true | |||||
| } else { | |||||
| // todo | |||||
| // if ((c as any)['__' + mType]) continue | |||||
| const newMat = (c as any)['__' + mType] || this.create(mType) | |||||
| if (!newMat) continue | |||||
| const applied2 = this.copyMaterialProps(c, material) | |||||
| if (applied2) applied = true | |||||
| } | |||||
| return applied | |||||
| } | |||||
| /** | |||||
| * copyProps from material to c | |||||
| * @param c | |||||
| * @param material | |||||
| */ | |||||
| copyMaterialProps(c: IMaterial, material: IMaterial) { | |||||
| let applied = false | |||||
| const mType = Object.getPrototypeOf(material).constructor.TYPE | |||||
| const cType = Object.getPrototypeOf(c).constructor.TYPE | |||||
| // console.log(cType, mType) | |||||
| if (cType === mType) { | |||||
| const n = c.name | |||||
| c.setValues(material) | |||||
| c.name = n | |||||
| applied = true | |||||
| } else { | |||||
| // todo | |||||
| // if ((c as any)['__' + mType]) continue | |||||
| const newMat = (c as any)['__' + mType] || this.create(mType) | |||||
| if (newMat) { | |||||
| const n = c.name | const n = c.name | ||||
| newMat.setValues(material) | newMat.setValues(material) | ||||
| newMat.name = n | newMat.name = n |
| this.getWorldPosition(this._positionWorld) | this.getWorldPosition(this._positionWorld) | ||||
| iCameraCommons.setDirty.call(this, options) | iCameraCommons.setDirty.call(this, options) | ||||
| this._camUi.forEach(u=>u?.uiRefresh?.(false, 'postFrame', 1)) // because camera changes a lot. so we dont want to deep refresh ui on every change | |||||
| if (options?.last !== false) | |||||
| this._camUi.forEach(u=>u?.uiRefresh?.(false, 'postFrame', 1)) // because camera changes a lot. so we dont want to deep refresh ui on every change | |||||
| } | } | ||||
| /** | /** | ||||
| if (this.autoAspect) { | if (this.autoAspect) { | ||||
| if (!this._canvas) console.error('cannot calculate aspect ratio without canvas/container') | if (!this._canvas) console.error('cannot calculate aspect ratio without canvas/container') | ||||
| else { | else { | ||||
| this.aspect = this._canvas.clientWidth / this._canvas.clientHeight | |||||
| let aspect = this._canvas.clientWidth / this._canvas.clientHeight | |||||
| if (!isFinite(aspect)) aspect = 1 | |||||
| this.aspect = aspect | |||||
| this.updateProjectionMatrix?.() | this.updateProjectionMatrix?.() | ||||
| } | } | ||||
| } | } | ||||
| private _initCameraControls() { | private _initCameraControls() { | ||||
| const mode = this.controlsMode | const mode = this.controlsMode | ||||
| this._controls = this._controlsCtors.get(mode)?.(this, this._canvas) ?? undefined | this._controls = this._controlsCtors.get(mode)?.(this, this._canvas) ?? undefined | ||||
| if (!this._controls && mode !== '') console.error('Unable to create controls with mode ' + mode + '. Are you missing a plugin?') | |||||
| if (!this._controls && mode !== '') console.error('PerspectiveCamera2 - Unable to create controls with mode ' + mode + '. Are you missing a plugin?') | |||||
| this._controls?.addEventListener('change', this._controlsChanged) | this._controls?.addEventListener('change', this._controlsChanged) | ||||
| this._currentControlsMode = this._controls ? mode : '' | this._currentControlsMode = this._controls ? mode : '' | ||||
| // todo maybe set target like this: | // todo maybe set target like this: |
| import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry' | import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry' | ||||
| import {autoGPUInstanceMeshes, isInScene, toIndexedGeometry} from '../../three/utils' | import {autoGPUInstanceMeshes, isInScene, toIndexedGeometry} from '../../three/utils' | ||||
| import {BufferGeometry, Vector3} from 'three' | import {BufferGeometry, Vector3} from 'three' | ||||
| import {ThreeViewer} from '../../viewer' | |||||
| export const iGeometryCommons = { | export const iGeometryCommons = { | ||||
| setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void { | setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void { | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Center Geometry', | label: 'Center Geometry', | ||||
| value: () => { | |||||
| value: async() => { | |||||
| if (!await ThreeViewer.Dialog.confirm('This will move the objects based on the geometry center, do you want to continue?\nThis action cannot be undone.')) return | |||||
| this.center() | this.center() | ||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Center Geometry (keep position)', | label: 'Center Geometry (keep position)', | ||||
| value: () => { | |||||
| value: async() => { | |||||
| if (!await ThreeViewer.Dialog.confirm('This will move the geometry center keeping the object position, do you want to continue?\nThis action cannot be undone.')) return | |||||
| this.center(undefined, true) | this.center(undefined, true) | ||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Compute vertex normals', | label: 'Compute vertex normals', | ||||
| value: () => { | |||||
| if (this.hasAttribute('normal') && !confirm('Normals already exist, replace with computed normals?')) return | |||||
| value: async() => { | |||||
| if (this.hasAttribute('normal') && !await ThreeViewer.Dialog.confirm('Normals already exist, replace with computed normals?\nThis action cannot be undone.')) return | |||||
| this.computeVertexNormals() | this.computeVertexNormals() | ||||
| this.setDirty() | this.setDirty() | ||||
| }, | }, | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Compute vertex tangents', | label: 'Compute vertex tangents', | ||||
| value: () => { | |||||
| if (this.hasAttribute('tangent') && !confirm('Tangents already exist, replace with computed tangents?')) return | |||||
| value: async() => { | |||||
| if (this.hasAttribute('tangent') && !await ThreeViewer.Dialog.confirm('Tangents already exist, replace with computed tangents?\nThis action cannot be undone.')) return | |||||
| this.computeTangents() | this.computeTangents() | ||||
| this.setDirty() | this.setDirty() | ||||
| }, | }, | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Convert to indexed', | label: 'Convert to indexed', | ||||
| hidden: () => !!this.index, | hidden: () => !!this.index, | ||||
| value: () => { | |||||
| value: async() => { | |||||
| if (this.attributes.index) return | if (this.attributes.index) return | ||||
| const tolerance = parseFloat(prompt('Tolerance', '-1') ?? '-1') | |||||
| const tolerance = parseFloat(await ThreeViewer.Dialog.prompt('Convert to Indexed: Tolerance?', '-1') ?? '-1') | |||||
| toIndexedGeometry(this, tolerance) | toIndexedGeometry(this, tolerance) | ||||
| this.setDirty() | this.setDirty() | ||||
| }, | }, | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Create uv1 from uv', | label: 'Create uv1 from uv', | ||||
| value: () => { | |||||
| value: async() => { | |||||
| if (this.hasAttribute('uv1')) { | if (this.hasAttribute('uv1')) { | ||||
| if (!confirm('uv1 already exists, replace with uv data?')) return | |||||
| if (!await ThreeViewer.Dialog.confirm('uv1 already exists, replace with uv data?\nThis action cannot be undone.')) return | |||||
| } | } | ||||
| this.setAttribute('uv1', this.getAttribute('uv')) | this.setAttribute('uv1', this.getAttribute('uv')) | ||||
| this.setDirty() | this.setDirty() | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Remove vertex color attribute', | label: 'Remove vertex color attribute', | ||||
| hidden: () => !this.hasAttribute('color'), | hidden: () => !this.hasAttribute('color'), | ||||
| value: () => { | |||||
| value: async() => { | |||||
| if (!this.hasAttribute('color')) { | if (!this.hasAttribute('color')) { | ||||
| prompt('No color attribute found') | |||||
| await ThreeViewer.Dialog.prompt('No color attribute found') | |||||
| return | return | ||||
| } | } | ||||
| if (!confirm('Remove color attribute?')) return | |||||
| if (!await ThreeViewer.Dialog.confirm('Remove color attribute?')) return | |||||
| this.deleteAttribute('color') | this.deleteAttribute('color') | ||||
| this.setDirty() | this.setDirty() | ||||
| }, | }, | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Auto GPU Instances', | label: 'Auto GPU Instances', | ||||
| hidden: ()=> !this.appliedMeshes || this.appliedMeshes.size < 2, | hidden: ()=> !this.appliedMeshes || this.appliedMeshes.size < 2, | ||||
| value: ()=>{ | |||||
| if (!confirm('This action is irreversible, do you want to continue?')) return | |||||
| value: async()=>{ | |||||
| if (!await ThreeViewer.Dialog.confirm('This will automatically create Instanced Mesh from geometry instances. This action is irreversible, do you want to continue?')) return | |||||
| autoGPUInstanceMeshes(this) | autoGPUInstanceMeshes(this) | ||||
| }, | }, | ||||
| }, | }, |
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: `Select ${material.constructor.TypeSlug}`, | label: `Select ${material.constructor.TypeSlug}`, | ||||
| value: ()=>{ | |||||
| uploadFile(false, false, material.constructor.TypeSlug).then(async(files)=>files?.[0]?.text()).then((text)=>{ | |||||
| if (!text) return | |||||
| const json = JSON.parse(text) | |||||
| if (json.uuid) delete json.uuid // just copy the material properties | |||||
| material.fromJSON(json, getEmptyMeta()) | |||||
| }) | |||||
| }, | |||||
| value: async()=>uploadFile(false, false, material.constructor.TypeSlug).then(async(files)=>files?.[0]?.text()).then((text)=>{ | |||||
| if (!text) return | |||||
| const json = JSON.parse(text) | |||||
| if (json.uuid) delete json.uuid // just copy the material properties | |||||
| const currentJson = material.toJSON() | |||||
| material.fromJSON(json, getEmptyMeta()) | |||||
| return { | |||||
| undo: ()=>material.fromJSON(currentJson, getEmptyMeta()), | |||||
| redo: ()=>material.fromJSON(json, getEmptyMeta()), | |||||
| } | |||||
| }), | |||||
| }, | }, | ||||
| ], | ], | ||||
| roughMetal: (material: PhysicalMaterial): UiObjectConfig => ( | roughMetal: (material: PhysicalMaterial): UiObjectConfig => ( |
| setDirty: function(this: IMaterial, options?: IMaterialSetDirtyOptions): void { | setDirty: function(this: IMaterial, options?: IMaterialSetDirtyOptions): void { | ||||
| if (options?.needsUpdate !== false) this.needsUpdate = true | if (options?.needsUpdate !== false) this.needsUpdate = true | ||||
| this.dispatchEvent({bubbleToObject: true, bubbleToParent: true, ...options, type: 'materialUpdate'}) // this sets sceneUpdate in root scene | this.dispatchEvent({bubbleToObject: true, bubbleToParent: true, ...options, type: 'materialUpdate'}) // this sets sceneUpdate in root scene | ||||
| this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) | |||||
| if (options?.last !== false) this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) | |||||
| }, | }, | ||||
| setValues: (superSetValues: Material['setValues']): IMaterial['setValues'] => | setValues: (superSetValues: Material['setValues']): IMaterial['setValues'] => | ||||
| function(this: IMaterial, parameters: Material | (MaterialParameters & {type?: string})): IMaterial { | function(this: IMaterial, parameters: Material | (MaterialParameters & {type?: string})): IMaterial { |
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | ||||
| import {ICamera} from '../ICamera' | import {ICamera} from '../ICamera' | ||||
| import {Vector3} from 'three' | import {Vector3} from 'three' | ||||
| import {ThreeViewer} from '../../viewer' | |||||
| export function makeICameraCommonUiConfig(this: ICamera, config: UiObjectConfig): UiObjectConfig[] { | export function makeICameraCommonUiConfig(this: ICamera, config: UiObjectConfig): UiObjectConfig[] { | ||||
| return [ | return [ | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Auto Scale', | label: 'Auto Scale', | ||||
| hidden: ()=>!this.autoScale, | hidden: ()=>!this.autoScale, | ||||
| prompt: ['Auto Scale Radius: Object will be scaled to the given radius', this.userData.autoScaleRadius || '2', true], | |||||
| // prompt: ['Auto Scale Radius: Object will be scaled to the given radius', this.userData.autoScaleRadius || '2', true], | |||||
| value: async()=>{ | value: async()=>{ | ||||
| const def = (this.userData.autoScaleRadius || 2) + '' | const def = (this.userData.autoScaleRadius || 2) + '' | ||||
| const res = prompt('Auto Scale Radius: Object will be scaled to the given radius', def) | |||||
| const res = await ThreeViewer.Dialog.prompt('Auto Scale Radius: Object will be scaled to the given radius', def) | |||||
| if (res === null) return | if (res === null) return | ||||
| const rad = parseFloat(res || def) | const rad = parseFloat(res || def) | ||||
| if (Math.abs(rad) > 0) { | if (Math.abs(rad) > 0) { | ||||
| this.autoScale?.(rad) | |||||
| return ()=>this.autoScale?.(rad, undefined, undefined, true) | |||||
| return { | |||||
| action: ()=>this.autoScale?.(rad), | |||||
| undo: ()=>this.autoScale?.(rad, undefined, undefined, true), | |||||
| } | |||||
| } | } | ||||
| }, | }, | ||||
| }, | }, | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Auto Center', | label: 'Auto Center', | ||||
| value: ()=>{ | value: ()=>{ | ||||
| const res = confirm('Auto Center: Object will be centered, are you sure you want to proceed?') | |||||
| if (!res) return | |||||
| this.autoCenter?.(true) | |||||
| return ()=>this.autoCenter?.(true, true) | |||||
| // const res = await ThreeViewer.Dialog.confirm('Auto Center: Object will be centered, are you sure you want to proceed?') | |||||
| // if (!res) return | |||||
| return { | |||||
| action: ()=>this.autoCenter?.(true), | |||||
| undo: ()=>this.autoCenter?.(true, true), | |||||
| } | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Rotate ' + l + '90', | label: 'Rotate ' + l + '90', | ||||
| value: ()=>{ | value: ()=>{ | ||||
| this.rotateOnAxis(new Vector3(l.includes('X') ? 1 : 0, l.includes('Y') ? 1 : 0, l.includes('Z') ? 1 : 0), Math.PI / 2 * (l.includes('-') ? -1 : 1)) | |||||
| this.setDirty?.({refreshScene: true, refreshUi: false}) | |||||
| const axis = new Vector3(l.includes('X') ? 1 : 0, l.includes('Y') ? 1 : 0, l.includes('Z') ? 1 : 0) | |||||
| const angle = Math.PI / 2 * (l.includes('-') ? -1 : 1) | |||||
| return { | |||||
| action: ()=>{ | |||||
| this.rotateOnAxis(axis, angle) | |||||
| this.setDirty?.({refreshScene: true, refreshUi: false}) | |||||
| }, | |||||
| undo: ()=>{ | |||||
| this.rotateOnAxis(axis, -angle) | |||||
| this.setDirty?.({refreshScene: true, refreshUi: false}) | |||||
| }, | |||||
| } | |||||
| }, | }, | ||||
| } | } | ||||
| }), | }), |
| import {IMaterial} from '../IMaterial' | import {IMaterial} from '../IMaterial' | ||||
| import {objectHasOwn} from 'ts-browser-helpers' | import {objectHasOwn} from 'ts-browser-helpers' | ||||
| import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject' | import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject' | ||||
| import {copyObject3DUserData} from '../../utils/serialization' | |||||
| import {copyObject3DUserData} from '../../utils' | |||||
| import {IGeometry, IGeometryEvent} from '../IGeometry' | import {IGeometry, IGeometryEvent} from '../IGeometry' | ||||
| import {Box3B} from '../../three/math/Box3B' | |||||
| import {Box3B} from '../../three' | |||||
| import {makeIObject3DUiConfig} from './IObjectUi' | import {makeIObject3DUiConfig} from './IObjectUi' | ||||
| import {iGeometryCommons} from '../geometry/iGeometryCommons' | import {iGeometryCommons} from '../geometry/iGeometryCommons' | ||||
| import {iMaterialCommons} from '../material/iMaterialCommons' | import {iMaterialCommons} from '../material/iMaterialCommons' | ||||
| export const iObjectCommons = { | export const iObjectCommons = { | ||||
| setDirty: function(this: IObject3D, options?: IObjectSetDirtyOptions): void { | setDirty: function(this: IObject3D, options?: IObjectSetDirtyOptions): void { | ||||
| this.dispatchEvent({bubbleToParent: true, ...options, type: 'objectUpdate', object: this}) // this sets sceneUpdate in root scene | this.dispatchEvent({bubbleToParent: true, ...options, type: 'objectUpdate', object: this}) // this sets sceneUpdate in root scene | ||||
| if (options?.refreshUi !== false) this.refreshUi?.() | |||||
| if (options?.refreshUi !== false && options?.last !== false) this.refreshUi?.() | |||||
| // console.log('object update') | // console.log('object update') | ||||
| }, | }, | ||||
| import {EasingFunctions, EasingFunctionType} from '../../utils' | import {EasingFunctions, EasingFunctionType} from '../../utils' | ||||
| import {CameraView, ICamera, ICameraView, PerspectiveCamera2} from '../../core' | import {CameraView, ICamera, ICameraView, PerspectiveCamera2} from '../../core' | ||||
| import {AnimationResult, PopmotionPlugin} from './PopmotionPlugin' | import {AnimationResult, PopmotionPlugin} from './PopmotionPlugin' | ||||
| import {InteractionPromptPlugin} from '../interaction/InteractionPromptPlugin' | |||||
| export interface CameraViewPluginOptions{duration?: number, ease?: EasingFunctionType, interpolateMode?: 'spherical'|'linear'} | export interface CameraViewPluginOptions{duration?: number, ease?: EasingFunctionType, interpolateMode?: 'spherical'|'linear'} | ||||
| return | return | ||||
| } | } | ||||
| const interactionPrompt = this._viewer?.getPlugin(InteractionPromptPlugin) | |||||
| if (interactionPrompt && interactionPrompt.animationRunning) { | |||||
| await interactionPrompt.stopAnimation({reset: true}) | |||||
| } | |||||
| this._currentView = view | this._currentView = view | ||||
| this._animating = true | this._animating = true | ||||
| const cam = this._viewer.scene.mainCamera | const cam = this._viewer.scene.mainCamera | ||||
| let cameraZ = 1 | let cameraZ = 1 | ||||
| if (cam.isPerspectiveCamera) { | |||||
| if (cam.isPerspectiveCamera && size.length() > 0.0001) { | |||||
| const aspect = isFinite(cam.aspect) ? cam.aspect : 1 | |||||
| // get the max side of the bounding box (fits to width OR height as needed ) | // get the max side of the bounding box (fits to width OR height as needed ) | ||||
| const fov = (cam as PerspectiveCamera2).fov * (Math.PI / 180) | |||||
| const fovh = 2 * Math.atan(Math.tan(fov / 2) * cam.aspect) | |||||
| const fov = Math.max(1, (cam as PerspectiveCamera2).fov) * (Math.PI / 180) | |||||
| const fovh = 2 * Math.atan(Math.tan(fov / 2) * aspect) | |||||
| const dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2)) | const dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2)) | ||||
| const dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2)) | const dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2)) | ||||
| cameraZ = Math.max(dx, dy) | cameraZ = Math.max(dx, dy) |
| driver: this.defaultDriver, | driver: this.defaultDriver, | ||||
| ...options, | ...options, | ||||
| onUpdate: !isBool ? options.onUpdate : undefined, | onUpdate: !isBool ? options.onUpdate : undefined, | ||||
| onComplete: ()=>{ | |||||
| onComplete: async()=>{ | |||||
| try { | try { | ||||
| if (isBool) options.onUpdate?.(options.to as any) | if (isBool) options.onUpdate?.(options.to as any) | ||||
| options.onComplete && options.onComplete() | |||||
| options.onComplete && await options.onComplete() | |||||
| } catch (e: any) { | } catch (e: any) { | ||||
| if (!end2()) return | if (!end2()) return | ||||
| reject(e) | reject(e) | ||||
| if (!end2()) return | if (!end2()) return | ||||
| resolve() | resolve() | ||||
| }, | }, | ||||
| onStop: ()=>{ | |||||
| onStop: async()=>{ | |||||
| try { | try { | ||||
| options.onStop && options.onStop() | |||||
| options.onStop && await options.onStop() | |||||
| } catch (e: any) { | } catch (e: any) { | ||||
| if (!end2()) return | if (!end2()) return | ||||
| reject(e) | reject(e) |
| return mesh | return mesh | ||||
| } | } | ||||
| setGeometry(g: BufferGeometry) { | |||||
| if (this._geometry) this._geometry.dispose() | |||||
| this._geometry = iGeometryCommons.upgradeGeometry.call(g) | |||||
| setGeometry(g?: BufferGeometry) { | |||||
| if (!g) g = this._geometry | |||||
| else if (this._geometry) this._geometry.dispose() | |||||
| if (!g) return | |||||
| iGeometryCommons.upgradeGeometry.call(g) | |||||
| if (!this._geometry.attributes.uv2) { | if (!this._geometry.attributes.uv2) { | ||||
| this._geometry.attributes.uv2 = (this._geometry.attributes.uv as any as BufferAttribute | InterleavedBufferAttribute).clone() | this._geometry.attributes.uv2 = (this._geometry.attributes.uv as any as BufferAttribute | InterleavedBufferAttribute).clone() | ||||
| this._geometry.attributes.uv2.needsUpdate = true | this._geometry.attributes.uv2.needsUpdate = true | ||||
| if (this._mesh) this._mesh.geometry = this._geometry | if (this._mesh) this._mesh.geometry = this._geometry | ||||
| } | } | ||||
| protected _createMaterial(material?: PhysicalMaterial): PhysicalMaterial { | protected _createMaterial(material?: PhysicalMaterial): PhysicalMaterial { | ||||
| if (!material) material = new PhysicalMaterial({ | if (!material) material = new PhysicalMaterial({ | ||||
| name: 'BaseGroundMaterial', | name: 'BaseGroundMaterial', |
| super.onAdded(viewer) | super.onAdded(viewer) | ||||
| // todo subscribe to plugin add event if picking is not added yet. | // todo subscribe to plugin add event if picking is not added yet. | ||||
| this._picking = viewer.getPlugin<PickingPlugin>('Picking') | |||||
| viewer.forPlugin(PickingPlugin, (p)=>{ | |||||
| this._picking = p | |||||
| this._picking?.addEventListener('selectedObjectChanged', this._refreshUiConfig) | |||||
| }, ()=>{ | |||||
| this._picking?.removeEventListener('selectedObjectChanged', this._refreshUiConfig) | |||||
| this._picking = undefined | |||||
| }) | |||||
| this._previewGenerator = new MaterialPreviewGenerator() | this._previewGenerator = new MaterialPreviewGenerator() | ||||
| this._picking?.addEventListener('selectedObjectChanged', this._refreshUiConfig) | |||||
| viewer.addEventListener('preFrame', this._refreshUi) | viewer.addEventListener('preFrame', this._refreshUi) | ||||
| } | } | ||||
| this.variations.splice(this.variations.indexOf(variation), 1) | this.variations.splice(this.variations.indexOf(variation), 1) | ||||
| this.refreshUi() | this.refreshUi() | ||||
| } | } | ||||
| addVariation(material?: IMaterial) { | |||||
| const clone = material?.clone?.() | |||||
| addVariation(material?: IMaterial, variationKey?: string, cloneMaterial = true) { | |||||
| const clone = cloneMaterial && material?.clone ? material.clone() : material | |||||
| if (material && clone) { | if (material && clone) { | ||||
| let variation = this.findVariation(material.uuid) | |||||
| if (!variation && material.name.length > 0) variation = this.findVariation(material.name) | |||||
| let variation = this.findVariation(variationKey ?? material.uuid) | |||||
| if (!variation && !variationKey && material.name.length > 0) variation = this.findVariation(material.name) | |||||
| if (!variation) { | if (!variation) { | ||||
| variation = this.createVariation(material) | |||||
| variation = this.createVariation(material, variationKey) | |||||
| } | } | ||||
| variation.materials.push(clone) | variation.materials.push(clone) | ||||
| this.refreshUi() | this.refreshUi() | ||||
| } | } | ||||
| } | } | ||||
| createVariation(material: IMaterial) { | |||||
| createVariation(material: IMaterial, variationKey?: string) { | |||||
| this.variations.push({ | this.variations.push({ | ||||
| uuid: material.name.length > 0 ? material.name : material.uuid, | |||||
| uuid: variationKey ?? material.name.length > 0 ? material.name : material.uuid, | |||||
| title: material.name.length > 0 ? material.name : 'No Name', | title: material.name.length > 0 ? material.name : 'No Name', | ||||
| preview: 'generate:sphere', | preview: 'generate:sphere', | ||||
| materials: [], | materials: [], |
| * This works by toggling the `visible` property of the children of a parent object. | * This works by toggling the `visible` property of the children of a parent object. | ||||
| * The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations. | * The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations. | ||||
| * It also provides a function to create snapshot previews of individual variations. This creates a limited render of the object with the selected child visible. | * It also provides a function to create snapshot previews of individual variations. This creates a limited render of the object with the selected child visible. | ||||
| * To get a proper render, its better to render it offline and set the image as a preview. | |||||
| * To get a proper render, it's better to render it offline and set the image as a preview. | |||||
| * | * | ||||
| * See `SwitchNodePlugin` in [plugin-configurator](https://threepipe.org/plugins/configurator/docs/index.html) for example on inheriting with a custom UI renderer. | * See `SwitchNodePlugin` in [plugin-configurator](https://threepipe.org/plugins/configurator/docs/index.html) for example on inheriting with a custom UI renderer. | ||||
| * | * | ||||
| if (!this.enabled) return false | if (!this.enabled) return false | ||||
| if (!this._viewer) return false | if (!this._viewer) return false | ||||
| this._uiNeedRefresh = false | this._uiNeedRefresh = false | ||||
| if (this.autoSnapIcons) this.snapIcons() | |||||
| this.refreshUiConfig() | this.refreshUiConfig() | ||||
| return true | return true | ||||
| if (refreshUi) this.refreshUi() | if (refreshUi) this.refreshUi() | ||||
| } | } | ||||
| /** | |||||
| * If true, the plugin will automatically take snapshots of the icons in _refreshUi and put them in the object.userdata.__icon | |||||
| * Otherwise, call {@link snapIcons} manually | |||||
| */ | |||||
| autoSnapIcons = false | |||||
| /** | |||||
| * Snapshots icons and puts in the userdata.__icon | |||||
| */ | |||||
| snapIcons() { | |||||
| for (const variation of this.variations) { | |||||
| const obj = this._viewer!.scene.getObjectByName(variation.name) | |||||
| if (!obj) { | |||||
| console.warn('no object found for variation, skipping', variation) | |||||
| continue | |||||
| } | |||||
| if (obj.children.length < 1) { | |||||
| console.warn('SwitchNode does not have enough children', variation) | |||||
| } | |||||
| for (const child of obj.children) { | |||||
| if (child.userData.__icon) return | |||||
| const image = this.getPreview(variation, child, false) | |||||
| if (image) child.userData.__icon = image | |||||
| } | |||||
| } | |||||
| } | |||||
| uiConfig: UiObjectConfig = { | uiConfig: UiObjectConfig = { | ||||
| label: 'Switch Node Plugin', | label: 'Switch Node Plugin', | ||||
| type: 'folder', | type: 'folder', |
| // } | // } | ||||
| } | } | ||||
| @uiButton() stopAnimation = () => { | |||||
| @uiButton() stopAnimation = async({reset = true}: {reset?: boolean} = {}) => { | |||||
| if (!this._viewer || !this.cursorEl) return // dont check for enabled here. | if (!this._viewer || !this.cursorEl) return // dont check for enabled here. | ||||
| this.animationRunning = false | this.animationRunning = false | ||||
| this.cursorEl.style.opacity = '0' | this.cursorEl.style.opacity = '0' | ||||
| if (this.currentSphericalPosition && reset) { | |||||
| this._viewer.scene.mainCamera.position.setFromSpherical(this.currentSphericalPosition).add(this._viewer.scene.mainCamera.target) | |||||
| this._viewer.scene.mainCamera.setDirty() | |||||
| } | |||||
| this._viewer.scene.mainCamera.setInteractions(true, InteractionPromptPlugin.PluginType) | this._viewer.scene.mainCamera.setInteractions(true, InteractionPromptPlugin.PluginType) | ||||
| // if (this.interactionsDisabled) { | // if (this.interactionsDisabled) { | ||||
| // this._viewer.scene.mainCamera.interactionsEnabled = true | // this._viewer.scene.mainCamera.interactionsEnabled = true | ||||
| // this.interactionsDisabled = false | // this.interactionsDisabled = false | ||||
| // } | // } | ||||
| return this._viewer.doOnce('postFrame') | |||||
| } | } | ||||
| private _pointerDown = () => { | private _pointerDown = () => { | ||||
| if (this.isDisabled()) return | if (this.isDisabled()) return | ||||
| if (this.autoStop) this.stopAnimation() | |||||
| if (this.autoStop) this.stopAnimation({reset: false}) // todo dont reset only on pointer drag, not down | |||||
| this.lastActionTime = now() | this.lastActionTime = now() | ||||
| } | } | ||||
| private _x = 0 | private _x = 0 |
| @onChange(LoadingScreenPlugin.prototype.refresh) | @onChange(LoadingScreenPlugin.prototype.refresh) | ||||
| @serialize() textColor = '#222222' | @serialize() textColor = '#222222' | ||||
| static LS_DEFAULT_LOGO = 'https://threepipe.org/logo.svg' | |||||
| @uiInput('Logo Image') | @uiInput('Logo Image') | ||||
| @onChange(LoadingScreenPlugin.prototype.refresh) | @onChange(LoadingScreenPlugin.prototype.refresh) | ||||
| @serialize() logoImage = 'https://threepipe.org/logo.svg' | |||||
| @serialize() logoImage = LoadingScreenPlugin.LS_DEFAULT_LOGO | |||||
| private _isPreviewing = false | private _isPreviewing = false | ||||
| private _previewState = new Map([['file.glb', {state: 'downloading', progress: 50}], ['environment.hdr', {state: 'adding'}]]) | private _previewState = new Map([['file.glb', {state: 'downloading', progress: 50}], ['environment.hdr', {state: 'adding'}]]) | ||||
| private _isHidden = false | private _isHidden = false | ||||
| get visible() { | |||||
| return !this._isHidden | |||||
| } | |||||
| async hide() { | async hide() { | ||||
| this._isHidden = true | this._isHidden = true | ||||
| this._mainDiv.style.opacity = '0' | this._mainDiv.style.opacity = '0' | ||||
| errors.length === processState.size && this.hideOnOnlyErrors)) { | errors.length === processState.size && this.hideOnOnlyErrors)) { | ||||
| this.hideDelay ? this.hideWithDelay() : this.hide() | this.hideDelay ? this.hideWithDelay() : this.hide() | ||||
| } else if (processState.size > 0 && this.showOnFilesLoading) { | } else if (processState.size > 0 && this.showOnFilesLoading) { | ||||
| const sceneObjects = this._viewer.scene.modelRoot.children | |||||
| if (sceneObjects.length > 0 && this.minimizeOnSceneObjectLoad && this._viewer.scene.environment) this.minimize() | |||||
| else this.maximize() | |||||
| this.show() | this.show() | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| // disables showOnSceneEmpty | |||||
| isEditor = false | |||||
| private _sceneUpdate = (e: any) => { | private _sceneUpdate = (e: any) => { | ||||
| if (!this._viewer) return | if (!this._viewer) return | ||||
| if (!e.hierarchyChanged) return | if (!e.hierarchyChanged) return | ||||
| const sceneObjects = this._viewer.scene.modelRoot.children | const sceneObjects = this._viewer.scene.modelRoot.children | ||||
| if (sceneObjects.length === 0 && this.showOnSceneEmpty) { | |||||
| if (sceneObjects.length === 0 && this.showOnSceneEmpty && !this.isEditor) { | |||||
| this.show() | this.show() | ||||
| } | } | ||||
| // console.log(sceneObjects.length) | // console.log(sceneObjects.length) |
| dependencies = [ProgressivePlugin] | dependencies = [ProgressivePlugin] | ||||
| // disables fadeOn... options but not serialized | |||||
| isEditor = false | |||||
| @serialize() @uiToggle() fadeOnActiveCameraChange = true | @serialize() @uiToggle() fadeOnActiveCameraChange = true | ||||
| @serialize() @uiToggle() fadeOnMaterialUpdate = true | @serialize() @uiToggle() fadeOnMaterialUpdate = true | ||||
| @serialize() @uiToggle() fadeOnSceneUpdate = true | @serialize() @uiToggle() fadeOnSceneUpdate = true | ||||
| } | } | ||||
| private _fadeCam = async(ev: any)=> | private _fadeCam = async(ev: any)=> | ||||
| ev.frameFade !== false && this.fadeOnActiveCameraChange && this.startTransition(ev.fadeDuration || 1000) | |||||
| ev.frameFade !== false && !this.isEditor && this.fadeOnActiveCameraChange && this.startTransition(ev.fadeDuration || 1000) | |||||
| private _fadeMat = async(ev: any)=> | private _fadeMat = async(ev: any)=> | ||||
| ev.frameFade !== false && this.fadeOnMaterialUpdate && this.startTransition(ev.fadeDuration || 200) | |||||
| ev.frameFade !== false && !this.isEditor && this.fadeOnMaterialUpdate && this.startTransition(ev.fadeDuration || 200) | |||||
| private _fadeScene = async(ev: any)=> | private _fadeScene = async(ev: any)=> | ||||
| ev.frameFade !== false && this.fadeOnSceneUpdate && this.startTransition(ev.fadeDuration || 500) | |||||
| ev.frameFade !== false && !this.isEditor && this.fadeOnSceneUpdate && this.startTransition(ev.fadeDuration || 500) | |||||
| private _fadeObjectUpdate = async(ev: any)=> | private _fadeObjectUpdate = async(ev: any)=> | ||||
| ev.frameFade && this.startTransition(ev.fadeDuration || 500) | |||||
| ev.frameFade && !this.isEditor && this.startTransition(ev.fadeDuration || 500) | |||||
| private _onPointerMove = (ev: PointerEvent)=> { | private _onPointerMove = (ev: PointerEvent)=> { | ||||
| const canvas = this._viewer?.canvas | const canvas = this._viewer?.canvas |
| import {Matrix4, Texture, TextureDataType, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget} from 'three' | import {Matrix4, Texture, TextureDataType, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget} from 'three' | ||||
| import {ExtendedShaderPass, IPassID, IPipelinePass} from '../../postprocessing' | import {ExtendedShaderPass, IPassID, IPipelinePass} from '../../postprocessing' | ||||
| import {type IViewerEvent, ThreeViewer} from '../../viewer' | |||||
| import {ThreeViewer} from '../../viewer' | |||||
| import {PipelinePassPlugin} from '../base/PipelinePassPlugin' | import {PipelinePassPlugin} from '../base/PipelinePassPlugin' | ||||
| import {uiConfig, uiFolderContainer, uiImage, uiSlider} from 'uiconfig.js' | import {uiConfig, uiFolderContainer, uiImage, uiSlider} from 'uiconfig.js' | ||||
| import {ICamera, IMaterial, IRenderManager, IScene, IWebGLRenderer, PhysicalMaterial} from '../../core' | import {ICamera, IMaterial, IRenderManager, IScene, IWebGLRenderer, PhysicalMaterial} from '../../core' | ||||
| onAdded(viewer: ThreeViewer) { | onAdded(viewer: ThreeViewer) { | ||||
| super.onAdded(viewer) | super.onAdded(viewer) | ||||
| const gbuffer = viewer.getPlugin(GBufferPlugin) | |||||
| if (gbuffer) gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this)) | |||||
| else viewer.addEventListener('addPlugin', this._onPluginAdd) | |||||
| viewer.forPlugin(GBufferPlugin, (gbuffer) => { | |||||
| gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this)) | |||||
| }, (gbuffer)=>{ | |||||
| gbuffer.unregisterGBufferUpdater(this.constructor.PluginType) | |||||
| }) | |||||
| this._gbufferUnpackExtensionChanged() | this._gbufferUnpackExtensionChanged() | ||||
| viewer.renderManager.addEventListener('gbufferUnpackExtensionChanged', this._gbufferUnpackExtensionChanged) | viewer.renderManager.addEventListener('gbufferUnpackExtensionChanged', this._gbufferUnpackExtensionChanged) | ||||
| } | } | ||||
| private _onPluginAdd = (e: IViewerEvent)=>{ | |||||
| if (e.plugin?.constructor?.PluginType !== GBufferPlugin.PluginType) return | |||||
| const gbuffer = e.plugin as GBufferPlugin | |||||
| gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this)) | |||||
| this._viewer?.removeEventListener('addPlugin', this._onPluginAdd) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer): void { | onRemove(viewer: ThreeViewer): void { | ||||
| viewer.removeEventListener('addPlugin', this._onPluginAdd) | |||||
| this._disposeTarget() | this._disposeTarget() | ||||
| return super.onRemove(viewer) | return super.onRemove(viewer) | ||||
| } | } |
| import {type AViewerPlugin, AViewerPluginSync} from '../../viewer/AViewerPlugin' | import {type AViewerPlugin, AViewerPluginSync} from '../../viewer/AViewerPlugin' | ||||
| import type {IViewerEvent, ThreeViewer} from '../../viewer' | |||||
| import type {ThreeViewer} from '../../viewer' | |||||
| import {MaterialExtension} from '../../materials' | import {MaterialExtension} from '../../materials' | ||||
| import {Shader, Vector4, WebGLRenderer} from 'three' | import {Shader, Vector4, WebGLRenderer} from 'three' | ||||
| import {IMaterial} from '../../core' | import {IMaterial} from '../../core' | ||||
| onAdded(viewer: ThreeViewer) { | onAdded(viewer: ThreeViewer) { | ||||
| super.onAdded(viewer) | super.onAdded(viewer) | ||||
| const gbuffer = viewer.getPlugin(GBufferPlugin) | |||||
| if (gbuffer) gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this)) | |||||
| else viewer.addEventListener('addPlugin', this._onPluginAdd) | |||||
| viewer.forPlugin(GBufferPlugin, (gbuffer) => { | |||||
| gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this)) | |||||
| }, (gbuffer)=>{ | |||||
| gbuffer.unregisterGBufferUpdater(this.constructor.PluginType) | |||||
| }) | |||||
| viewer.renderManager.screenPass.material.registerMaterialExtensions([this]) | viewer.renderManager.screenPass.material.registerMaterialExtensions([this]) | ||||
| } | } | ||||
| private _onPluginAdd = (e: IViewerEvent)=>{ | |||||
| if (e.plugin?.constructor?.PluginType !== GBufferPlugin.PluginType) return | |||||
| const gbuffer = e.plugin as GBufferPlugin | |||||
| gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this)) | |||||
| this._viewer?.removeEventListener('addPlugin', this._onPluginAdd) | |||||
| } | |||||
| onRemove(viewer: ThreeViewer) { | onRemove(viewer: ThreeViewer) { | ||||
| viewer.removeEventListener('addPlugin', this._onPluginAdd) | |||||
| viewer.getPlugin(GBufferPlugin)?.unregisterGBufferUpdater(this.constructor.PluginType) | viewer.getPlugin(GBufferPlugin)?.unregisterGBufferUpdater(this.constructor.PluginType) | ||||
| viewer.renderManager.screenPass.material.unregisterMaterialExtensions([this]) | viewer.renderManager.screenPass.material.unregisterMaterialExtensions([this]) | ||||
| super.onRemove(viewer) | super.onRemove(viewer) |
| FloatType, | FloatType, | ||||
| HalfFloatType, | HalfFloatType, | ||||
| LinearSRGBColorSpace, | LinearSRGBColorSpace, | ||||
| Texture, | |||||
| Texture, TextureImageData, | |||||
| TextureDataType, | TextureDataType, | ||||
| UnsignedByteType, | UnsignedByteType, | ||||
| WebGLRenderer, | WebGLRenderer, | ||||
| } from 'three' | } from 'three' | ||||
| import {TextureImageData} from 'three/src/textures/types' | |||||
| import {canvasFlipY, LinearToSRGB} from 'ts-browser-helpers' | import {canvasFlipY, LinearToSRGB} from 'ts-browser-helpers' | ||||
| export function getTextureDataType(renderer?: WebGLRenderer): TextureDataType { | export function getTextureDataType(renderer?: WebGLRenderer): TextureDataType { |
| if (oldType) this.plugins[oldType] = p | if (oldType) this.plugins[oldType] = p | ||||
| await p.onAdded(this) | await p.onAdded(this) | ||||
| this.dispatchEvent({type: 'addPlugin', target: this, plugin: p}) | |||||
| this.setDirty(p) | |||||
| this._onPluginAdd(p) | |||||
| return p | return p | ||||
| } | } | ||||
| if (oldType && this.plugins[oldType]) this.console.error('Plugin type mismatch') | if (oldType && this.plugins[oldType]) this.console.error('Plugin type mismatch') | ||||
| if (oldType) this.plugins[oldType] = p | if (oldType) this.plugins[oldType] = p | ||||
| p.onAdded(this) | p.onAdded(this) | ||||
| this.dispatchEvent({type: 'addPlugin', target: this, plugin: p}) | |||||
| this.setDirty(p) | |||||
| this._onPluginAdd(p) | |||||
| return p | return p | ||||
| } | } | ||||
| const type = p.constructor.PluginType | const type = p.constructor.PluginType | ||||
| if (!this.plugins[type]) return | if (!this.plugins[type]) return | ||||
| await p.onRemove(this) | await p.onRemove(this) | ||||
| this.dispatchEvent({type: 'removePlugin', target: this, plugin: p}) | |||||
| delete this.plugins[type] | |||||
| if (dispose) await p.dispose() // todo await? | |||||
| this.setDirty(p) | |||||
| this._onPluginRemove(p, dispose) | |||||
| } | } | ||||
| /** | /** | ||||
| const type = p.constructor.PluginType | const type = p.constructor.PluginType | ||||
| if (!this.plugins[type]) return | if (!this.plugins[type]) return | ||||
| p.onRemove(this) | p.onRemove(this) | ||||
| this.dispatchEvent({type: 'removePlugin', target: this, plugin: p}) | |||||
| this._onPluginRemove(p, dispose) | |||||
| delete this.plugins[type] | delete this.plugins[type] | ||||
| if (dispose) p.dispose() | if (dispose) p.dispose() | ||||
| this.setDirty(p) | this.setDirty(p) | ||||
| if (filter && filter.length === 0) return [] | if (filter && filter.length === 0) return [] | ||||
| return Object.entries(this.plugins).map(p=> { | return Object.entries(this.plugins).map(p=> { | ||||
| if (filter && !filter.includes(p[1].constructor.PluginType)) return | if (filter && !filter.includes(p[1].constructor.PluginType)) return | ||||
| if (this.serializePluginsIgnored.includes((p[1].constructor as any).PluginType)) return | |||||
| // if (!p[1].toJSON) this.console.log(`Plugin of type ${p[0]} is not serializable`) | // if (!p[1].toJSON) this.console.log(`Plugin of type ${p[0]} is not serializable`) | ||||
| return p[1].serializeWithViewer !== false ? p[1].toJSON?.(meta) : undefined | return p[1].serializeWithViewer !== false ? p[1].toJSON?.(meta) : undefined | ||||
| }).filter(p=> !!p) | }).filter(p=> !!p) | ||||
| this.console.warn('Invalid plugin to import ', p) | this.console.warn('Invalid plugin to import ', p) | ||||
| return | return | ||||
| } | } | ||||
| if (this.serializePluginsIgnored.includes(p.type)) return | |||||
| const plugin = this.getPlugin(p.type) | const plugin = this.getPlugin(p.type) | ||||
| if (!plugin) { | if (!plugin) { | ||||
| // this.console.warn(`Plugin of type ${p.type} is not added, cannot deserialize`) | // this.console.warn(`Plugin of type ${p.type} is not added, cannot deserialize`) | ||||
| return | return | ||||
| } | } | ||||
| plugin.fromJSON?.(p, meta) | |||||
| plugin.fromJSON && plugin.fromJSON(p, meta) | |||||
| }) | }) | ||||
| return this | return this | ||||
| } | } | ||||
| return this.plugins[type] as T | undefined | return this.plugins[type] as T | undefined | ||||
| } | } | ||||
| private _onPluginAdd(p: IViewerPlugin) { | |||||
| const ev = {type: 'addPlugin', target: this, plugin: p} as const | |||||
| this.dispatchEvent(ev) | |||||
| this._pluginListeners.add.filter(l=> !l.p.length || l.p.includes(p.constructor.PluginType)).forEach(l=> l.l(ev)) | |||||
| this.setDirty(p) | |||||
| } | |||||
| private _onPluginRemove(p: IViewerPlugin, dispose = false) { | |||||
| const ev = {type: 'removePlugin', target: this, plugin: p} as const | |||||
| this.dispatchEvent(ev) | |||||
| this._pluginListeners.remove.filter(l=> !l.p.length || l.p.includes(p.constructor.PluginType)).forEach(l=> l.l(ev)) | |||||
| delete this.plugins[p.constructor.PluginType] | |||||
| if (dispose) p.dispose() // todo await? | |||||
| this.setDirty(p) | |||||
| } | |||||
| private _pluginListeners: Record<'add' | 'remove', ({p: string[], l: (event: IViewerEvent) => void})[]> = { | |||||
| add: [], | |||||
| remove: [], | |||||
| } | |||||
| addPluginListener(type: 'add' | 'remove', listener: (event: IViewerEvent) => void, ...plugins: string[]): void { | |||||
| this._pluginListeners[type].push({p: plugins, l: listener}) | |||||
| } | |||||
| removePluginListener(type: 'add' | 'remove', listener: (event: IViewerEvent) => void): void { | |||||
| this._pluginListeners[type] = this._pluginListeners[type].filter(l=> l.l !== listener) | |||||
| } | |||||
| /** | |||||
| * Can be used to "subscribe" to plugins. | |||||
| * @param plugin | |||||
| * @param mount | |||||
| * @param unmount | |||||
| */ | |||||
| forPlugin<T extends IViewerPlugin>(plugin: string|Class<T>, mount: (p: T) => void, unmount?: (p: T) => void): void { | |||||
| const um = ()=>{ | |||||
| if (unmount) { | |||||
| const lis = () => { | |||||
| const p1 = this.getPlugin(plugin) | |||||
| if (!p1) return | |||||
| this.removePluginListener('remove', lis) | |||||
| unmount(p1) | |||||
| } | |||||
| this.addPluginListener('remove', lis, typeof plugin === 'string' ? plugin : (plugin as any).PluginType) | |||||
| } | |||||
| } | |||||
| const p = this.getPlugin(plugin) | |||||
| if (p) { | |||||
| mount(p) | |||||
| um() | |||||
| } else { | |||||
| const lis = () => { | |||||
| const p1 = this.getPlugin(plugin) | |||||
| if (!p1) return | |||||
| this.removePluginListener('add', lis) | |||||
| mount(p1) | |||||
| um() | |||||
| } | |||||
| this.addPluginListener('add', lis, typeof plugin === 'string' ? plugin : (plugin as any).PluginType) | |||||
| } | |||||
| } | |||||
| /** | |||||
| * plugins that are not serialized/deserialized with the viewer from config. useful when loading files exported from the editor, etc | |||||
| * (runtime only, not serialized itself) | |||||
| */ | |||||
| serializePluginsIgnored: string[] = [] | |||||
| } | } |
| const isProd = process.env.NODE_ENV === 'production' | const isProd = process.env.NODE_ENV === 'production' | ||||
| const { name, version, author } = packageJson | const { name, version, author } = packageJson | ||||
| const {main, module, browser} = packageJson['clean-package'].replace | |||||
| const {main, module, browser} = packageJson | |||||
| export default defineConfig({ | export default defineConfig({ | ||||
| optimizeDeps: { | optimizeDeps: { |