| @@ -121,7 +121,9 @@ export class AssetManager extends EventDispatcher<BaseEvent&{data?: ImportResult | |||
| if (!this.importer || !this.viewer) return [] | |||
| const imported = await this.importer.import<T>(assetOrPath, options) | |||
| 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 this.loadImported<(T | undefined)[]>(imported, options) | |||
| @@ -319,7 +319,6 @@ export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> { | |||
| } | |||
| applyMaterial(material: IMaterial, nameRegexOrUuid: string, regex = true): boolean { | |||
| const mType = Object.getPrototypeOf(material).constructor.TYPE | |||
| let currentMats = this.findMaterialsByName(nameRegexOrUuid, regex) | |||
| if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameRegexOrUuid) as any] | |||
| let applied = false | |||
| @@ -328,18 +327,32 @@ export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> { | |||
| if (!c) continue | |||
| if (c === material) 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 | |||
| newMat.setValues(material) | |||
| newMat.name = n | |||
| @@ -204,8 +204,9 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | |||
| this.getWorldPosition(this._positionWorld) | |||
| 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 | |||
| } | |||
| /** | |||
| @@ -216,7 +217,9 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | |||
| if (this.autoAspect) { | |||
| if (!this._canvas) console.error('cannot calculate aspect ratio without canvas/container') | |||
| 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?.() | |||
| } | |||
| } | |||
| @@ -266,7 +269,7 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | |||
| private _initCameraControls() { | |||
| const mode = this.controlsMode | |||
| 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._currentControlsMode = this._controls ? mode : '' | |||
| // todo maybe set target like this: | |||
| @@ -2,6 +2,7 @@ import {UiObjectConfig} from 'uiconfig.js' | |||
| import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry' | |||
| import {autoGPUInstanceMeshes, isInScene, toIndexedGeometry} from '../../three/utils' | |||
| import {BufferGeometry, Vector3} from 'three' | |||
| import {ThreeViewer} from '../../viewer' | |||
| export const iGeometryCommons = { | |||
| setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void { | |||
| @@ -57,22 +58,24 @@ export const iGeometryCommons = { | |||
| { | |||
| type: 'button', | |||
| 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() | |||
| }, | |||
| }, | |||
| { | |||
| type: 'button', | |||
| 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) | |||
| }, | |||
| }, | |||
| { | |||
| type: 'button', | |||
| 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.setDirty() | |||
| }, | |||
| @@ -80,8 +83,8 @@ export const iGeometryCommons = { | |||
| { | |||
| type: 'button', | |||
| 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.setDirty() | |||
| }, | |||
| @@ -98,9 +101,9 @@ export const iGeometryCommons = { | |||
| type: 'button', | |||
| label: 'Convert to indexed', | |||
| hidden: () => !!this.index, | |||
| value: () => { | |||
| value: async() => { | |||
| 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) | |||
| this.setDirty() | |||
| }, | |||
| @@ -118,9 +121,9 @@ export const iGeometryCommons = { | |||
| { | |||
| type: 'button', | |||
| label: 'Create uv1 from uv', | |||
| value: () => { | |||
| value: async() => { | |||
| 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.setDirty() | |||
| @@ -130,12 +133,12 @@ export const iGeometryCommons = { | |||
| type: 'button', | |||
| label: 'Remove vertex color attribute', | |||
| hidden: () => !this.hasAttribute('color'), | |||
| value: () => { | |||
| value: async() => { | |||
| if (!this.hasAttribute('color')) { | |||
| prompt('No color attribute found') | |||
| await ThreeViewer.Dialog.prompt('No color attribute found') | |||
| return | |||
| } | |||
| if (!confirm('Remove color attribute?')) return | |||
| if (!await ThreeViewer.Dialog.confirm('Remove color attribute?')) return | |||
| this.deleteAttribute('color') | |||
| this.setDirty() | |||
| }, | |||
| @@ -144,8 +147,8 @@ export const iGeometryCommons = { | |||
| type: 'button', | |||
| label: 'Auto GPU Instances', | |||
| 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) | |||
| }, | |||
| }, | |||
| @@ -259,14 +259,17 @@ export const iMaterialUI = { | |||
| { | |||
| type: 'button', | |||
| 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 => ( | |||
| @@ -77,7 +77,7 @@ export const iMaterialCommons = { | |||
| setDirty: function(this: IMaterial, options?: IMaterialSetDirtyOptions): void { | |||
| if (options?.needsUpdate !== false) this.needsUpdate = true | |||
| 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'] => | |||
| function(this: IMaterial, parameters: Material | (MaterialParameters & {type?: string})): IMaterial { | |||
| @@ -2,6 +2,7 @@ import {IObject3D} from '../IObject' | |||
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | |||
| import {ICamera} from '../ICamera' | |||
| import {Vector3} from 'three' | |||
| import {ThreeViewer} from '../../viewer' | |||
| export function makeICameraCommonUiConfig(this: ICamera, config: UiObjectConfig): UiObjectConfig[] { | |||
| return [ | |||
| @@ -127,15 +128,17 @@ export function makeIObject3DUiConfig(this: IObject3D, isMesh?:boolean): UiObjec | |||
| type: 'button', | |||
| label: 'Auto Scale', | |||
| 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()=>{ | |||
| 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 | |||
| const rad = parseFloat(res || def) | |||
| 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), | |||
| } | |||
| } | |||
| }, | |||
| }, | |||
| @@ -143,10 +146,12 @@ export function makeIObject3DUiConfig(this: IObject3D, isMesh?:boolean): UiObjec | |||
| type: 'button', | |||
| label: 'Auto Center', | |||
| 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), | |||
| } | |||
| }, | |||
| }, | |||
| { | |||
| @@ -159,8 +164,18 @@ export function makeIObject3DUiConfig(this: IObject3D, isMesh?:boolean): UiObjec | |||
| type: 'button', | |||
| label: 'Rotate ' + l + '90', | |||
| 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}) | |||
| }, | |||
| } | |||
| }, | |||
| } | |||
| }), | |||
| @@ -2,9 +2,9 @@ import {Event, Mesh, Vector3} from 'three' | |||
| import {IMaterial} from '../IMaterial' | |||
| import {objectHasOwn} from 'ts-browser-helpers' | |||
| import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject' | |||
| import {copyObject3DUserData} from '../../utils/serialization' | |||
| import {copyObject3DUserData} from '../../utils' | |||
| import {IGeometry, IGeometryEvent} from '../IGeometry' | |||
| import {Box3B} from '../../three/math/Box3B' | |||
| import {Box3B} from '../../three' | |||
| import {makeIObject3DUiConfig} from './IObjectUi' | |||
| import {iGeometryCommons} from '../geometry/iGeometryCommons' | |||
| import {iMaterialCommons} from '../material/iMaterialCommons' | |||
| @@ -13,7 +13,7 @@ import {ILight} from '../light/ILight' | |||
| export const iObjectCommons = { | |||
| setDirty: function(this: IObject3D, options?: IObjectSetDirtyOptions): void { | |||
| 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') | |||
| }, | |||
| @@ -7,6 +7,7 @@ import {generateUiConfig, uiButton, uiDropdown, uiInput, UiObjectConfig, uiSlide | |||
| import {EasingFunctions, EasingFunctionType} from '../../utils' | |||
| import {CameraView, ICamera, ICameraView, PerspectiveCamera2} from '../../core' | |||
| import {AnimationResult, PopmotionPlugin} from './PopmotionPlugin' | |||
| import {InteractionPromptPlugin} from '../interaction/InteractionPromptPlugin' | |||
| export interface CameraViewPluginOptions{duration?: number, ease?: EasingFunctionType, interpolateMode?: 'spherical'|'linear'} | |||
| @@ -257,6 +258,11 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| return | |||
| } | |||
| const interactionPrompt = this._viewer?.getPlugin(InteractionPromptPlugin) | |||
| if (interactionPrompt && interactionPrompt.animationRunning) { | |||
| await interactionPrompt.stopAnimation({reset: true}) | |||
| } | |||
| this._currentView = view | |||
| this._animating = true | |||
| @@ -337,10 +343,11 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC | |||
| const cam = this._viewer.scene.mainCamera | |||
| 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 ) | |||
| 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 dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2)) | |||
| cameraZ = Math.max(dx, dy) | |||
| @@ -188,10 +188,10 @@ export class PopmotionPlugin extends AViewerPluginSync<''> { | |||
| driver: this.defaultDriver, | |||
| ...options, | |||
| onUpdate: !isBool ? options.onUpdate : undefined, | |||
| onComplete: ()=>{ | |||
| onComplete: async()=>{ | |||
| try { | |||
| if (isBool) options.onUpdate?.(options.to as any) | |||
| options.onComplete && options.onComplete() | |||
| options.onComplete && await options.onComplete() | |||
| } catch (e: any) { | |||
| if (!end2()) return | |||
| reject(e) | |||
| @@ -200,9 +200,9 @@ export class PopmotionPlugin extends AViewerPluginSync<''> { | |||
| if (!end2()) return | |||
| resolve() | |||
| }, | |||
| onStop: ()=>{ | |||
| onStop: async()=>{ | |||
| try { | |||
| options.onStop && options.onStop() | |||
| options.onStop && await options.onStop() | |||
| } catch (e: any) { | |||
| if (!end2()) return | |||
| reject(e) | |||
| @@ -236,9 +236,11 @@ export class BaseGroundPlugin<TEvent extends string = ''> extends AViewerPluginS | |||
| 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) { | |||
| this._geometry.attributes.uv2 = (this._geometry.attributes.uv as any as BufferAttribute | InterleavedBufferAttribute).clone() | |||
| this._geometry.attributes.uv2.needsUpdate = true | |||
| @@ -246,7 +248,6 @@ export class BaseGroundPlugin<TEvent extends string = ''> extends AViewerPluginS | |||
| if (this._mesh) this._mesh.geometry = this._geometry | |||
| } | |||
| protected _createMaterial(material?: PhysicalMaterial): PhysicalMaterial { | |||
| if (!material) material = new PhysicalMaterial({ | |||
| name: 'BaseGroundMaterial', | |||
| @@ -36,10 +36,14 @@ export class MaterialConfiguratorBasePlugin extends AViewerPluginSync<''> { | |||
| super.onAdded(viewer) | |||
| // 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._picking?.addEventListener('selectedObjectChanged', this._refreshUiConfig) | |||
| viewer.addEventListener('preFrame', this._refreshUi) | |||
| } | |||
| @@ -256,22 +260,22 @@ export class MaterialConfiguratorBasePlugin extends AViewerPluginSync<''> { | |||
| this.variations.splice(this.variations.indexOf(variation), 1) | |||
| 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) { | |||
| 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) { | |||
| variation = this.createVariation(material) | |||
| variation = this.createVariation(material, variationKey) | |||
| } | |||
| variation.materials.push(clone) | |||
| this.refreshUi() | |||
| } | |||
| } | |||
| createVariation(material: IMaterial) { | |||
| createVariation(material: IMaterial, variationKey?: string) { | |||
| 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', | |||
| preview: 'generate:sphere', | |||
| materials: [], | |||
| @@ -13,7 +13,7 @@ import {snapObject} from '../../three' | |||
| * 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. | |||
| * 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. | |||
| * | |||
| @@ -117,6 +117,7 @@ export class SwitchNodeBasePlugin extends AViewerPluginSync<''> { | |||
| if (!this.enabled) return false | |||
| if (!this._viewer) return false | |||
| this._uiNeedRefresh = false | |||
| if (this.autoSnapIcons) this.snapIcons() | |||
| this.refreshUiConfig() | |||
| return true | |||
| @@ -169,6 +170,34 @@ export class SwitchNodeBasePlugin extends AViewerPluginSync<''> { | |||
| 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 = { | |||
| label: 'Switch Node Plugin', | |||
| type: 'folder', | |||
| @@ -226,20 +226,25 @@ export class InteractionPromptPlugin extends AViewerPluginSync<''> { | |||
| // } | |||
| } | |||
| @uiButton() stopAnimation = () => { | |||
| @uiButton() stopAnimation = async({reset = true}: {reset?: boolean} = {}) => { | |||
| if (!this._viewer || !this.cursorEl) return // dont check for enabled here. | |||
| this.animationRunning = false | |||
| 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) | |||
| // if (this.interactionsDisabled) { | |||
| // this._viewer.scene.mainCamera.interactionsEnabled = true | |||
| // this.interactionsDisabled = false | |||
| // } | |||
| return this._viewer.doOnce('postFrame') | |||
| } | |||
| private _pointerDown = () => { | |||
| 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() | |||
| } | |||
| private _x = 0 | |||
| @@ -86,9 +86,11 @@ export class LoadingScreenPlugin extends AAssetManagerProcessStatePlugin { | |||
| @onChange(LoadingScreenPlugin.prototype.refresh) | |||
| @serialize() textColor = '#222222' | |||
| static LS_DEFAULT_LOGO = 'https://threepipe.org/logo.svg' | |||
| @uiInput('Logo Image') | |||
| @onChange(LoadingScreenPlugin.prototype.refresh) | |||
| @serialize() logoImage = 'https://threepipe.org/logo.svg' | |||
| @serialize() logoImage = LoadingScreenPlugin.LS_DEFAULT_LOGO | |||
| private _isPreviewing = false | |||
| private _previewState = new Map([['file.glb', {state: 'downloading', progress: 50}], ['environment.hdr', {state: 'adding'}]]) | |||
| @@ -127,6 +129,10 @@ export class LoadingScreenPlugin extends AAssetManagerProcessStatePlugin { | |||
| private _isHidden = false | |||
| get visible() { | |||
| return !this._isHidden | |||
| } | |||
| async hide() { | |||
| this._isHidden = true | |||
| this._mainDiv.style.opacity = '0' | |||
| @@ -207,16 +213,22 @@ export class LoadingScreenPlugin extends AAssetManagerProcessStatePlugin { | |||
| errors.length === processState.size && this.hideOnOnlyErrors)) { | |||
| this.hideDelay ? this.hideWithDelay() : this.hide() | |||
| } 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() | |||
| } | |||
| } | |||
| } | |||
| // disables showOnSceneEmpty | |||
| isEditor = false | |||
| private _sceneUpdate = (e: any) => { | |||
| if (!this._viewer) return | |||
| if (!e.hierarchyChanged) return | |||
| const sceneObjects = this._viewer.scene.modelRoot.children | |||
| if (sceneObjects.length === 0 && this.showOnSceneEmpty) { | |||
| if (sceneObjects.length === 0 && this.showOnSceneEmpty && !this.isEditor) { | |||
| this.show() | |||
| } | |||
| // console.log(sceneObjects.length) | |||
| @@ -27,6 +27,9 @@ export class FrameFadePlugin | |||
| dependencies = [ProgressivePlugin] | |||
| // disables fadeOn... options but not serialized | |||
| isEditor = false | |||
| @serialize() @uiToggle() fadeOnActiveCameraChange = true | |||
| @serialize() @uiToggle() fadeOnMaterialUpdate = true | |||
| @serialize() @uiToggle() fadeOnSceneUpdate = true | |||
| @@ -102,13 +105,13 @@ export class FrameFadePlugin | |||
| } | |||
| 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)=> | |||
| 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)=> | |||
| 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)=> | |||
| ev.frameFade && this.startTransition(ev.fadeDuration || 500) | |||
| ev.frameFade && !this.isEditor && this.startTransition(ev.fadeDuration || 500) | |||
| private _onPointerMove = (ev: PointerEvent)=> { | |||
| const canvas = this._viewer?.canvas | |||
| @@ -1,6 +1,6 @@ | |||
| import {Matrix4, Texture, TextureDataType, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget} from 'three' | |||
| import {ExtendedShaderPass, IPassID, IPipelinePass} from '../../postprocessing' | |||
| import {type IViewerEvent, ThreeViewer} from '../../viewer' | |||
| import {ThreeViewer} from '../../viewer' | |||
| import {PipelinePassPlugin} from '../base/PipelinePassPlugin' | |||
| import {uiConfig, uiFolderContainer, uiImage, uiSlider} from 'uiconfig.js' | |||
| import {ICamera, IMaterial, IRenderManager, IScene, IWebGLRenderer, PhysicalMaterial} from '../../core' | |||
| @@ -108,22 +108,16 @@ export class SSAOPlugin | |||
| onAdded(viewer: ThreeViewer) { | |||
| 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() | |||
| 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 { | |||
| viewer.removeEventListener('addPlugin', this._onPluginAdd) | |||
| this._disposeTarget() | |||
| return super.onRemove(viewer) | |||
| } | |||
| @@ -1,5 +1,5 @@ | |||
| import {type AViewerPlugin, AViewerPluginSync} from '../../viewer/AViewerPlugin' | |||
| import type {IViewerEvent, ThreeViewer} from '../../viewer' | |||
| import type {ThreeViewer} from '../../viewer' | |||
| import {MaterialExtension} from '../../materials' | |||
| import {Shader, Vector4, WebGLRenderer} from 'three' | |||
| import {IMaterial} from '../../core' | |||
| @@ -81,21 +81,15 @@ export abstract class AScreenPassExtensionPlugin<T extends string> extends AView | |||
| onAdded(viewer: ThreeViewer) { | |||
| 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]) | |||
| } | |||
| 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) { | |||
| viewer.removeEventListener('addPlugin', this._onPluginAdd) | |||
| viewer.getPlugin(GBufferPlugin)?.unregisterGBufferUpdater(this.constructor.PluginType) | |||
| viewer.renderManager.screenPass.material.unregisterMaterialExtensions([this]) | |||
| super.onRemove(viewer) | |||
| @@ -5,12 +5,11 @@ import { | |||
| FloatType, | |||
| HalfFloatType, | |||
| LinearSRGBColorSpace, | |||
| Texture, | |||
| Texture, TextureImageData, | |||
| TextureDataType, | |||
| UnsignedByteType, | |||
| WebGLRenderer, | |||
| } from 'three' | |||
| import {TextureImageData} from 'three/src/textures/types' | |||
| import {canvasFlipY, LinearToSRGB} from 'ts-browser-helpers' | |||
| export function getTextureDataType(renderer?: WebGLRenderer): TextureDataType { | |||
| @@ -762,8 +762,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| if (oldType) this.plugins[oldType] = p | |||
| await p.onAdded(this) | |||
| this.dispatchEvent({type: 'addPlugin', target: this, plugin: p}) | |||
| this.setDirty(p) | |||
| this._onPluginAdd(p) | |||
| return p | |||
| } | |||
| @@ -792,8 +791,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| if (oldType && this.plugins[oldType]) this.console.error('Plugin type mismatch') | |||
| if (oldType) this.plugins[oldType] = p | |||
| p.onAdded(this) | |||
| this.dispatchEvent({type: 'addPlugin', target: this, plugin: p}) | |||
| this.setDirty(p) | |||
| this._onPluginAdd(p) | |||
| return p | |||
| } | |||
| @@ -823,10 +821,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| const type = p.constructor.PluginType | |||
| if (!this.plugins[type]) return | |||
| 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) | |||
| } | |||
| /** | |||
| @@ -838,7 +833,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| const type = p.constructor.PluginType | |||
| if (!this.plugins[type]) return | |||
| p.onRemove(this) | |||
| this.dispatchEvent({type: 'removePlugin', target: this, plugin: p}) | |||
| this._onPluginRemove(p, dispose) | |||
| delete this.plugins[type] | |||
| if (dispose) p.dispose() | |||
| this.setDirty(p) | |||
| @@ -988,6 +983,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| if (filter && filter.length === 0) return [] | |||
| return Object.entries(this.plugins).map(p=> { | |||
| 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`) | |||
| return p[1].serializeWithViewer !== false ? p[1].toJSON?.(meta) : undefined | |||
| }).filter(p=> !!p) | |||
| @@ -1005,12 +1001,13 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| this.console.warn('Invalid plugin to import ', p) | |||
| return | |||
| } | |||
| if (this.serializePluginsIgnored.includes(p.type)) return | |||
| const plugin = this.getPlugin(p.type) | |||
| if (!plugin) { | |||
| // this.console.warn(`Plugin of type ${p.type} is not added, cannot deserialize`) | |||
| return | |||
| } | |||
| plugin.fromJSON?.(p, meta) | |||
| plugin.fromJSON && plugin.fromJSON(p, meta) | |||
| }) | |||
| return this | |||
| } | |||
| @@ -1405,4 +1402,74 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| 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[] = [] | |||
| } | |||
| @@ -9,7 +9,7 @@ import path from 'node:path'; | |||
| const isProd = process.env.NODE_ENV === 'production' | |||
| const { name, version, author } = packageJson | |||
| const {main, module, browser} = packageJson['clean-package'].replace | |||
| const {main, module, browser} = packageJson | |||
| export default defineConfig({ | |||
| optimizeDeps: { | |||