| bubbleToParent?: boolean | bubbleToParent?: boolean | ||||
| uiChangeEvent?: ChangeEvent | uiChangeEvent?: ChangeEvent | ||||
| } | } | ||||
| select: { // todo remove? | |||||
| ui?: boolean | |||||
| // focusCamera?: boolean // todo ? | |||||
| bubbleToObject?: boolean | |||||
| bubbleToParent?: boolean | |||||
| material: IMaterial | |||||
| value?: /* IObject3D | */ IMaterial | null // todo is this required? | |||||
| source?: string // who is triggering the event. so that recursive events can be prevented | |||||
| } /* & IObjectSetDirtyOptions*/ | |||||
| } | } | ||||
| } | } | ||||
| */ | */ | ||||
| needsUpdate?: boolean, | needsUpdate?: boolean, | ||||
| change?: string | keyof IMaterial | |||||
| [key: string]: any | [key: string]: any | ||||
| } | } | ||||
| export interface IMaterialUserData extends IImportResultUserData{ | export interface IMaterialUserData extends IImportResultUserData{ |
| import {IMaterial, IMaterialEventMap} from './IMaterial' | |||||
| import {IMaterial, IMaterialEventMap, IMaterialSetDirtyOptions} from './IMaterial' | |||||
| import {Box3, EventListener2, Object3D, Object3DEventMap, Sphere, Vector3} from 'three' | import {Box3, EventListener2, Object3D, Object3DEventMap, Sphere, Vector3} from 'three' | ||||
| import {ChangeEvent, IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | import {ChangeEvent, IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | ||||
| import {IGeometry, IGeometryEventMap} from './IGeometry' | |||||
| import {IGeometry, IGeometryEventMap, IGeometrySetDirtyOptions} from './IGeometry' | |||||
| import {IImportResultUserData} from '../assetmanager' | import {IImportResultUserData} from '../assetmanager' | ||||
| import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js' | import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js' | ||||
| import {ICamera, type ICameraSetDirtyOptions} from './ICamera' | import {ICamera, type ICameraSetDirtyOptions} from './ICamera' | ||||
| declare module 'three'{ | declare module 'three'{ | ||||
| export interface Object3DEventMap{ | export interface Object3DEventMap{ | ||||
| select: { // todo | |||||
| select: { // todo remove? | |||||
| ui?: boolean | ui?: boolean | ||||
| focusCamera?: boolean | focusCamera?: boolean | ||||
| bubbleToParent?: boolean | bubbleToParent?: boolean | ||||
| object: IObject3D | object: IObject3D | ||||
| value?: IObject3D /* | Material*/ // todo is this required? | |||||
| value?: IObject3D|null /* | Material*/ // todo is this required? | |||||
| source?: string // who is triggering the event. so that recursive events can be prevented | source?: string // who is triggering the event. so that recursive events can be prevented | ||||
| } /* & IObjectSetDirtyOptions*/ | } /* & IObjectSetDirtyOptions*/ | ||||
| materialUpdate: { | materialUpdate: { | ||||
| // object: IObject3D | // object: IObject3D | ||||
| material: IMaterial|IMaterial[] | material: IMaterial|IMaterial[] | ||||
| } | |||||
| } & IMaterialSetDirtyOptions | |||||
| objectUpdate: { | objectUpdate: { | ||||
| object: IObject3D | object: IObject3D | ||||
| change?: string | |||||
| args?: any[] | args?: any[] | ||||
| bubbleToParent: boolean | bubbleToParent: boolean | ||||
| } | |||||
| } & Omit<IObjectSetDirtyOptions, 'bubbleToParent'> | |||||
| textureUpdate: { | textureUpdate: { | ||||
| // object: IObject3D | // object: IObject3D | ||||
| // todo | // todo | ||||
| geometry: IGeometry | geometry: IGeometry | ||||
| // oldGeometry: IGeometry | // oldGeometry: IGeometry | ||||
| bubbleToParent: boolean | bubbleToParent: boolean | ||||
| } | |||||
| } & IGeometrySetDirtyOptions | |||||
| added: { | added: { | ||||
| // object: IObject3D | // object: IObject3D | ||||
| // todo | // todo | ||||
| camera?: ICamera | null | camera?: ICamera | null | ||||
| bubbleToParent: boolean | bubbleToParent: boolean | ||||
| // object: IObject3D | // object: IObject3D | ||||
| } | } | ||||
| cameraUpdate: { | cameraUpdate: { | ||||
| ui?: boolean | ui?: boolean | ||||
| // object: IObject3D | // object: IObject3D | ||||
| bubbleToParent: boolean | bubbleToParent: boolean | ||||
| // todo | // todo | ||||
| } & ICameraSetDirtyOptions | |||||
| } & Omit<ICameraSetDirtyOptions, 'bubbleToParent'> | |||||
| } | } | ||||
| // Record<keyof IObject3DEventMap0, IObject3DEventMap0[keyof IObject3DEventMap0] & { | // Record<keyof IObject3DEventMap0, IObject3DEventMap0[keyof IObject3DEventMap0] & { | ||||
| // // bubbleToParent?: boolean | // // bubbleToParent?: boolean | ||||
| isScene?: boolean | isScene?: boolean | ||||
| // isHelper?: boolean | // isHelper?: boolean | ||||
| isWidget?: boolean | isWidget?: boolean | ||||
| isPoints?: boolean | |||||
| readonly isObject3D: true | readonly isObject3D: true | ||||
| material?: IMaterial | IMaterial[] | material?: IMaterial | IMaterial[] |
| { | { | ||||
| type: 'checkbox', | type: 'checkbox', | ||||
| property: [material, 'transparent'], | property: [material, 'transparent'], | ||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev}), | |||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev, change: 'transparent'}), | |||||
| }, | }, | ||||
| { | { | ||||
| type: 'dropdown', | type: 'dropdown', | ||||
| { | { | ||||
| type: 'checkbox', | type: 'checkbox', | ||||
| property: [material, 'depthTest'], | property: [material, 'depthTest'], | ||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev}), | |||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev, change: 'depthTest'}), | |||||
| }, | }, | ||||
| { | { | ||||
| type: 'checkbox', | type: 'checkbox', | ||||
| property: [material, 'depthWrite'], | property: [material, 'depthWrite'], | ||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev}), | |||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev, change: 'depthWrite'}), | |||||
| }, | }, | ||||
| { | { | ||||
| type: 'checkbox', | type: 'checkbox', | ||||
| property: [material, 'colorWrite'], | property: [material, 'colorWrite'], | ||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev}), | |||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev, change: 'colorWrite'}), | |||||
| }, | }, | ||||
| { | { | ||||
| type: 'slider', | type: 'slider', | ||||
| setValue: (v: boolean)=>{ | setValue: (v: boolean)=>{ | ||||
| if (!v && !material.userData.renderToGBuffer) return | if (!v && !material.userData.renderToGBuffer) return | ||||
| material.userData.renderToGBuffer = v | material.userData.renderToGBuffer = v | ||||
| material.setDirty() | |||||
| material.setDirty({change: 'userData', key: 'renderToGBuffer'}) | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| setValue: (v: boolean)=>{ | setValue: (v: boolean)=>{ | ||||
| if (!v && !material.userData.renderToDepth) return | if (!v && !material.userData.renderToDepth) return | ||||
| material.userData.renderToDepth = v | material.userData.renderToDepth = v | ||||
| material.setDirty() | |||||
| material.setDirty({change: 'userData', key: 'renderToDepth'}) | |||||
| }, | }, | ||||
| }, | }, | ||||
| material.isPhysicalMaterial ? { | material.isPhysicalMaterial ? { | ||||
| getValue: ()=>material.userData.inverseAlphaMap === true, | getValue: ()=>material.userData.inverseAlphaMap === true, | ||||
| setValue: (v: boolean)=>{ | setValue: (v: boolean)=>{ | ||||
| material.userData.inverseAlphaMap = v ? v : undefined | material.userData.inverseAlphaMap = v ? v : undefined | ||||
| material.setDirty() | |||||
| material.setDirty({change: 'userData', key: 'inverseAlphaMap'}) | |||||
| }, | }, | ||||
| } : {}, | } : {}, | ||||
| ], | ], | ||||
| setValue: (v: string)=>{ | setValue: (v: string)=>{ | ||||
| material.userData.envMapSlotKey = v | material.userData.envMapSlotKey = v | ||||
| if (!v) delete material.userData.envMapSlotKey | if (!v) delete material.userData.envMapSlotKey | ||||
| material.setDirty() | |||||
| material.setDirty({change: 'userData', key: 'envMapSlotKey'}) | |||||
| }, | }, | ||||
| }, | }, | ||||
| ], | ], | ||||
| bounds: [0, 500], | bounds: [0, 500], | ||||
| label: 'Thickness0', | label: 'Thickness0', | ||||
| property: [material.iridescenceThicknessRange, '0'], | property: [material.iridescenceThicknessRange, '0'], | ||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev}), | |||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev, change: 'iridescenceThicknessRange', key: '0'}), | |||||
| }, | }, | ||||
| { | { | ||||
| type: 'slider', | type: 'slider', | ||||
| bounds: [0, 500], | bounds: [0, 500], | ||||
| label: 'Thickness1', | label: 'Thickness1', | ||||
| property: [material.iridescenceThicknessRange, '1'], | property: [material.iridescenceThicknessRange, '1'], | ||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev}), | |||||
| onChange: (ev)=>material.setDirty({uiChangeEvent: ev, change: 'iridescenceThicknessRange', key: '1'}), | |||||
| }, | }, | ||||
| { | { | ||||
| type: 'image', | type: 'image', |
| expanded: true, | expanded: true, | ||||
| onChange: (ev)=>{ | onChange: (ev)=>{ | ||||
| if (!ev.config || ev.config.onChange) return | if (!ev.config || ev.config.onChange) return | ||||
| let key = Array.isArray(ev.config.property) ? ev.config.property[1] : ev.config.property | |||||
| key = typeof key === 'string' ? key : undefined | |||||
| // todo set needsUpdate true only for properties that require it like maps. | // todo set needsUpdate true only for properties that require it like maps. | ||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last}) | |||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last, change: key}) | |||||
| }, | }, | ||||
| children: [ | children: [ | ||||
| ...iMaterialUI.base(this), | ...iMaterialUI.base(this), |
| // m.computeLineDistances() | // m.computeLineDistances() | ||||
| // } | // } | ||||
| // }) | // }) | ||||
| let key = Array.isArray(ev.config.property) ? ev.config.property[1] : ev.config.property | |||||
| key = typeof key === 'string' ? key : undefined | |||||
| // todo set needsUpdate true only for properties that require it like maps. | // todo set needsUpdate true only for properties that require it like maps. | ||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last}) | |||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last, change: key}) | |||||
| }, | }, | ||||
| children: [ | children: [ | ||||
| ...generateUiConfig(this) || [], | ...generateUiConfig(this) || [], |
| expanded: true, | expanded: true, | ||||
| onChange: (ev)=>{ | onChange: (ev)=>{ | ||||
| if (!ev.config || ev.config.onChange) return | if (!ev.config || ev.config.onChange) return | ||||
| let key = Array.isArray(ev.config.property) ? ev.config.property[1] : ev.config.property | |||||
| key = typeof key === 'string' ? key : undefined | |||||
| // todo set needsUpdate true only for properties that require it like maps. | // todo set needsUpdate true only for properties that require it like maps. | ||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last}) | |||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last, change: key}) | |||||
| }, | }, | ||||
| children: [ | children: [ | ||||
| ...generateUiConfig(this), | ...generateUiConfig(this), |
| onChange: (ev)=>{ | onChange: (ev)=>{ | ||||
| if (!ev.config || ev.config.onChange) return | if (!ev.config || ev.config.onChange) return | ||||
| // todo frameFade | // todo frameFade | ||||
| let key = Array.isArray(ev.config.property) ? ev.config.property[1] : ev.config.property | |||||
| key = typeof key === 'string' ? key : undefined | |||||
| // todo set needsUpdate true only for properties that require it like maps. | // todo set needsUpdate true only for properties that require it like maps. | ||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last}) | |||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last, change: key}) | |||||
| }, | }, | ||||
| children: [ | children: [ | ||||
| ...iMaterialUI.base(this), | ...iMaterialUI.base(this), |
| expanded: true, | expanded: true, | ||||
| onChange: (ev)=>{ | onChange: (ev)=>{ | ||||
| if (!ev.config || ev.config.onChange) return | if (!ev.config || ev.config.onChange) return | ||||
| let key = Array.isArray(ev.config.property) ? ev.config.property[1] : ev.config.property | |||||
| key = typeof key === 'string' ? key : undefined | |||||
| // todo set needsUpdate true only for properties that require it like maps. | // todo set needsUpdate true only for properties that require it like maps. | ||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last}) | |||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last, change: key}) | |||||
| }, | }, | ||||
| children: [ | children: [ | ||||
| { | { |
| expanded: true, | expanded: true, | ||||
| onChange: (ev)=>{ | onChange: (ev)=>{ | ||||
| if (!ev.config || ev.config.onChange) return | if (!ev.config || ev.config.onChange) return | ||||
| let key = Array.isArray(ev.config.property) ? ev.config.property[1] : ev.config.property | |||||
| key = typeof key === 'string' ? key : undefined | |||||
| // todo set needsUpdate true only for properties that require it like maps. | // todo set needsUpdate true only for properties that require it like maps. | ||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last}) | |||||
| this.setDirty({uiChangeEvent: ev, needsUpdate: !!ev.last, refreshUi: !!ev.last, change: key}) | |||||
| }, | }, | ||||
| children: [ | children: [ | ||||
| ...iMaterialUI.base(this), | ...iMaterialUI.base(this), |
| 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 | ||||
| if (options?.last !== false) this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) | |||||
| if (options?.last !== false && options?.refreshUi !== 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 { | ||||
| superDispatchEvent.call(this, event) | superDispatchEvent.call(this, event) | ||||
| const type = event.type | const type = event.type | ||||
| if ((event as IMaterialEventMap['materialUpdate']).bubbleToObject && ( | if ((event as IMaterialEventMap['materialUpdate']).bubbleToObject && ( | ||||
| type === 'beforeDeserialize' || type === 'materialUpdate' || type === 'textureUpdate' // todo - add more events | |||||
| type === 'beforeDeserialize' || type === 'materialUpdate' || type === 'textureUpdate' || type === 'select' // todo - add more events | |||||
| )) { | )) { | ||||
| this.appliedMeshes.forEach(m => m.dispatchEvent({...event, material: this, type})) | this.appliedMeshes.forEach(m => m.dispatchEvent({...event, material: this, type})) | ||||
| } | } |
| expanded: true, | expanded: true, | ||||
| onChange: (ev)=>{ | onChange: (ev)=>{ | ||||
| if (!ev.config || ev.config.onChange) return | if (!ev.config || ev.config.onChange) return | ||||
| this.setDirty({uiChangeEvent: ev, refreshScene: false, refreshUi: true}) | |||||
| let key = Array.isArray(ev.config.property) ? ev.config.property[1] : ev.config.property | |||||
| key = typeof key === 'string' ? key : undefined | |||||
| this.setDirty({uiChangeEvent: ev, refreshScene: false, refreshUi: !!ev.last, change: key}) | |||||
| }, | }, | ||||
| children: [ | children: [ | ||||
| { | { | ||||
| label: 'Name', | label: 'Name', | ||||
| property: [this, 'name'], | property: [this, 'name'], | ||||
| onChange: (e)=>{ | onChange: (e)=>{ | ||||
| if (e.last) this.setDirty?.({uiChangeEvent: e, refreshScene: true, frameFade: false, refreshUi: true}) | |||||
| if (e.last) this.setDirty?.({uiChangeEvent: e, refreshScene: true, frameFade: false, refreshUi: true, change: 'name'}) | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Pivot to Node Center', | label: 'Pivot to Node Center', | ||||
| tags: ['context-menu', 'interaction'], | |||||
| value: async()=>{ | value: async()=>{ | ||||
| const res = await ThreeViewer.Dialog.confirm('Pivot to Center: Adjust the pivot to bounding box center. The object will rotate around the new pivot, are you sure you want to proceed?') | const res = await ThreeViewer.Dialog.confirm('Pivot to Center: Adjust the pivot to bounding box center. The object will rotate around the new pivot, are you sure you want to proceed?') | ||||
| if (!res) return | if (!res) return | ||||
| return this.pivotToBoundsCenter?.(true) // return value is the undo function | return this.pivotToBoundsCenter?.(true) // return value is the undo function | ||||
| }, | }, | ||||
| }, | }, | ||||
| { | |||||
| type: 'button', | |||||
| label: 'Duplicate Object', | |||||
| tags: ['context-menu'], | |||||
| value: async()=>{ | |||||
| const parent = this.parent | |||||
| const clone = this.clone(true) as IObject3D | |||||
| clone.name = this.name + ' (copy)' | |||||
| return { | |||||
| action: ()=>{ | |||||
| if (parent && !clone.parent) | |||||
| parent.add(clone) // todo same index? | |||||
| }, | |||||
| undo: ()=>{ | |||||
| if (clone.parent === parent) | |||||
| clone.removeFromParent() | |||||
| }, | |||||
| } | |||||
| }, | |||||
| }, | |||||
| { | { | ||||
| type: 'button', | type: 'button', | ||||
| label: 'Delete Object', | label: 'Delete Object', |
| @uiToggle() clipBackground = false | @uiToggle() clipBackground = false | ||||
| beforeRender(_: IScene, _1: ICamera, renderManager: ViewerRenderManager) { | beforeRender(_: IScene, _1: ICamera, renderManager: ViewerRenderManager) { | ||||
| this.material.uniforms.tTransparent.value = renderManager.renderPass.preserveTransparentTarget ? renderManager.renderPass.transparentTarget?.texture || null : null | |||||
| this.material.defines.HAS_TRANSPARENT_TARGET = this.material.uniforms.tTransparent.value ? 1 : undefined | |||||
| if (!this.material.defines.HAS_TRANSPARENT_TARGET) delete this.material.defines.HAS_TRANSPARENT_TARGET | |||||
| if (this.material.uniforms.tTransparent) { | |||||
| this.material.uniforms.tTransparent.value = renderManager.renderPass.preserveTransparentTarget ? renderManager.renderPass.transparentTarget?.texture || null : null | |||||
| this.material.defines.HAS_TRANSPARENT_TARGET = this.material.uniforms.tTransparent.value ? 1 : undefined | |||||
| if (!this.material.defines.HAS_TRANSPARENT_TARGET) delete this.material.defines.HAS_TRANSPARENT_TARGET | |||||
| } | |||||
| } | } | ||||
| setDirty() { | setDirty() { |