| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- import {AViewerPluginSync, ThreeViewer} from '../../viewer'
- import {PickingPlugin} from '../interaction/PickingPlugin'
- import {imageBitmapToBase64, makeColorSvgCircle, serialize} from 'ts-browser-helpers'
- import {UiObjectConfig} from 'uiconfig.js'
- import {IMaterial, PhysicalMaterial} from '../../core'
- import {MaterialPreviewGenerator} from '../../three'
- import {Color} from 'three'
-
- /**
- * Material Configurator Plugin (Base)
- * This plugin allows you to create variations of materials mapped to material names or uuids in the scene.
- * These variations can be applied to the materials in the scene. (This copies the properties to the same material instances instead of assigning new materials)
- * The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations.
- *
- * See `MaterialConfiguratorPlugin` in [plugin-configurator](https://threepipe.org/plugins/configurator/docs/index.html) for example on inheriting with a custom UI renderer.
- *
- * @category Plugins
- */
- export class MaterialConfiguratorBasePlugin extends AViewerPluginSync<''> {
- enabled = true
- public static PluginType = 'MaterialConfiguratorPlugin'
- private _picking: PickingPlugin | undefined
- protected _previewGenerator: MaterialPreviewGenerator | undefined
- private _uiNeedRefresh = false
-
- constructor() {
- super()
- this.addEventListener('deserialize', this.refreshUi)
- this.refreshUi = this.refreshUi.bind(this)
- this._refreshUi = this._refreshUi.bind(this)
- this._refreshUiConfig = this._refreshUiConfig.bind(this)
- }
-
- onAdded(viewer: ThreeViewer) {
- super.onAdded(viewer)
-
- // todo subscribe to plugin add event if picking is not added yet.
- this._picking = viewer.getPlugin<PickingPlugin>('Picking')
- this._previewGenerator = new MaterialPreviewGenerator()
-
- this._picking?.addEventListener('selectedObjectChanged', this._refreshUiConfig)
- viewer.addEventListener('preFrame', this._refreshUi)
- }
-
- /**
- * Apply all variations(by selected index or first item) when a config is loaded
- */
- applyOnLoad = true
-
- /**
- * Reapply all selected variations again.
- * Useful when the scene is loaded or changed and the variations are not applied.
- */
- reapplyAll() {
- this.variations.forEach(v => this.applyVariation(v, v.materials[v.selectedIndex ?? 0].uuid))
- }
-
- fromJSON(data: any, meta?: any): this | Promise<this | null> | null {
- this.variations = []
- if (!super.fromJSON(data, meta)) return null // its not a promise
- if (data.applyOnLoad === undefined) { // old files
- this.applyOnLoad = false
- }
- if (this.applyOnLoad) this.reapplyAll()
- return this
- }
-
- onRemove(viewer: ThreeViewer) {
- this._previewGenerator?.dispose()
- this._previewGenerator = undefined
-
- this._picking?.removeEventListener('selectedObjectChanged', this._refreshUiConfig)
- this.removeEventListener('deserialize', this.refreshUi)
- viewer.removeEventListener('preFrame', this._refreshUi)
-
- this._picking = undefined
-
- return super.onRemove(viewer)
- }
-
- findVariation(uuid?: string): MaterialVariations|undefined {
- return uuid ? this.variations.find(v => v.uuid === uuid) : undefined
- }
-
- getSelectedVariation(): MaterialVariations|undefined {
- return this.findVariation(this._selectedMaterial()?.uuid) || this.findVariation(this._selectedMaterial()?.name)
- }
-
- /**
- * Apply a material variation based on index or uuid.
- * @param variations
- * @param matUuidOrIndex
- */
- applyVariation(variations: MaterialVariations, matUuidOrIndex: string|number): boolean {
- const m = this._viewer?.materialManager
- if (!m) return false
- const material = typeof matUuidOrIndex === 'string' ?
- variations.materials.find(m1 => m1.uuid === matUuidOrIndex) :
- variations.materials[matUuidOrIndex]
- if (!material) return false
- variations.selectedIndex = variations.materials.indexOf(material)
- return m.applyMaterial(material, variations.uuid)
- }
-
- /**
- * Get the preview for a material variation
- * Should be called from preFrame ideally. (or preRender but set viewerSetDirty = false)
- * @param preview - Type of preview. Could be generate:sphere, generate:cube, color, map, emissive, etc.
- * @param material - Material or index of the material in the variation.
- * @param viewerSetDirty - call viewer.setDirty() after setting the preview. So that the preview is cleared from the canvas.
- */
- getPreview(material: IMaterial, preview: string, viewerSetDirty = true): string {
- if (!this._viewer) return ''
- // const m = typeof material === 'number' ? variation.materials[material] : material
- const m = material
- if (!m) return ''
- let image = ''
- if (!preview.startsWith('generate:')) {
- const pp = (m as any)[preview] || '#ff00ff'
- image = pp.image ? imageBitmapToBase64(pp.image, 100) : ''
- if (!image.length) image = makeColorSvgCircle(pp.isColor ? (pp as Color).getHexString() : pp)
- } else {
- image = this._previewGenerator!.generate(m,
- this._viewer.renderManager.renderer,
- this._viewer.scene.environment,
- preview.split(':')[1]
- )
- }
- if (viewerSetDirty) this._viewer.setDirty() // because called from preFrame
- return image
- }
-
- /**
- * Refreshes the UI in the next frame
- */
- refreshUi(): void {
- if (!this.enabled || !this._viewer) return
- this._uiNeedRefresh = true
- }
-
- private _refreshUiConfig() {
- if (!this.enabled) return
- this.uiConfig.uiRefresh?.() // don't call this.refreshUi here
- }
-
- // must be called from preFrame
- protected async _refreshUi(): Promise<boolean> {
- if (!this.enabled) return false
- if (!this._viewer || !this._uiNeedRefresh) return false
- this._uiNeedRefresh = false
- this._refreshUiConfig()
- return true
- }
-
- @serialize()
- variations: MaterialVariations[] = []
-
- private _selectedMaterial = () => (this._picking?.getSelectedObject()?.material || undefined) as IMaterial | undefined
- uiConfig: UiObjectConfig = {
- label: 'Material Configurator',
- type: 'folder',
- // expanded: true,
- children: [
- () => [
- {
- type: 'input',
- label: 'uuid',
- property: [this._selectedMaterial(), 'uuid'],
- hidden: () => !this._selectedMaterial(),
- disabled: true,
- },
- {
- type: 'input',
- label: 'mapping',
- hidden: () => !this._selectedMaterial(),
- property: () => [this.getSelectedVariation(), 'uuid'],
- onChange: async() => this.refreshUi(),
- },
- {
- type: 'input',
- label: 'title',
- hidden: () => !this._selectedMaterial(),
- property: () => [this.getSelectedVariation(), 'title'],
- onChange: async() => this.refreshUi(),
- },
- {
- type: 'dropdown',
- label: 'Preview Type',
- hidden: () => !this._selectedMaterial(),
- property: () => [this.getSelectedVariation(), 'preview'],
- onChange: async() => this.refreshUi(),
- children: ['generate:sphere', 'generate:cube', 'color', 'map', 'emissive', ...Object.keys(PhysicalMaterial.MaterialProperties).filter(x => x.endsWith('Map'))].map(k => ({
- label: k,
- value: k,
- })),
- },
- ...this.getSelectedVariation()?.materials.map(m => {
- return m.uiConfig ? Object.assign(m.uiConfig, {expanded: false}) : {}
- }) || [],
- {
- type: 'button',
- label: 'Clear variations',
- hidden: () => !this._selectedMaterial(),
- value: async() => {
- const v = this.getSelectedVariation()
- if (v && await this._viewer!.dialog.confirm('Material configurator: Remove all variations for this material?')) v.materials = []
- this.refreshUi()
- },
- },
- {
- type: 'button',
- label: 'Remove completely',
- hidden: () => !this._selectedMaterial(),
- value: async() => {
- const v = this.getSelectedVariation()
- if (v && await this._viewer!.dialog.confirm('Material configurator: Remove this variation?')) {
- this.removeVariation(v)
- }
- },
- },
- {
- type: 'button',
- label: 'Add Variation',
- hidden: () => !this._selectedMaterial(),
- value: async() => {
- const mat = this._selectedMaterial()
- if (!mat) return
- if (!mat.name && !await this._viewer?.dialog.confirm('Material configurator: Material has no name. Use uuid instead?')) return
- this.addVariation(mat)
- },
- },
- {
- type: 'button',
- label: 'Refresh Ui',
- value: () => this.refreshUi(),
- },
- {
- type: 'button',
- label: 'Apply All',
- value: () => {
- this.variations.forEach(v => this.applyVariation(v, v.materials[0].uuid))
- },
- },
- ],
- ],
- }
-
- removeVariationForMaterial(material: IMaterial) {
- let variation = this.findVariation(material.uuid)
- if (!variation && material.name.length > 0) variation = this.findVariation(material.name)
- if (variation) this.removeVariation(variation)
- }
- removeVariation(variation: MaterialVariations) {
- if (!variation) return
- this.variations.splice(this.variations.indexOf(variation), 1)
- this.refreshUi()
- }
- addVariation(material?: IMaterial) {
- const clone = material?.clone?.()
- if (material && clone) {
- let variation = this.findVariation(material.uuid)
- if (!variation && material.name.length > 0) variation = this.findVariation(material.name)
- if (!variation) {
- variation = this.createVariation(material)
- }
- variation.materials.push(clone)
- this.refreshUi()
- }
- }
-
- createVariation(material: IMaterial) {
- this.variations.push({
- uuid: material.name.length > 0 ? material.name : material.uuid,
- title: material.name.length > 0 ? material.name : 'No Name',
- preview: 'generate:sphere',
- materials: [],
- })
- return this.variations[this.variations.length - 1]
- }
- }
-
- export interface MaterialVariations {
- /**
- * The name or the uuid of the material in the scene
- */
- uuid: string
- /**
- * Title to show in the UI
- */
- title: string
- preview: keyof PhysicalMaterial | 'generate:sphere' | 'generate:cube' | 'generate:cylinder'
- materials: IMaterial[]
- data?: {
- icon?: string,
- [key: string]: any
- }[]
- selectedIndex?: number
- }
|