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

ThreeViewer.ts 41KB

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