threepipe
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

MaterialConfiguratorBasePlugin.ts 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import {AViewerPluginSync, ThreeViewer} from '../../viewer'
  2. import {PickingPlugin} from '../interaction/PickingPlugin'
  3. import {imageBitmapToBase64, makeColorSvgCircle, serialize} from 'ts-browser-helpers'
  4. import {UiObjectConfig} from 'uiconfig.js'
  5. import {IMaterial, PhysicalMaterial} from '../../core'
  6. import {MaterialPreviewGenerator} from '../../three'
  7. import {Color} from 'three'
  8. /**
  9. * Material Configurator Plugin (Base)
  10. * This plugin allows you to create variations of materials mapped to material names or uuids in the scene.
  11. * 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)
  12. * The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations.
  13. *
  14. * See `MaterialConfiguratorPlugin` in [plugin-configurator](https://threepipe.org/plugins/configurator/docs/index.html) for example on inheriting with a custom UI renderer.
  15. *
  16. * @category Plugins
  17. */
  18. export class MaterialConfiguratorBasePlugin extends AViewerPluginSync<''> {
  19. enabled = true
  20. public static PluginType = 'MaterialConfiguratorPlugin'
  21. private _picking: PickingPlugin | undefined
  22. protected _previewGenerator: MaterialPreviewGenerator | undefined
  23. private _uiNeedRefresh = false
  24. constructor() {
  25. super()
  26. this.addEventListener('deserialize', this.refreshUi)
  27. this.refreshUi = this.refreshUi.bind(this)
  28. this._refreshUi = this._refreshUi.bind(this)
  29. this._refreshUiConfig = this._refreshUiConfig.bind(this)
  30. }
  31. onAdded(viewer: ThreeViewer) {
  32. super.onAdded(viewer)
  33. // todo subscribe to plugin add event if picking is not added yet.
  34. this._picking = viewer.getPlugin<PickingPlugin>('Picking')
  35. this._previewGenerator = new MaterialPreviewGenerator()
  36. this._picking?.addEventListener('selectedObjectChanged', this._refreshUiConfig)
  37. viewer.addEventListener('preFrame', this._refreshUi)
  38. }
  39. /**
  40. * Apply all variations(by selected index or first item) when a config is loaded
  41. */
  42. applyOnLoad = true
  43. /**
  44. * Reapply all selected variations again.
  45. * Useful when the scene is loaded or changed and the variations are not applied.
  46. */
  47. reapplyAll() {
  48. this.variations.forEach(v => this.applyVariation(v, v.materials[v.selectedIndex ?? 0].uuid))
  49. }
  50. fromJSON(data: any, meta?: any): this | Promise<this | null> | null {
  51. this.variations = []
  52. if (!super.fromJSON(data, meta)) return null // its not a promise
  53. if (data.applyOnLoad === undefined) { // old files
  54. this.applyOnLoad = false
  55. }
  56. if (this.applyOnLoad) this.reapplyAll()
  57. return this
  58. }
  59. onRemove(viewer: ThreeViewer) {
  60. this._previewGenerator?.dispose()
  61. this._previewGenerator = undefined
  62. this._picking?.removeEventListener('selectedObjectChanged', this._refreshUiConfig)
  63. this.removeEventListener('deserialize', this.refreshUi)
  64. viewer.removeEventListener('preFrame', this._refreshUi)
  65. this._picking = undefined
  66. return super.onRemove(viewer)
  67. }
  68. findVariation(uuid?: string): MaterialVariations|undefined {
  69. return uuid ? this.variations.find(v => v.uuid === uuid) : undefined
  70. }
  71. getSelectedVariation(): MaterialVariations|undefined {
  72. return this.findVariation(this._selectedMaterial()?.uuid) || this.findVariation(this._selectedMaterial()?.name)
  73. }
  74. /**
  75. * Apply a material variation based on index or uuid.
  76. * @param variations
  77. * @param matUuidOrIndex
  78. */
  79. applyVariation(variations: MaterialVariations, matUuidOrIndex: string|number): boolean {
  80. const m = this._viewer?.materialManager
  81. if (!m) return false
  82. const material = typeof matUuidOrIndex === 'string' ?
  83. variations.materials.find(m1 => m1.uuid === matUuidOrIndex) :
  84. variations.materials[matUuidOrIndex]
  85. if (!material) return false
  86. variations.selectedIndex = variations.materials.indexOf(material)
  87. return m.applyMaterial(material, variations.uuid)
  88. }
  89. /**
  90. * Get the preview for a material variation
  91. * Should be called from preFrame ideally. (or preRender but set viewerSetDirty = false)
  92. * @param preview - Type of preview. Could be generate:sphere, generate:cube, color, map, emissive, etc.
  93. * @param material - Material or index of the material in the variation.
  94. * @param viewerSetDirty - call viewer.setDirty() after setting the preview. So that the preview is cleared from the canvas.
  95. */
  96. getPreview(material: IMaterial, preview: string, viewerSetDirty = true): string {
  97. if (!this._viewer) return ''
  98. // const m = typeof material === 'number' ? variation.materials[material] : material
  99. const m = material
  100. if (!m) return ''
  101. let image = ''
  102. if (!preview.startsWith('generate:')) {
  103. const pp = (m as any)[preview] || '#ff00ff'
  104. image = pp.image ? imageBitmapToBase64(pp.image, 100) : ''
  105. if (!image.length) image = makeColorSvgCircle(pp.isColor ? (pp as Color).getHexString() : pp)
  106. } else {
  107. image = this._previewGenerator!.generate(m,
  108. this._viewer.renderManager.renderer,
  109. this._viewer.scene.environment,
  110. preview.split(':')[1]
  111. )
  112. }
  113. if (viewerSetDirty) this._viewer.setDirty() // because called from preFrame
  114. return image
  115. }
  116. /**
  117. * Refreshes the UI in the next frame
  118. */
  119. refreshUi(): void {
  120. if (!this.enabled || !this._viewer) return
  121. this._uiNeedRefresh = true
  122. }
  123. private _refreshUiConfig() {
  124. if (!this.enabled) return
  125. this.uiConfig.uiRefresh?.() // don't call this.refreshUi here
  126. }
  127. // must be called from preFrame
  128. protected async _refreshUi(): Promise<boolean> {
  129. if (!this.enabled) return false
  130. if (!this._viewer || !this._uiNeedRefresh) return false
  131. this._uiNeedRefresh = false
  132. this._refreshUiConfig()
  133. return true
  134. }
  135. @serialize()
  136. variations: MaterialVariations[] = []
  137. private _selectedMaterial = () => (this._picking?.getSelectedObject()?.material || undefined) as IMaterial | undefined
  138. uiConfig: UiObjectConfig = {
  139. label: 'Material Configurator',
  140. type: 'folder',
  141. // expanded: true,
  142. children: [
  143. () => [
  144. {
  145. type: 'input',
  146. label: 'uuid',
  147. property: [this._selectedMaterial(), 'uuid'],
  148. hidden: () => !this._selectedMaterial(),
  149. disabled: true,
  150. },
  151. {
  152. type: 'input',
  153. label: 'mapping',
  154. hidden: () => !this._selectedMaterial(),
  155. property: () => [this.getSelectedVariation(), 'uuid'],
  156. onChange: async() => this.refreshUi(),
  157. },
  158. {
  159. type: 'input',
  160. label: 'title',
  161. hidden: () => !this._selectedMaterial(),
  162. property: () => [this.getSelectedVariation(), 'title'],
  163. onChange: async() => this.refreshUi(),
  164. },
  165. {
  166. type: 'dropdown',
  167. label: 'Preview Type',
  168. hidden: () => !this._selectedMaterial(),
  169. property: () => [this.getSelectedVariation(), 'preview'],
  170. onChange: async() => this.refreshUi(),
  171. children: ['generate:sphere', 'generate:cube', 'color', 'map', 'emissive', ...Object.keys(PhysicalMaterial.MaterialProperties).filter(x => x.endsWith('Map'))].map(k => ({
  172. label: k,
  173. value: k,
  174. })),
  175. },
  176. ...this.getSelectedVariation()?.materials.map(m => {
  177. return m.uiConfig ? Object.assign(m.uiConfig, {expanded: false}) : {}
  178. }) || [],
  179. {
  180. type: 'button',
  181. label: 'Clear variations',
  182. hidden: () => !this._selectedMaterial(),
  183. value: async() => {
  184. const v = this.getSelectedVariation()
  185. if (v && await this._viewer!.dialog.confirm('Material configurator: Remove all variations for this material?')) v.materials = []
  186. this.refreshUi()
  187. },
  188. },
  189. {
  190. type: 'button',
  191. label: 'Remove completely',
  192. hidden: () => !this._selectedMaterial(),
  193. value: async() => {
  194. const v = this.getSelectedVariation()
  195. if (v && await this._viewer!.dialog.confirm('Material configurator: Remove this variation?')) {
  196. this.removeVariation(v)
  197. }
  198. },
  199. },
  200. {
  201. type: 'button',
  202. label: 'Add Variation',
  203. hidden: () => !this._selectedMaterial(),
  204. value: async() => {
  205. const mat = this._selectedMaterial()
  206. if (!mat) return
  207. if (!mat.name && !await this._viewer?.dialog.confirm('Material configurator: Material has no name. Use uuid instead?')) return
  208. this.addVariation(mat)
  209. },
  210. },
  211. {
  212. type: 'button',
  213. label: 'Refresh Ui',
  214. value: () => this.refreshUi(),
  215. },
  216. {
  217. type: 'button',
  218. label: 'Apply All',
  219. value: () => {
  220. this.variations.forEach(v => this.applyVariation(v, v.materials[0].uuid))
  221. },
  222. },
  223. ],
  224. ],
  225. }
  226. removeVariationForMaterial(material: IMaterial) {
  227. let variation = this.findVariation(material.uuid)
  228. if (!variation && material.name.length > 0) variation = this.findVariation(material.name)
  229. if (variation) this.removeVariation(variation)
  230. }
  231. removeVariation(variation: MaterialVariations) {
  232. if (!variation) return
  233. this.variations.splice(this.variations.indexOf(variation), 1)
  234. this.refreshUi()
  235. }
  236. addVariation(material?: IMaterial) {
  237. const clone = material?.clone?.()
  238. if (material && clone) {
  239. let variation = this.findVariation(material.uuid)
  240. if (!variation && material.name.length > 0) variation = this.findVariation(material.name)
  241. if (!variation) {
  242. variation = this.createVariation(material)
  243. }
  244. variation.materials.push(clone)
  245. this.refreshUi()
  246. }
  247. }
  248. createVariation(material: IMaterial) {
  249. this.variations.push({
  250. uuid: material.name.length > 0 ? material.name : material.uuid,
  251. title: material.name.length > 0 ? material.name : 'No Name',
  252. preview: 'generate:sphere',
  253. materials: [],
  254. })
  255. return this.variations[this.variations.length - 1]
  256. }
  257. }
  258. export interface MaterialVariations {
  259. /**
  260. * The name or the uuid of the material in the scene
  261. */
  262. uuid: string
  263. /**
  264. * Title to show in the UI
  265. */
  266. title: string
  267. preview: keyof PhysicalMaterial | 'generate:sphere' | 'generate:cube' | 'generate:cylinder'
  268. materials: IMaterial[]
  269. data?: {
  270. icon?: string,
  271. [key: string]: any
  272. }[]
  273. selectedIndex?: number
  274. }