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

ThreeViewer.ts 43KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  1. import {BaseEvent, Color, Event, EventDispatcher, LinearSRGBColorSpace, Object3D, Vector2} from 'three'
  2. import {Class, createCanvasElement, onChange, serialize} from 'ts-browser-helpers'
  3. import {TViewerScreenShader} from '../postprocessing'
  4. import {
  5. AddObjectOptions,
  6. IAnimationLoopEvent,
  7. IMaterial,
  8. IObject3D,
  9. IObjectProcessor,
  10. ITexture,
  11. PerspectiveCamera2,
  12. RootScene,
  13. } from '../core'
  14. import {ViewerRenderManager} from './ViewerRenderManager'
  15. import {
  16. convertArrayBufferToStringsInMeta,
  17. getEmptyMeta,
  18. GLStatsJS,
  19. IDialogWrapper,
  20. jsonToBlob,
  21. metaFromResources,
  22. MetaImporter,
  23. metaToResources,
  24. SerializationMetaType,
  25. SerializationResourcesType,
  26. ThreeSerialization,
  27. windowDialogWrapper,
  28. } from '../utils'
  29. import {
  30. AssetManager,
  31. AssetManagerOptions,
  32. BlobExt,
  33. ExportFileOptions,
  34. IAsset,
  35. ImportAddOptions,
  36. ImportAssetOptions,
  37. ImportResult,
  38. RootSceneImportResult,
  39. } from '../assetmanager'
  40. import {IViewerPlugin, IViewerPluginSync} from './IViewerPlugin'
  41. // noinspection ES6PreferShortImport
  42. import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin'
  43. import {uiConfig, uiFolderContainer, UiObjectConfig} from 'uiconfig.js'
  44. import {IRenderTarget} from '../rendering'
  45. import {TonemapPlugin} from '../plugins'
  46. export type IViewerEvent = BaseEvent & {
  47. type: 'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'renderEnabled'|'renderDisabled'
  48. }
  49. export type IViewerEventTypes = IViewerEvent['type']
  50. export interface ISerializedConfig {
  51. assetType: 'config',
  52. type: string,
  53. metadata?: {
  54. generator: string,
  55. version: number,
  56. [key: string]: any
  57. },
  58. [key: string]: any
  59. }
  60. export interface ISerializedViewerConfig extends ISerializedConfig{
  61. type: 'ThreeViewer'|'ViewerApp',
  62. version: string,
  63. plugins: ISerializedConfig[],
  64. resources?: Partial<SerializationResourcesType> | SerializationMetaType
  65. renderManager?: any // todo
  66. scene?: any
  67. [key: string]: any
  68. }
  69. export type IConsoleWrapper = Partial<Console> & Pick<Console, 'log'|'warn'|'error'>
  70. /**
  71. * Options for the ThreeViewer creation.
  72. * @category Viewer
  73. */
  74. export interface ThreeViewerOptions {
  75. /**
  76. * The canvas element to use for rendering. Only one of container and canvas must be specified.
  77. */
  78. canvas?: HTMLCanvasElement,
  79. /**
  80. * The container for the canvas. A new canvas will be created in this container. Only one of container and canvas must be specified.
  81. */
  82. container?: HTMLElement,
  83. /**
  84. * The fragment shader snippet to render on screen.
  85. */
  86. screenShader?: TViewerScreenShader,
  87. /**
  88. * Use MSAA.
  89. */
  90. msaa?: boolean,
  91. /**
  92. * Use RGBM HDR Pipeline
  93. */
  94. rgbm?: boolean
  95. /**
  96. * Use rendered gbuffer as depth-prepass / z-prepass.
  97. */
  98. zPrepass?: boolean
  99. debug?: boolean
  100. /**
  101. * TonemapPlugin is added to the viewer if this is true.
  102. * @default true
  103. */
  104. tonemap?: boolean
  105. /**
  106. * Options for the asset manager.
  107. */
  108. assetManager?: AssetManagerOptions
  109. /**
  110. * Add the dropzone plugin to the viewer, allowing to drag and drop files into the viewer over the canvas/container.
  111. * Set to true/false to enable/disable the plugin, or pass options to configure the plugin. Assuming true if options are passed.
  112. * @default - false
  113. */
  114. dropzone?: boolean|DropzonePluginOptions
  115. /**
  116. * @deprecated use {@link msaa} instead
  117. */
  118. isAntialiased?: boolean,
  119. /**
  120. * @deprecated use {@link rgbm} instead
  121. */
  122. useRgbm?: boolean
  123. /**
  124. * @deprecated use {@link zPrepass} instead
  125. */
  126. useGBufferDepth?: boolean
  127. }
  128. const VIEWER_VERSION = '0.0.10-dev.1'
  129. /**
  130. * Three Viewer
  131. *
  132. * The ThreeViewer is the main class in the framework to manage a scene, render and add plugins to it.
  133. * @category Viewer
  134. */
  135. @uiFolderContainer('Viewer')
  136. export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes> {
  137. public static readonly VERSION = VIEWER_VERSION
  138. public static readonly ConfigTypeSlug = 'vjson'
  139. uiConfig!: UiObjectConfig
  140. static Console: IConsoleWrapper = {
  141. log: console.log.bind(console),
  142. warn: console.warn.bind(console),
  143. error: console.error.bind(console),
  144. }
  145. static Dialog: IDialogWrapper = windowDialogWrapper
  146. renderStats: GLStatsJS
  147. readonly assetManager: AssetManager
  148. get console(): IConsoleWrapper {
  149. return ThreeViewer.Console
  150. }
  151. get dialog(): IDialogWrapper {
  152. return ThreeViewer.Dialog
  153. }
  154. @serialize() readonly type = 'ThreeViewer'
  155. private readonly _canvas: HTMLCanvasElement
  156. // this can be used by other plugins to add ui elements alongside the canvas
  157. private readonly _container: HTMLElement // todo: add a way to move the canvas to a new container... and dispatch event...
  158. @uiConfig() @serialize('renderManager')
  159. readonly renderManager: ViewerRenderManager
  160. /**
  161. * The Scene attached to the viewer, this cannot be changed.
  162. * @type {RootScene}
  163. */
  164. @uiConfig() @serialize('scene')
  165. private readonly _scene: RootScene
  166. public readonly plugins: Record<string, IViewerPlugin> = {}
  167. private _needsResize = false
  168. /**
  169. * If the viewer is enabled. Set this `false` to disable RAF loop.
  170. * @type {boolean}
  171. */
  172. enabled = true
  173. /**
  174. * Enable or disable all rendering, Animation loop including any frame/render events won't be fired when this is false.
  175. */
  176. @onChange(ThreeViewer.prototype._renderEnabledChanged)
  177. renderEnabled = true
  178. private _isRenderingFrame = false
  179. /**
  180. * Specifies how many frames to render in a single request animation frame. Keep to 1 for realtime rendering.
  181. * Note: should be max (screen refresh rate / animation frame rate) like 60Hz / 30fps
  182. * @type {number}
  183. */
  184. public maxFramePerLoop = 1
  185. get scene(): RootScene {
  186. return this._scene
  187. }
  188. /**
  189. * The ResizeObserver observing the canvas element. Add more elements to this observer to resize viewer on their size change.
  190. * @type {ResizeObserver | undefined}
  191. */
  192. readonly resizeObserver = window?.ResizeObserver ? new window.ResizeObserver(_ => this.resize()) : undefined
  193. readonly debug: boolean
  194. /**
  195. * Create a viewer instance for using the webgi viewer SDK.
  196. * @param options - {@link ThreeViewerOptions}
  197. */
  198. constructor({debug = true, ...options}: ThreeViewerOptions) {
  199. super()
  200. this.debug = debug
  201. this._canvas = options.canvas || createCanvasElement()
  202. let container = options.container
  203. if (container && !options.canvas) container.appendChild(this._canvas)
  204. if (!container) container = this._canvas.parentElement ?? undefined
  205. if (!container) throw new Error('No container.')
  206. this._container = container
  207. this.setDirty = this.setDirty.bind(this)
  208. this._animationLoop = this._animationLoop.bind(this)
  209. this._setActiveCameraView = this._setActiveCameraView.bind(this)
  210. this.renderStats = new GLStatsJS(this._container)
  211. if (debug) this.renderStats.show()
  212. if (!(window as any).threeViewers) (window as any).threeViewers = [];
  213. (window as any).threeViewers.push(this)
  214. // camera
  215. const camera = new PerspectiveCamera2('orbit', this._canvas)
  216. camera.name = 'Default Camera'
  217. camera.position.set(0, 0, 5)
  218. camera.userData.autoLookAtTarget = true
  219. this.addEventListener('postFrame', () => { // todo: move inside RootScene.
  220. const cam = this._scene.mainCamera
  221. if (cam && cam.canUserInteract) {
  222. // todo
  223. // const d = this.getPluginByType<ProgressivePlugin>('Progressive')?.postFrameConvergedRecordingDelta()
  224. // // if (d && d > 0) delta = d
  225. // if (d !== undefined && d === 0) return // not converged yet.
  226. // // if d < 0 or undefined: not recording, do nothing
  227. cam.controls?.update()
  228. }
  229. })
  230. // scene
  231. this._scene = new RootScene(camera, this._objectProcessor)
  232. this._scene.setBackgroundColor('#ffffff')
  233. // this._scene.addEventListener('addSceneObject', this._addSceneObject)
  234. this._scene.addEventListener('setView', this._setActiveCameraView)
  235. this._scene.addEventListener('activateMain', this._setActiveCameraView)
  236. this._scene.addEventListener('materialUpdate', (e) => this.setDirty(this._scene, e))
  237. this._scene.addEventListener('materialChanged', (e) => this.setDirty(this._scene, e))
  238. this._scene.addEventListener('objectUpdate', (e) => this.setDirty(this._scene, e))
  239. this._scene.addEventListener('textureUpdate', (e) => this.setDirty(this._scene, e))
  240. this._scene.addEventListener('sceneUpdate', (e) => {
  241. this.setDirty(this._scene, e)
  242. if (e.geometryChanged === false) return
  243. this.renderManager.resetShadows()
  244. })
  245. // render manager
  246. if (options.isAntialiased !== undefined || options.useRgbm !== undefined || options.useGBufferDepth !== undefined) {
  247. this.console.warn('isAntialiased, useRgbm and useGBufferDepth are deprecated, use msaa, rgbm and zPrepass instead.')
  248. }
  249. this.renderManager = new ViewerRenderManager({
  250. canvas: this._canvas,
  251. msaa: options.msaa ?? options.isAntialiased ?? false,
  252. rgbm: options.rgbm ?? options.useRgbm ?? false,
  253. zPrepass: options.zPrepass ?? options.useGBufferDepth ?? false,
  254. depthBuffer: !(options.zPrepass ?? options.useGBufferDepth ?? false),
  255. screenShader: options.screenShader,
  256. })
  257. this.renderManager.addEventListener('animationLoop', this._animationLoop as any)
  258. this.renderManager.addEventListener('resize', ()=> this._scene.mainCamera.refreshAspect())
  259. this.renderManager.addEventListener('update', (e) => {
  260. if (e.change === 'registerPass' && e.pass?.materialExtension)
  261. this.assetManager.materials.registerMaterialExtension(e.pass.materialExtension)
  262. else if (e.change === 'unregisterPass' && e.pass?.materialExtension)
  263. this.assetManager.materials.unregisterMaterialExtension(e.pass.materialExtension)
  264. this.setDirty(this.renderManager, e)
  265. })
  266. this.assetManager = new AssetManager(this, options.assetManager)
  267. if (this.resizeObserver) this.resizeObserver.observe(this._canvas)
  268. // sometimes resize observer is late, so extra check
  269. window && window.addEventListener('resize', this.resize)
  270. this._canvas.addEventListener('webglcontextrestored', this._onContextRestore, false)
  271. this._canvas.addEventListener('webglcontextlost', this._onContextLost, false)
  272. if (options.dropzone) {
  273. this.addPluginSync(new DropzonePlugin(typeof options.dropzone === 'object' ? options.dropzone : undefined))
  274. }
  275. if (options.tonemap !== false) {
  276. this.addPluginSync(new TonemapPlugin())
  277. }
  278. this.console.log('ThreePipe Viewer instance initialized, version: ', ThreeViewer.VERSION)
  279. }
  280. private _objectProcessor: IObjectProcessor = {
  281. processObject: (object: IObject3D)=>{
  282. if (object.material) {
  283. if (Array.isArray(object.material)) this.assetManager.materials.registerMaterials(object.material)
  284. else this.assetManager.materials.registerMaterial(object.material)
  285. }
  286. },
  287. }
  288. // todo: find a better fix for context loss and restore?
  289. private _lastSize = new Vector2()
  290. private _onContextRestore = (_: Event) => {
  291. this.enabled = true
  292. this._canvas.width = this._lastSize.width
  293. this._canvas.height = this._lastSize.height
  294. this.resize()
  295. this._scene.setDirty({refreshScene: true, frameFade: false})
  296. }
  297. private _onContextLost = (_: Event) => {
  298. this._lastSize.set(this._canvas.width, this._canvas.height)
  299. this._canvas.width = 2
  300. this._canvas.height = 2
  301. this.resize()
  302. this.enabled = false
  303. }
  304. /**
  305. * Mark that the canvas is resized. If the size is changed, the renderer and all render targets are resized. This happens before the render of the next frame.
  306. */
  307. resize = () => {
  308. this._needsResize = true
  309. this.setDirty()
  310. }
  311. private _needsReset = true // renderer reset
  312. /**
  313. * Set the viewer to dirty and trigger render of the next frame.
  314. * @param source - The source of the dirty event. like plugin or 3d object
  315. * @param event - The event that triggered the dirty event.
  316. */
  317. setDirty(source?: any, event?: Event): void {
  318. this._needsReset = true
  319. source = source ?? this
  320. this.dispatchEvent({...event ?? {}, type: 'update', source})
  321. }
  322. /**
  323. * The renderer for the viewer that's attached to the canvas. This is wrapper around WebGLRenderer and EffectComposer and manages post-processing passes and rendering logic
  324. * @deprecated - use {@link renderManager} instead
  325. */
  326. get renderer(): ViewerRenderManager {
  327. this.console.error('renderer is deprecated, use renderManager instead')
  328. return this.renderManager
  329. }
  330. /**
  331. * Add an object/model/material/viewer-config/plugin-preset/... to the viewer scene from url or an {@link IAsset} object.
  332. * Same as {@link AssetManager.addAssetSingle}
  333. * @param obj
  334. * @param options
  335. */
  336. async load<T extends ImportResult = ImportResult>(obj: string | IAsset | null, options?: ImportAddOptions) {
  337. if (!obj) return
  338. return await this.assetManager.addAssetSingle<T>(obj, options)
  339. }
  340. /**
  341. * Imports an object/model/material/texture/viewer-config/plugin-preset/... to the viewer scene from url or an {@link IAsset} object.
  342. * Same as {@link AssetImporter.importSingle}
  343. * @param obj
  344. * @param options
  345. */
  346. async import<T extends ImportResult = ImportResult>(obj: string | IAsset | null, options?: ImportAddOptions) {
  347. if (!obj) return
  348. return await this.assetManager.importer.importSingle<T>(obj, options)
  349. }
  350. /**
  351. * Exports an object/mesh/material/texture/render-target/plugin-preset/viewer to a blob.
  352. * If no object is given, a glb is exported with the current viewer state.
  353. * @param obj
  354. * @param options
  355. */
  356. async export(obj?: IObject3D|IMaterial|ITexture|IRenderTarget|IViewerPlugin|(typeof this), options?: ExportFileOptions) {
  357. if (!obj) obj = this._scene // this will export the glb with the scene and viewer config
  358. if ((<typeof this>obj).type === this.type) return jsonToBlob((<typeof this>obj).exportConfig())
  359. if ((<IViewerPlugin>obj).constructor?.PluginType) return jsonToBlob(this.exportPluginConfig(<IViewerPlugin>obj))
  360. return await this.assetManager.exporter.exportObject(<IObject3D|IMaterial|ITexture|IRenderTarget>obj, options)
  361. }
  362. /**
  363. * Set the environment map of the scene from url or an {@link IAsset} object.
  364. * @param map
  365. * @param setBackground - Set the background image of the scene from the same map.
  366. * @param options - Options for importing the asset. See {@link ImportAssetOptions}
  367. */
  368. async setEnvironmentMap(map: string | IAsset | null | ITexture, {setBackground = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> {
  369. this._scene.environment = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null
  370. if (setBackground) return this.setBackgroundMap(this._scene.environment)
  371. return this._scene.environment
  372. }
  373. /**
  374. * Set the background image of the scene from url or an {@link IAsset} object.
  375. * @param map
  376. * @param setEnvironment - Set the environment map of the scene from the same map.
  377. * @param options - Options for importing the asset. See {@link ImportAssetOptions}
  378. */
  379. async setBackgroundMap(map: string | IAsset | null | ITexture, {setEnvironment = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> {
  380. this._scene.background = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null
  381. if (setEnvironment) return this.setEnvironmentMap(this._scene.background)
  382. return this._scene.background
  383. }
  384. /**
  385. * Disposes the viewer and frees up all resource and events. Do not use the viewer after calling dispose.
  386. * @note - If you want to reuse the viewer, set viewer.enabled to false instead, then set it to true again when required. To dispose all the objects, materials in the scene use `viewer.scene.disposeSceneModels()`
  387. * This function is not fully implemented yet. There might be some memory leaks.
  388. * @todo - return promise?
  389. */
  390. public dispose(): void {
  391. // todo: dispose stuff from constructor etc
  392. for (const plugin of [...Object.values(this.plugins)]) {
  393. this.removePlugin(plugin, true)
  394. }
  395. this._scene.dispose()
  396. this.renderManager.dispose()
  397. this._canvas.removeEventListener('webglcontextrestored', this._onContextRestore, false)
  398. this._canvas.removeEventListener('webglcontextlost', this._onContextLost, false)
  399. ;(window as any).threeViewers?.splice((window as any).threeViewers.indexOf(this), 1)
  400. if (this.resizeObserver) this.resizeObserver.unobserve(this._canvas)
  401. else window.removeEventListener('resize', this.resize)
  402. this.dispatchEvent({type: 'dispose'})
  403. }
  404. private _animationLoop(event: IAnimationLoopEvent): void {
  405. if (!this.enabled || !this.renderEnabled) return
  406. if (this._isRenderingFrame) {
  407. this.console.warn('animation loop: frame skip') // not possible actually, since this is not async
  408. return
  409. }
  410. this._isRenderingFrame = true
  411. this.renderStats.begin()
  412. for (let i = 0; i < this.maxFramePerLoop; i++) {
  413. if (this._needsReset) {
  414. this.renderManager.reset()
  415. this._needsReset = false
  416. }
  417. if (this._needsResize) {
  418. const size = [this._canvas.clientWidth, this._canvas.clientHeight]
  419. if (event.xrFrame) { // todo: find a better way to resize for XR.
  420. const cam = this.renderManager.webglRenderer.xr.getCamera()?.cameras[0]?.viewport
  421. if (cam) {
  422. if (cam.x !== 0 || cam.y !== 0) {
  423. this.console.warn('x and y must be 0?')
  424. }
  425. size[0] = cam.width
  426. size[1] = cam.height
  427. this.console.log('resize for xr', size)
  428. } else {
  429. this._needsResize = false
  430. }
  431. }
  432. if (this._needsResize) {
  433. this.renderManager.setSize(...size)
  434. this._needsResize = false
  435. }
  436. }
  437. this.dispatchEvent({...event, type: 'preFrame', target: this}) // event will have time, deltaTime and xrFrame
  438. const dirtyPlugins = Object.values(this.plugins).filter(value => value.dirty)
  439. if (dirtyPlugins.length > 0) {
  440. // console.log('dirty plugins', dirtyPlugins)
  441. this.setDirty(dirtyPlugins)
  442. }
  443. if (this._needsReset) {
  444. this.renderManager.reset()
  445. this._needsReset = false
  446. }
  447. // Check if the renderManger is dirty, which happens when it's reset above or if any pass in the composer is dirty
  448. const needsRender = this.renderManager.needsRender
  449. if (needsRender) {
  450. this.dispatchEvent({type: 'preRender', target: this})
  451. if (this.debug) this.renderManager.render(this._scene)
  452. else {
  453. try {
  454. this.renderManager.render(this._scene)
  455. } catch (e) {
  456. this.console.error(e)
  457. // this.enabled = false
  458. }
  459. }
  460. this.dispatchEvent({type: 'postRender', target: this})
  461. }
  462. this.dispatchEvent({type: 'postFrame', target: this})
  463. this.renderManager.onPostFrame()
  464. if (!needsRender) // break if no frame rendered
  465. break
  466. }
  467. this.renderStats.end()
  468. this._isRenderingFrame = false
  469. }
  470. /**
  471. * Get the HTML Element containing the canvas
  472. * @returns {HTMLElement}
  473. */
  474. get container(): HTMLElement {
  475. return this._container
  476. }
  477. /**
  478. * Get the HTML Canvas Element where the viewer is rendering
  479. * @returns {HTMLCanvasElement}
  480. */
  481. get canvas(): HTMLCanvasElement {
  482. return this._canvas
  483. }
  484. /**
  485. * Get the Plugin by a constructor type or by the string type.
  486. * Use string type if the plugin is not a dependency and you don't want to bundle the plugin.
  487. * @param type - The class of the plugin to get, or the string type of the plugin to get which is in the static PluginType property of the plugin
  488. * @returns {T | undefined} - The plugin of the specified type.
  489. */
  490. getPlugin<T extends IViewerPlugin>(type: Class<T>|string): T | undefined {
  491. return this.plugins[typeof type === 'string' ? type : (type as any).PluginType] as T | undefined
  492. }
  493. /**
  494. * Get the Plugin by the string type.
  495. * @deprecated - Use {@link getPlugin} instead.
  496. * @param type
  497. * @returns {T | undefined}
  498. */
  499. getPluginByType<T extends IViewerPlugin>(type: string): T | undefined {
  500. return this.plugins[type] as T | undefined
  501. }
  502. async getOrAddPlugin<T extends IViewerPlugin>(type: Class<T>, ...args: ConstructorParameters<Class<T>>): Promise<T> {
  503. const plugin = this.getPlugin(type)
  504. if (plugin) return plugin
  505. return this.addPlugin(type, ...args)
  506. }
  507. getOrAddPluginSync<T extends IViewerPluginSync>(type: Class<T>, ...args: ConstructorParameters<Class<T>>): T {
  508. const plugin = this.getPlugin(type)
  509. if (plugin) return plugin
  510. return this.addPluginSync(type, ...args)
  511. }
  512. /**
  513. * Add a plugin to the viewer.
  514. * @param plugin - The instance of the plugin to add or the class of the plugin to add.
  515. * @param args - Arguments for the constructor of the plugin, in case a class is passed.
  516. * @returns {Promise<T>} - The plugin added.
  517. */
  518. async addPlugin<T extends IViewerPlugin>(plugin: T | Class<T>, ...args: ConstructorParameters<Class<T>>): Promise<T> {
  519. const p = this._resolvePluginOrClass(plugin, ...args)
  520. const type = p.constructor.PluginType
  521. if (!p.constructor.PluginType) {
  522. this.console.error('PluginType is not defined for', p)
  523. return p
  524. }
  525. for (const d of p.dependencies || []) {
  526. await this.getOrAddPlugin(d)
  527. }
  528. if (this.plugins[type]) {
  529. this.console.error(`Plugin of type ${type} already exists, removing and disposing old plugin. This might break functionality, ensure only one plugin of a type is added`, this.plugins[type], p)
  530. await this.removePlugin(this.plugins[type])
  531. }
  532. this.plugins[type] = p
  533. await p.onAdded(this)
  534. this.dispatchEvent({type: 'addPlugin', target: this, plugin: p})
  535. this.setDirty(p)
  536. return p
  537. }
  538. /**
  539. * Add a plugin to the viewer(sync).
  540. * @param plugin
  541. * @param args
  542. */
  543. addPluginSync<T extends IViewerPluginSync>(plugin: T|Class<T>, ...args: ConstructorParameters<Class<T>>): T {
  544. const p = this._resolvePluginOrClass(plugin, ...args)
  545. const type = p.constructor.PluginType
  546. if (!p.constructor.PluginType) {
  547. this.console.error('PluginType is not defined for', p)
  548. return p
  549. }
  550. for (const d of p.dependencies || []) {
  551. this.getOrAddPluginSync(d)
  552. }
  553. if (this.plugins[type]) {
  554. this.console.error(`Plugin of type ${type} already exists, removing and disposing old plugin. This might break functionality, ensure only one plugin of a type is added`, this.plugins[type], p)
  555. this.removePluginSync(this.plugins[type])
  556. }
  557. this.plugins[type] = p
  558. p.onAdded(this)
  559. this.dispatchEvent({type: 'addPlugin', target: this, plugin: p})
  560. this.setDirty(p)
  561. return p
  562. }
  563. async addPlugins(plugins: (IViewerPlugin | Class<IViewerPlugin>)[]): Promise<void> {
  564. for (const p of plugins) await this.addPlugin(p)
  565. }
  566. async addPluginsSync(plugins: (IViewerPluginSync | Class<IViewerPluginSync>)[]): Promise<void> {
  567. for (const p of plugins) this.addPluginSync(p)
  568. }
  569. /**
  570. * Remove a plugin instance or a plugin class. Works similar to {@link ThreeViewer.addPlugin}
  571. * @param p
  572. * @param dispose
  573. * @returns {Promise<void>}
  574. */
  575. async removePlugin(p: IViewerPlugin<ThreeViewer, false>, dispose = true): Promise<void> {
  576. const type = p.constructor.PluginType
  577. if (!this.plugins[type]) return
  578. await p.onRemove(this)
  579. delete this.plugins[type]
  580. if (dispose) await p.dispose() // todo await?
  581. this.setDirty(p)
  582. }
  583. removePluginSync(p: IViewerPluginSync, dispose = true): void {
  584. const type = p.constructor.PluginType
  585. if (!this.plugins[type]) return
  586. p.onRemove(this)
  587. delete this.plugins[type]
  588. if (dispose) p.dispose()
  589. this.setDirty(p)
  590. }
  591. /**
  592. * Set size of the canvas and update the renderer.
  593. * If no width/height is passed, canvas is set to 100% of the container.
  594. * @param size
  595. */
  596. setSize(size?: {width?: number, height?: number}) {
  597. this._canvas.style.width = size?.width ? size.width + 'px' : '100%'
  598. this._canvas.style.height = size?.height ? size.height + 'px' : '100%'
  599. // this._canvas.style.maxWidth = '100%' // this is upto the app to do.
  600. // this._canvas.style.maxHeight = '100%'
  601. this.resize()
  602. }
  603. // private _addSceneObject = (e: IEvent<any>) => {
  604. // if (!e || !e.object) return
  605. // const config = e.object.__importedViewerConfig // this is set in gltf.ts when gltf file is imported. This is done here so that scene settings are applied whenever the imported object is added to scene.
  606. // if (!config) return
  607. // this.fromJSON(config, config.resources)
  608. // }
  609. /**
  610. * @deprecated use {@link assetManager} instead.
  611. * Gets the Asset manager, contains useful functions for managing, loading and inserting assets.
  612. */
  613. getManager(): AssetManager|undefined {
  614. return this.assetManager
  615. }
  616. // todo
  617. // public async fitToView(selected?: Object3D, distanceMultiplier = 1.5, duration?: number, ease?: Easing|EasingFunctionType) {
  618. // const camViews = this.getPluginByType<CameraViewPlugin>('CameraViews')
  619. // if (!camViews) {
  620. // this.console.error('CameraViews plugin is required for fitToView to work')
  621. // return
  622. // }
  623. // await camViews?.animateToFitObject(selected, distanceMultiplier, duration, ease, {min: (this.scene.activeCamera.getControls<OrbitControls3>()?.minDistance ?? 0.5) + 0.5, max: 1000.0})
  624. // }
  625. /**
  626. * Traverse all objects in scene model root.
  627. * @param callback
  628. */
  629. traverseSceneObjects<T extends IObject3D = IObject3D>(callback: (o: T)=>void): void {
  630. this._scene.modelRoot.traverse(callback)
  631. }
  632. // todo: create/load texture utils
  633. /**
  634. * Serialize all the plugins and their settings to save or create presets. Used in {@link toJSON}.
  635. * @param meta - The meta object.
  636. * @param filter - List of PluginType for the to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
  637. * @returns {any[]}
  638. */
  639. serializePlugins(meta: SerializationMetaType, filter?: string[]): any[] {
  640. if (filter && filter.length === 0) return []
  641. return Object.entries(this.plugins).map(p=> {
  642. if (filter && !filter.includes(p[1].constructor.PluginType)) return
  643. // if (!p[1].toJSON) this.console.log(`Plugin of type ${p[0]} is not serializable`)
  644. return p[1].serializeWithViewer !== false ? p[1].toJSON?.(meta) : undefined
  645. }).filter(p=> !!p)
  646. }
  647. /**
  648. * Deserialize all the plugins and their settings from a preset. Used in {@link fromJSON}.
  649. * @param plugins - The output of {@link serializePlugins}.
  650. * @param meta - The meta object.
  651. * @returns {this}
  652. */
  653. deserializePlugins(plugins: any[], meta?: SerializationMetaType): this {
  654. plugins.forEach(p=>{
  655. if (!p.type) {
  656. this.console.warn('Invalid plugin to import ', p)
  657. return
  658. }
  659. const plugin = this.getPlugin(p.type)
  660. if (!plugin) {
  661. // this.console.warn(`Plugin of type ${p.type} is not added, cannot deserialize`)
  662. return
  663. }
  664. plugin.fromJSON?.(p, meta)
  665. })
  666. return this
  667. }
  668. /**
  669. * Serialize a single plugin settings.
  670. */
  671. exportPluginConfig(plugin?: string|Class<IViewerPlugin>|IViewerPlugin): ISerializedConfig | Record<string, never> {
  672. if (plugin && typeof plugin === 'string' || (plugin as any).PluginType) plugin = this.getPlugin(plugin as any)
  673. if (!plugin) return {}
  674. const meta = getEmptyMeta()
  675. const data = (<IViewerPlugin>plugin).toJSON?.(meta)
  676. if (!data) return {}
  677. data.resources = metaToResources(meta)
  678. return data
  679. }
  680. /**
  681. * Deserialize and import a single plugin settings.
  682. * Can also use {@link ThreeViewer.importConfig} to import only plugin config.
  683. * @param json
  684. * @param plugin
  685. */
  686. async importPluginConfig(json: ISerializedConfig, plugin?: IViewerPlugin) {
  687. // this.console.log('importing plugin preset', json, plugin)
  688. const type = json.type
  689. plugin = plugin || this.getPlugin(type)
  690. if (!plugin) {
  691. this.console.warn(`No plugin found for type ${type} to import config`)
  692. return undefined
  693. }
  694. if (!plugin.fromJSON) {
  695. this.console.warn(`Plugin ${type} does not support importing presets`)
  696. return undefined
  697. }
  698. const resources = json.resources || {}
  699. if (json.resources) delete json.resources
  700. const meta = await this.loadConfigResources(resources)
  701. await plugin.fromJSON(json, meta)
  702. if (meta) json.resources = meta
  703. return plugin
  704. }
  705. /**
  706. * Serialize multiple plugin settings.
  707. * @param filter - List of PluginType to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
  708. */
  709. exportPluginsConfig(filter?: string[]): ISerializedViewerConfig {
  710. const meta = getEmptyMeta()
  711. const plugins = this.serializePlugins(meta, filter)
  712. convertArrayBufferToStringsInMeta(meta) // assuming not binary
  713. return {
  714. ...this._defaultConfig,
  715. plugins, resources: metaToResources(meta),
  716. }
  717. }
  718. /**
  719. * Serialize all the viewer and plugin settings.
  720. * @param binary - Indicate that the output will be converted and saved as binary data. (default: false)
  721. * @param pluginFilter - List of PluginType to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
  722. */
  723. exportConfig(binary = false, pluginFilter?: string[]) {
  724. return this.toJSON(binary, pluginFilter)
  725. }
  726. /**
  727. * Deserialize and import all the viewer and plugin settings, exported with {@link exportConfig}.
  728. */
  729. async importConfig(json: ISerializedConfig|ISerializedViewerConfig) {
  730. if (json.type !== this.type && <string>json.type !== 'ViewerApp') {
  731. if (this.getPlugin(json.type)) {
  732. return this.importPluginConfig(json)
  733. } else {
  734. this.console.error(`Unknown config type ${json.type} to import`)
  735. return undefined
  736. }
  737. }
  738. const resources = await this.loadConfigResources(json.resources || {})
  739. this.fromJSON(<ISerializedViewerConfig>json, resources)
  740. }
  741. /**
  742. * Serialize all the viewer and plugin settings and versions.
  743. * @param binary - Indicate that the output will be converted and saved as binary data. (default: true)
  744. * @param pluginFilter - List of PluginType to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
  745. * @returns {any} - Serializable JSON object.
  746. */
  747. toJSON(binary = true, pluginFilter?: string[]): ISerializedViewerConfig {
  748. const meta = getEmptyMeta()
  749. const data: ISerializedViewerConfig = Object.assign({
  750. ...this._defaultConfig,
  751. plugins: this.serializePlugins(meta, pluginFilter),
  752. }, ThreeSerialization.Serialize(this, meta, true))
  753. // this.console.log(dat)
  754. if (!binary) convertArrayBufferToStringsInMeta(meta)
  755. data.resources = metaToResources(meta)
  756. return data
  757. }
  758. /**
  759. * Deserialize all the viewer and plugin settings.
  760. * @note use async {@link ThreeViewer.importConfig} to import a json/config exported with {@link ThreeViewer.exportConfig} or {@link ThreeViewer.toJSON}.
  761. * @param data - The serialized JSON object retured from {@link toJSON}.
  762. * @param meta - The meta object
  763. * @returns {this}
  764. */
  765. fromJSON(data: ISerializedViewerConfig, meta?: SerializationMetaType): this|null {
  766. const data2: Partial<ISerializedViewerConfig> = {...data} // shallow copy
  767. // region legacy
  768. if (data2.backgroundIntensity !== undefined && data2.scene?.backgroundIntensity === undefined) {
  769. this.console.warn('old file format, backgroundIntensity moved to RootScene')
  770. this._scene.backgroundIntensity = data2.backgroundIntensity
  771. delete data2.backgroundIntensity
  772. }
  773. if (data2.useLegacyLights !== undefined && data2.renderManager?.useLegacyLights === undefined) {
  774. this.console.warn('old file format, useLegacyLights moved to RenderManager')
  775. this.renderManager.useLegacyLights = data2.useLegacyLights
  776. delete data2.useLegacyLights
  777. }
  778. if (data2.background !== undefined && data2.scene?.background === undefined) {
  779. this.console.warn('old file format, background moved to RootScene')
  780. if (data2.background === 'envMapBackground') data2.background = 'environment'
  781. else if (typeof data2.background === 'number')
  782. data2.background = new Color().setHex(data2.background, LinearSRGBColorSpace)
  783. else if (typeof data2.background === 'string')
  784. data2.background = new Color().setStyle(data2.background, LinearSRGBColorSpace)
  785. else if (data2.background?.isColor) data2.background = new Color(data2.background)
  786. if (data2.background?.isColor) { // color
  787. this._scene.backgroundColor = data2.background
  788. this._scene.background = null
  789. } else if (!data2.background) { // null
  790. this._scene.backgroundColor = null
  791. this._scene.background = null
  792. } else { // texture or 'environment'
  793. this._scene.backgroundColor = new Color('#ffffff')
  794. if (!data2.scene) data2.scene = {}
  795. data2.scene.background = data2.background
  796. }
  797. delete data2.background
  798. }
  799. // endregion
  800. if (!meta && data2.resources && data2.resources.__isLoadedResources) {
  801. meta = data2.resources as SerializationMetaType
  802. delete data2.resources
  803. }
  804. if (!meta?.__isLoadedResources) {
  805. this.console.error('meta in fromJSON is not available or is not loaded resources, call viewer.loadConfigResources first, or directly use viewer.importConfig')
  806. return null
  807. }
  808. if (Array.isArray(data2.plugins)) {
  809. this.deserializePlugins(data2.plugins, meta)
  810. delete data2.plugins
  811. }
  812. // meta = meta || data.resources
  813. ThreeSerialization.Deserialize(data2, this, meta, true)
  814. // todo: handle
  815. // __useCount set in ThreeSerialization while deserializing resources
  816. // for (const mat of Object.values(resources.materials) as any) {
  817. // if (!mat.__useCount) this.materialManager?.unregisterMaterial(mat) // todo: also dispose?
  818. // else delete mat.__useCount
  819. // }
  820. // for (const tex of Object.values(resources.textures) as any) {
  821. // if (!tex.__useCount) {
  822. // // todo: dispose?
  823. // } else {
  824. // delete tex.__useCount
  825. // }
  826. // }
  827. return this
  828. }
  829. loadConfigResources = async(json: Partial<SerializationMetaType>, extraResources?: Partial<SerializationResourcesType>): Promise<any> => {
  830. // this.console.log(json)
  831. if (json.__isLoadedResources) return json
  832. const meta = metaFromResources(json, this)
  833. return await MetaImporter.ImportMeta(meta, extraResources)
  834. }
  835. async addSceneObject<T extends IObject3D|Object3D|RootSceneImportResult = RootSceneImportResult>(imported: T, options?: AddObjectOptions): Promise<T> {
  836. if (imported.userData?.rootSceneModelRoot) {
  837. const obj = <RootSceneImportResult>imported
  838. if (obj.importedViewerConfig && options?.importConfig !== false) await this.importConfig(obj.importedViewerConfig)
  839. this._scene.loadModelRoot(obj, options)
  840. return this._scene.modelRoot as T
  841. }
  842. this._scene.addObject(imported, options)
  843. return imported
  844. }
  845. private _setActiveCameraView(event: any = {}): void {
  846. if (event.type === 'setView') {
  847. if (!event.camera) {
  848. this.console.warn('Cannot find camera', event)
  849. return
  850. }
  851. this._scene.mainCamera.copy(event.camera)
  852. } else if (event.type === 'activateMain')
  853. this._scene.mainCamera = event.camera || undefined // event.camera should have been upgraded when added to the scene.
  854. }
  855. private _resolvePluginOrClass<T extends IViewerPlugin>(plugin: T | Class<T>, ...args: ConstructorParameters<Class<T>>): T {
  856. let p: T
  857. if ((plugin as Class<IViewerPlugin>).prototype) p = new (plugin as Class<T>)(...args)
  858. else p = plugin as T
  859. if ((plugin as Class<IViewerPlugin>).prototype) {
  860. const p1 = this.getPlugin(plugin as Class<T>)
  861. if (p1) {
  862. this.console.error(`Plugin of type ${p1.constructor.PluginType} already exists, no new plugin created`, p1)
  863. return p1
  864. }
  865. p = new (plugin as Class<T>)(...args)
  866. } else p = plugin as T
  867. return p
  868. }
  869. async doOnce<TRet>(event: IViewerEventTypes, func: (...args: any[]) => TRet): Promise<TRet> {
  870. return new Promise((resolve) => {
  871. const listener = async(...args: any[]) => {
  872. this.removeEventListener(event, listener)
  873. resolve(await func(...args))
  874. }
  875. this.addEventListener(event, listener)
  876. })
  877. }
  878. /**
  879. * Export the scene to a file (default: glb with viewer config) and return a blob
  880. * @param options
  881. */
  882. async exportScene(options?: ExportFileOptions): Promise<BlobExt | undefined> {
  883. return this.assetManager.exporter.exportObject(this._scene.modelRoot, options)
  884. }
  885. async getScreenshotBlob({mimeType = 'image/jpeg', quality = 90} = {}): Promise<Blob | null> {
  886. const blobPromise = async()=> new Promise<Blob|null>((resolve) => {
  887. this._canvas.toBlob((blob) => {
  888. resolve(blob)
  889. }, mimeType, quality)
  890. })
  891. if (!this.renderEnabled) return blobPromise()
  892. return await this.doOnce('postFrame', async() => {
  893. this.renderEnabled = false
  894. const blob = await blobPromise()
  895. this.renderEnabled = true
  896. return blob
  897. })
  898. }
  899. async getScreenshotDataUrl({mimeType = 'image/jpeg', quality = 90} = {}): Promise<string | null> {
  900. if (!this.renderEnabled) return this._canvas.toDataURL(mimeType, quality)
  901. return await this.doOnce('postFrame', () => this._canvas.toDataURL(mimeType, quality))
  902. }
  903. private _renderEnabledChanged(): void {
  904. this.dispatchEvent({type: this.renderEnabled ? 'renderEnabled' : 'renderDisabled'})
  905. }
  906. private readonly _defaultConfig: ISerializedViewerConfig = {
  907. assetType: 'config',
  908. type: this.type,
  909. version: ThreeViewer.VERSION,
  910. metadata: {
  911. generator: 'ThreePipe',
  912. version: 1,
  913. },
  914. plugins: [],
  915. }
  916. // region legacy creation functions
  917. // /**
  918. // * Converts a three.js Camera instance to be used in the viewer.
  919. // * @param camera - The three.js OrthographicCamera or PerspectiveCamera instance
  920. // * @returns {CameraController} - A wrapper around the camera with some useful methods and properties.
  921. // */
  922. // createCamera(camera: OrthographicCamera | PerspectiveCamera): CameraController {
  923. // const cam: CameraController = camera.userData.iCamera ?? new CameraController(camera, {
  924. // controlsMode: '',
  925. // controlsEnabled: false,
  926. // }, this._canvas)
  927. // if (camera.userData.autoLookAtTarget === undefined) {
  928. // cam.autoLookAtTarget = false
  929. // camera.userData.autoLookAtTarget = false
  930. // } else {
  931. // cam.autoLookAtTarget = camera.userData.autoLookAtTarget
  932. // }
  933. // return cam
  934. // }
  935. // /**
  936. // * Create a new empty object in the scene or add an existing three.js object to the scene.
  937. // * @param object
  938. // */
  939. // async createObject3D(object?: Object3D): Promise<Object3DModel | undefined> {
  940. // return this.getManager()?.addImportedSingle<Object3DModel>(object || new Object3D(), {autoScale: false, pseudoCenter: false})
  941. // }
  942. // /**
  943. // * Create a new physical material from a template or another material. It returns the same material if a material is passed created by the material manager.
  944. // * @param material
  945. // */
  946. // createPhysicalMaterial(material?: Material|MeshPhysicalMaterialParameters): MeshStandardMaterial2 | undefined {
  947. // return this.createMaterial<MeshStandardMaterial2>('standard', material)
  948. // }
  949. // /**
  950. // * Create a new material from a template or another material. It returns the same material if a material is passed created by the material manager.
  951. // * @param template - template name registered in MaterialManager
  952. // * @param material - three.js material object or material params to create a new material
  953. // */
  954. // createMaterial<T extends IMaterial<any>>(template: 'standard' | 'basic' | 'diamond' | string, material?: Material|any): T | undefined {
  955. // if ((material as Material)?.isMaterial) {
  956. // const f = this.getManager()?.materials?.findMaterial((material as Material).uuid)
  957. // if (f) return f as T
  958. // }
  959. // return this.getManager()?.materials?.generateFromTemplate(template, material) as T
  960. // }
  961. // endregion
  962. }