threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ThreeViewer.ts 57KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408
  1. import {
  2. BaseEvent,
  3. CanvasTexture,
  4. Color,
  5. Event,
  6. EventDispatcher,
  7. LinearSRGBColorSpace,
  8. Object3D,
  9. Quaternion,
  10. Scene,
  11. Vector2,
  12. Vector3,
  13. } from 'three'
  14. import {Class, createCanvasElement, downloadBlob, onChange, serialize, ValOrArr} from 'ts-browser-helpers'
  15. import {TViewerScreenShader} from '../postprocessing'
  16. import {
  17. AddObjectOptions,
  18. IAnimationLoopEvent,
  19. IMaterial,
  20. IObject3D,
  21. IObjectProcessor,
  22. ITexture,
  23. PerspectiveCamera2,
  24. RootScene,
  25. TCameraControlsMode,
  26. } from '../core'
  27. import {ViewerRenderManager} from './ViewerRenderManager'
  28. import {
  29. convertArrayBufferToStringsInMeta,
  30. EasingFunctionType,
  31. getEmptyMeta,
  32. GLStatsJS,
  33. IDialogWrapper,
  34. jsonToBlob,
  35. metaFromResources,
  36. MetaImporter,
  37. metaToResources,
  38. SerializationMetaType,
  39. SerializationResourcesType,
  40. ThreeSerialization,
  41. windowDialogWrapper,
  42. } from '../utils'
  43. import {
  44. AssetManager,
  45. AssetManagerOptions,
  46. BlobExt,
  47. ExportFileOptions,
  48. IAsset,
  49. ImportAddOptions,
  50. ImportAssetOptions,
  51. ImportResult,
  52. RootSceneImportResult,
  53. } from '../assetmanager'
  54. import {IViewerPlugin, IViewerPluginSync} from './IViewerPlugin'
  55. import {uiConfig, UiObjectConfig, uiPanelContainer} from 'uiconfig.js'
  56. import {IRenderTarget} from '../rendering'
  57. import {
  58. AssetExporterPlugin,
  59. CameraViewPlugin,
  60. CanvasSnapshotPlugin,
  61. FileTransferPlugin,
  62. ProgressivePlugin,
  63. } from '../plugins'
  64. // noinspection ES6PreferShortImport
  65. import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin'
  66. // noinspection ES6PreferShortImport
  67. import {TonemapPlugin} from '../plugins/postprocessing/TonemapPlugin'
  68. import {VERSION} from './version'
  69. import {Easing} from 'popmotion'
  70. import {OrbitControls3} from '../three'
  71. export interface IViewerEvent extends BaseEvent, Partial<IAnimationLoopEvent> {
  72. type: '*'|'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'removePlugin'|'renderEnabled'|'renderDisabled'
  73. eType?: '*'|'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'removePlugin'|'renderEnabled'|'renderDisabled'
  74. [p: string]: any
  75. }
  76. export type IViewerEventTypes = IViewerEvent['type']
  77. export interface ISerializedConfig {
  78. assetType: 'config',
  79. type: string,
  80. metadata?: {
  81. generator: string,
  82. version: number,
  83. [key: string]: any
  84. },
  85. [key: string]: any
  86. }
  87. export interface ISerializedViewerConfig extends ISerializedConfig{
  88. type: 'ThreeViewer'|'ViewerApp',
  89. version: string,
  90. plugins: ISerializedConfig[],
  91. resources?: Partial<SerializationResourcesType> | SerializationMetaType
  92. renderManager?: any // todo
  93. scene?: any
  94. [key: string]: any
  95. }
  96. export type IConsoleWrapper = Partial<Console> & Pick<Console, 'log'|'warn'|'error'>
  97. /**
  98. * Options for the ThreeViewer creation.
  99. * @category Viewer
  100. */
  101. export interface ThreeViewerOptions {
  102. /**
  103. * The canvas element to use for rendering. Only one of container and canvas must be specified.
  104. */
  105. canvas?: HTMLCanvasElement,
  106. /**
  107. * The container for the canvas. A new canvas will be created in this container. Only one of container and canvas must be specified.
  108. */
  109. container?: HTMLElement,
  110. /**
  111. * The fragment shader snippet to render on screen.
  112. */
  113. screenShader?: TViewerScreenShader,
  114. /**
  115. * Use MSAA.
  116. */
  117. msaa?: boolean,
  118. /**
  119. * Use Uint8 RGBM HDR Render Pipeline.
  120. * Provides better performance with post-processing.
  121. * RenderManager Uses Half-float if set to false.
  122. */
  123. rgbm?: boolean
  124. /**
  125. * Use rendered gbuffer as depth-prepass / z-prepass. (Requires DepthBufferPlugin/GBufferPlugin)
  126. * todo: It will be disabled when there are any transparent/transmissive objects with render to depth buffer enabled.
  127. */
  128. zPrepass?: boolean
  129. /**
  130. * Force z-prepass even if there are transparent/transmissive objects with render to depth buffer enabled.
  131. */
  132. forceZPrepass?: boolean // todo
  133. /*
  134. * Render scale, 1 = full resolution, 0.5 = half resolution, 2 = double resolution.
  135. * Same as pixelRatio in three.js
  136. * Can be set to `window.devicePixelRatio` to render at device resolution in browsers.
  137. * An optimal value is `Math.min(2, window.devicePixelRatio)` to prevent issues on mobile. This is set when 'auto' is passed.
  138. * Default is 1.
  139. */
  140. renderScale?: number | 'auto'
  141. debug?: boolean
  142. /**
  143. * Add initial plugins.
  144. */
  145. plugins?: (IViewerPluginSync | Class<IViewerPluginSync>)[]
  146. load?: {
  147. /**
  148. * Load one or more source files
  149. */
  150. src?: ValOrArr<string | IAsset | null>
  151. /**
  152. * Load environment map
  153. */
  154. environment?: string | IAsset | ITexture | undefined | null
  155. /**
  156. * Load background map
  157. */
  158. background?: string | IAsset | ITexture | undefined | null
  159. }
  160. onLoad?: (results: any) => void
  161. /**
  162. * TonemapPlugin is added to the viewer if this is true.
  163. * @default true
  164. */
  165. tonemap?: boolean
  166. camera?: {
  167. controlsMode?: TCameraControlsMode,
  168. position?: Vector3,
  169. target?: Vector3,
  170. }
  171. /**
  172. * Options for the asset manager.
  173. */
  174. assetManager?: AssetManagerOptions
  175. /**
  176. * Add the dropzone plugin to the viewer, allowing to drag and drop files into the viewer over the canvas/container.
  177. * Set to true/false to enable/disable the plugin, or pass options to configure the plugin. Assuming true if options are passed.
  178. * @default - false
  179. */
  180. dropzone?: boolean|DropzonePluginOptions
  181. /**
  182. * @deprecated use {@link msaa} instead
  183. */
  184. isAntialiased?: boolean,
  185. /**
  186. * @deprecated use {@link rgbm} instead
  187. */
  188. useRgbm?: boolean
  189. /**
  190. * @deprecated use {@link zPrepass} instead
  191. */
  192. useGBufferDepth?: boolean
  193. }
  194. /**
  195. * Three Viewer
  196. *
  197. * The ThreeViewer is the main class in the framework to manage a scene, render and add plugins to it.
  198. * @category Viewer
  199. */
  200. @uiPanelContainer('Viewer')
  201. export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes> {
  202. public static readonly VERSION = VERSION
  203. public static readonly ConfigTypeSlug = 'vjson'
  204. uiConfig!: UiObjectConfig
  205. static Console: IConsoleWrapper = {
  206. log: console.log.bind(console),
  207. warn: console.warn.bind(console),
  208. error: console.error.bind(console),
  209. }
  210. static Dialog: IDialogWrapper = windowDialogWrapper
  211. /**
  212. * If the viewer is enabled. Set this `false` to disable RAF loop.
  213. * @type {boolean}
  214. */
  215. enabled = true
  216. /**
  217. * Enable or disable all rendering, Animation loop including any frame/render events won't be fired when this is false.
  218. */
  219. @onChange(ThreeViewer.prototype._renderEnabledChanged)
  220. renderEnabled = true
  221. renderStats: GLStatsJS
  222. readonly assetManager: AssetManager
  223. @uiConfig() @serialize('renderManager')
  224. readonly renderManager: ViewerRenderManager
  225. get materialManager() {
  226. return this.assetManager.materials
  227. }
  228. public readonly plugins: Record<string, IViewerPlugin> = {}
  229. /**
  230. * Scene with object hierarchy used for rendering
  231. */
  232. get scene(): RootScene&Scene {
  233. return this._scene as RootScene&Scene
  234. }
  235. /**
  236. * Specifies how many frames to render in a single request animation frame. Keep to 1 for realtime rendering.
  237. * Note: should be max (screen refresh rate / animation frame rate) like 60Hz / 30fps
  238. * @type {number}
  239. */
  240. maxFramePerLoop = 1
  241. readonly debug: boolean
  242. /**
  243. * Number of times to run composer render. If set to more than 1, preRender and postRender events will also be called multiple times.
  244. */
  245. rendersPerFrame = 1
  246. /**
  247. * Get the HTML Element containing the canvas
  248. * @returns {HTMLElement}
  249. */
  250. get container(): HTMLElement {
  251. // todo console.warn('container is deprecated, NOTE: subscribe to events when the canvas is moved to another container')
  252. if (this._canvas.parentElement !== this._container) {
  253. this.console.error('ThreeViewer: Canvas is not in the container, this might cause issues with some plugins.')
  254. }
  255. return this._container
  256. }
  257. /**
  258. * Get the HTML Canvas Element where the viewer is rendering
  259. * @returns {HTMLCanvasElement}
  260. */
  261. get canvas(): HTMLCanvasElement {
  262. return this._canvas
  263. }
  264. get console(): IConsoleWrapper {
  265. return ThreeViewer.Console
  266. }
  267. get dialog(): IDialogWrapper {
  268. return ThreeViewer.Dialog
  269. }
  270. @serialize() readonly type = 'ThreeViewer'
  271. /**
  272. * The ResizeObserver observing the canvas element. Add more elements to this observer to resize viewer on their size change.
  273. * @type {ResizeObserver | undefined}
  274. */
  275. readonly resizeObserver = window?.ResizeObserver ? new window.ResizeObserver(_ => this.resize()) : undefined
  276. private readonly _canvas: HTMLCanvasElement
  277. // this can be used by other plugins to add ui elements alongside the canvas
  278. private readonly _container: HTMLElement // todo: add a way to move the canvas to a new container... and dispatch event...
  279. /**
  280. * The Scene attached to the viewer, this cannot be changed.
  281. * @type {RootScene}
  282. */
  283. @uiConfig() @serialize('scene')
  284. private readonly _scene: RootScene
  285. private _needsResize = false
  286. private _isRenderingFrame = false
  287. private _objectProcessor: IObjectProcessor = {
  288. processObject: (object: IObject3D)=>{
  289. if (object.material) {
  290. if (Array.isArray(object.material)) this.assetManager.materials.registerMaterials(object.material)
  291. else this.assetManager.materials.registerMaterial(object.material)
  292. }
  293. },
  294. }
  295. private _needsReset = true // renderer needs reset
  296. // Helpers for tracking main camera change and setting dirty automatically
  297. private _lastCameraPosition: Vector3 = new Vector3()
  298. private _lastCameraQuat: Quaternion = new Quaternion()
  299. private _lastCameraTarget: Vector3 = new Vector3()
  300. private _tempVec: Vector3 = new Vector3()
  301. private _tempQuat: Quaternion = new Quaternion()
  302. /**
  303. * If any of the viewers are in debug mode, this will be true.
  304. * This is required for debugging/logging in some cases.
  305. */
  306. public static ViewerDebugging = false // todo use in shaderReplaceString
  307. /**
  308. * Create a viewer instance for using the webgi viewer SDK.
  309. * @param options - {@link ThreeViewerOptions}
  310. */
  311. constructor({debug = false, ...options}: ThreeViewerOptions) {
  312. super()
  313. this.debug = debug
  314. if (debug) ThreeViewer.ViewerDebugging = true
  315. this._canvas = options.canvas || createCanvasElement()
  316. let container = options.container
  317. if (container && !options.canvas) container.appendChild(this._canvas)
  318. if (!container) container = this._canvas.parentElement ?? undefined
  319. if (!container) throw new Error('No container(or canvas).')
  320. this._container = container
  321. this.setDirty = this.setDirty.bind(this)
  322. this._animationLoop = this._animationLoop.bind(this)
  323. this._setActiveCameraView = this._setActiveCameraView.bind(this)
  324. this.renderStats = new GLStatsJS(this._container)
  325. if (debug) this.renderStats.show()
  326. if (!(window as any).threeViewers) (window as any).threeViewers = [];
  327. (window as any).threeViewers.push(this)
  328. // camera
  329. const camera = new PerspectiveCamera2(options.camera?.controlsMode ?? 'orbit', this._canvas)
  330. camera.name = 'Default Camera'
  331. options.camera?.position ? camera.position.copy(options.camera.position) : camera.position.set(0, 0, 5)
  332. options.camera?.target ? camera.target.copy(options.camera.target) : camera.target.set(0, 0, 0)
  333. camera.setDirty()
  334. camera.userData.autoLookAtTarget = true // only for when controls are disabled / not available
  335. // Update camera controls postFrame if allowed to interact
  336. this.addEventListener('postFrame', () => { // todo: move inside RootScene.
  337. const cam = this._scene.mainCamera
  338. if (cam && cam.canUserInteract) {
  339. const d = this.getPlugin<ProgressivePlugin>('ProgressivePlugin')?.postFrameConvergedRecordingDelta()
  340. // if (d && d > 0) delta = d
  341. if (d !== undefined && d === 0) return // not converged yet.
  342. // if d < 0 or undefined: not recording, do nothing
  343. cam.controls?.update()
  344. }
  345. })
  346. // if camera position or target changed in last frame, call setDirty on camera
  347. this.addEventListener('preFrame', () => { // todo: move inside RootScene.
  348. const cam = this._scene.mainCamera
  349. if (
  350. cam.getWorldPosition(this._tempVec).sub(this._lastCameraPosition).lengthSq() // position is in local space
  351. + this._tempVec.subVectors(cam.target, this._lastCameraTarget).lengthSq() // target is in world space
  352. + cam.getWorldQuaternion(this._tempQuat).angleTo(this._lastCameraQuat)
  353. > 0.000001) cam.setDirty()
  354. })
  355. // scene
  356. this._scene = new RootScene(camera, this._objectProcessor)
  357. this._scene.setBackgroundColor('#ffffff')
  358. // this._scene.addEventListener('addSceneObject', this._addSceneObject)
  359. this._scene.addEventListener('setView', this._setActiveCameraView)
  360. this._scene.addEventListener('activateMain', this._setActiveCameraView)
  361. this._scene.addEventListener('materialUpdate', (e) => this.setDirty(this._scene, e))
  362. this._scene.addEventListener('materialChanged', (e) => this.setDirty(this._scene, e))
  363. this._scene.addEventListener('objectUpdate', (e) => this.setDirty(this._scene, e))
  364. this._scene.addEventListener('textureUpdate', (e) => this.setDirty(this._scene, e))
  365. this._scene.addEventListener('sceneUpdate', (e) => {
  366. this.setDirty(this._scene, e)
  367. if (e.geometryChanged === false) return
  368. this.renderManager.resetShadows()
  369. })
  370. this._scene.addEventListener('mainCameraUpdate', () => {
  371. this._scene.mainCamera.getWorldPosition(this._lastCameraPosition)
  372. this._lastCameraTarget.copy(this._scene.mainCamera.target)
  373. this._scene.mainCamera.getWorldQuaternion(this._lastCameraQuat)
  374. })
  375. // render manager
  376. if (options.isAntialiased !== undefined || options.useRgbm !== undefined || options.useGBufferDepth !== undefined) {
  377. this.console.warn('isAntialiased, useRgbm and useGBufferDepth are deprecated, use msaa, rgbm and zPrepass instead.')
  378. }
  379. this.renderManager = new ViewerRenderManager({
  380. canvas: this._canvas,
  381. msaa: options.msaa ?? options.isAntialiased ?? false,
  382. rgbm: options.rgbm ?? options.useRgbm ?? false,
  383. zPrepass: options.zPrepass ?? options.useGBufferDepth ?? false,
  384. depthBuffer: !(options.zPrepass ?? options.useGBufferDepth ?? false),
  385. screenShader: options.screenShader,
  386. renderScale: typeof options.renderScale === 'string' ? options.renderScale === 'auto' ?
  387. Math.min(2, window.devicePixelRatio) : parseFloat(options.renderScale) :
  388. options.renderScale,
  389. })
  390. this.renderManager.addEventListener('animationLoop', this._animationLoop as any)
  391. this.renderManager.addEventListener('resize', ()=> this._scene.mainCamera.refreshAspect())
  392. this.renderManager.addEventListener('update', (e) => {
  393. if (e.change === 'registerPass' && e.pass?.materialExtension)
  394. this.assetManager.materials.registerMaterialExtension(e.pass.materialExtension)
  395. else if (e.change === 'unregisterPass' && e.pass?.materialExtension)
  396. this.assetManager.materials.unregisterMaterialExtension(e.pass.materialExtension)
  397. this.setDirty(this.renderManager, e)
  398. })
  399. this.assetManager = new AssetManager(this, options.assetManager)
  400. if (this.resizeObserver) this.resizeObserver.observe(this._canvas)
  401. // sometimes resize observer is late, so extra check
  402. window && window.addEventListener('resize', this.resize)
  403. this._canvas.addEventListener('webglcontextrestored', this._onContextRestore, false)
  404. this._canvas.addEventListener('webglcontextlost', this._onContextLost, false)
  405. if (options.dropzone) {
  406. this.addPluginSync(new DropzonePlugin(typeof options.dropzone === 'object' ? options.dropzone : undefined))
  407. }
  408. if (options.tonemap !== false) {
  409. this.addPluginSync(new TonemapPlugin())
  410. }
  411. for (const p of options.plugins ?? []) this.addPluginSync(p)
  412. this.console.log('ThreePipe Viewer instance initialized, version: ', ThreeViewer.VERSION)
  413. if (options.load) {
  414. const sources = [options.load.src].flat().filter(s=> s)
  415. const promises: Promise<any>[] = sources.map(async s=> s && this.load(s))
  416. if (options.load.environment) promises.push(this.setEnvironmentMap(options.load.environment))
  417. if (options.load.background) promises.push(this.setBackgroundMap(options.load.background))
  418. Promise.all(promises).then(options.onLoad)
  419. }
  420. }
  421. /**
  422. * Add an object/model/material/viewer-config/plugin-preset/... to the viewer scene from url or an {@link IAsset} object.
  423. * Same as {@link AssetManager.addAssetSingle}
  424. * @param obj
  425. * @param options
  426. */
  427. async load<T extends ImportResult = ImportResult>(obj: string | IAsset | File | null, options?: ImportAddOptions) {
  428. if (!obj) return
  429. return await this.assetManager.addAssetSingle<T>(obj, options)
  430. }
  431. /**
  432. * Imports an object/model/material/texture/viewer-config/plugin-preset/... to the viewer scene from url or an {@link IAsset} object.
  433. * Same as {@link AssetImporter.importSingle}
  434. * @param obj
  435. * @param options
  436. */
  437. async import<T extends ImportResult = ImportResult>(obj: string | IAsset | null, options?: ImportAddOptions) {
  438. if (!obj) return
  439. return await this.assetManager.importer.importSingle<T>(obj, options)
  440. }
  441. /**
  442. * Set the environment map of the scene from url or an {@link IAsset} object.
  443. * @param map
  444. * @param setBackground - Set the background image of the scene from the same map.
  445. * @param options - Options for importing the asset. See {@link ImportAssetOptions}
  446. */
  447. async setEnvironmentMap(map: string | IAsset | null | ITexture | undefined, {setBackground = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> {
  448. this._scene.environment = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null
  449. if (setBackground) return this.setBackgroundMap(this._scene.environment)
  450. return this._scene.environment
  451. }
  452. /**
  453. * Set the background image of the scene from url or an {@link IAsset} object.
  454. * @param map
  455. * @param setEnvironment - Set the environment map of the scene from the same map.
  456. * @param options - Options for importing the asset. See {@link ImportAssetOptions}
  457. */
  458. async setBackgroundMap(map: string | IAsset | null | ITexture | undefined, {setEnvironment = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> {
  459. this._scene.background = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null
  460. if (setEnvironment) return this.setEnvironmentMap(this._scene.background)
  461. return this._scene.background
  462. }
  463. /**
  464. * Exports an object/mesh/material/texture/render-target/plugin-preset/viewer to a blob.
  465. * If no object is given, a glb is exported with the current viewer state.
  466. * @param obj
  467. * @param options
  468. */
  469. async export(obj?: IObject3D|IMaterial|ITexture|IRenderTarget|IViewerPlugin|(typeof this), options?: ExportFileOptions) {
  470. if (!obj) obj = this._scene.modelRoot // this will export the glb with the scene and viewer config
  471. if ((<typeof this>obj).type === this.type) return jsonToBlob((<typeof this>obj).exportConfig())
  472. if ((<IViewerPlugin>obj).constructor?.PluginType) return jsonToBlob(this.exportPluginConfig(<IViewerPlugin>obj))
  473. return await this.assetManager.exporter.exportObject(<IObject3D|IMaterial|ITexture|IRenderTarget>obj, options)
  474. }
  475. /**
  476. * Export the scene to a file (default: glb with viewer config) and return a blob
  477. * @param options
  478. * @param useExporterPlugin - uses the {@link AssetExporterPlugin} if available. This is useful to use the options configured by the user in the plugin.
  479. */
  480. async exportScene(options?: ExportFileOptions, useExporterPlugin = true): Promise<BlobExt | undefined> {
  481. const exporter = useExporterPlugin ? this.getPlugin<AssetExporterPlugin>('AssetExporterPlugin') : undefined
  482. if (exporter) return exporter.exportScene(options)
  483. return this.assetManager.exporter.exportObject(this._scene.modelRoot, options)
  484. }
  485. /**
  486. * Returns a blob with the screenshot of the canvas.
  487. * If {@link CanvasSnapshotPlugin} is added, it will be used, otherwise canvas.toBlob will be used directly.
  488. * @param mimeType default image/jpeg
  489. * @param quality between 0 and 100
  490. */
  491. async getScreenshotBlob({mimeType = 'image/jpeg', quality = 90} = {}): Promise<Blob | null | undefined> {
  492. const plugin = this.getPlugin<CanvasSnapshotPlugin>('CanvasSnapshotPlugin')
  493. if (plugin) {
  494. return plugin.getFile('snapshot.' + mimeType.split('/')[1], {mimeType, quality, waitForProgressive: true})
  495. }
  496. const blobPromise = async()=> new Promise<Blob|null>((resolve) => {
  497. this._canvas.toBlob((blob) => {
  498. resolve(blob)
  499. }, mimeType, quality)
  500. })
  501. if (!this.renderEnabled) return blobPromise()
  502. return await this.doOnce('postFrame', async() => {
  503. this.renderEnabled = false
  504. const blob = await blobPromise()
  505. this.renderEnabled = true
  506. return blob
  507. })
  508. }
  509. async getScreenshotDataUrl({mimeType = 'image/jpeg', quality = 0.9} = {}): Promise<string | null | undefined> {
  510. if (!this.renderEnabled) return this._canvas.toDataURL(mimeType, quality)
  511. return await this.doOnce('postFrame', () => this._canvas.toDataURL(mimeType, quality))
  512. }
  513. /**
  514. * Disposes the viewer and frees up all resource and events. Do not use the viewer after calling dispose.
  515. * @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()`
  516. * This function is not fully implemented yet. There might be some leaks.
  517. * @todo - return promise?
  518. */
  519. public dispose(clear = true): void {
  520. // todo: dispose stuff from constructor etc
  521. if (clear) {
  522. for (const plugin of [...Object.values(this.plugins)]) {
  523. this.removePlugin(plugin, true)
  524. }
  525. }
  526. this._scene.dispose(clear)
  527. this.renderManager.dispose(clear)
  528. if (clear) {
  529. this._canvas.removeEventListener('webglcontextrestored', this._onContextRestore, false)
  530. this._canvas.removeEventListener('webglcontextlost', this._onContextLost, false)
  531. ;(window as any).threeViewers?.splice((window as any).threeViewers.indexOf(this), 1)
  532. if (this.resizeObserver) this.resizeObserver.unobserve(this._canvas)
  533. window.removeEventListener('resize', this.resize)
  534. }
  535. this.dispatchEvent({type: 'dispose', clear})
  536. }
  537. /**
  538. * 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.
  539. */
  540. resize = () => {
  541. this._needsResize = true
  542. this.setDirty()
  543. }
  544. /**
  545. * Set the viewer to dirty and trigger render of the next frame.
  546. * @param source - The source of the dirty event. like plugin or 3d object
  547. * @param event - The event that triggered the dirty event.
  548. */
  549. setDirty(source?: any, event?: Event): void {
  550. this._needsReset = true
  551. source = source ?? this
  552. this.dispatchEvent({...event ?? {}, type: 'update', source})
  553. }
  554. protected _animationLoop(event: IAnimationLoopEvent): void {
  555. if (!this.enabled || !this.renderEnabled) return
  556. if (this._isRenderingFrame) {
  557. this.console.warn('animation loop: frame skip') // not possible actually, since this is not async
  558. return
  559. }
  560. this._isRenderingFrame = true
  561. this.renderStats.begin()
  562. for (let i = 0; i < this.maxFramePerLoop; i++) {
  563. if (this._needsReset) {
  564. this.renderManager.reset()
  565. this._needsReset = false
  566. }
  567. if (this._needsResize) {
  568. const size = [this._canvas.clientWidth, this._canvas.clientHeight]
  569. if (event.xrFrame) { // todo: find a better way to resize for XR.
  570. const cam = this.renderManager.webglRenderer.xr.getCamera()?.cameras[0]?.viewport
  571. if (cam) {
  572. if (cam.x !== 0 || cam.y !== 0) {
  573. this.console.warn('x and y must be 0?')
  574. }
  575. size[0] = cam.width
  576. size[1] = cam.height
  577. this.console.log('resize for xr', size)
  578. } else {
  579. this._needsResize = false
  580. }
  581. }
  582. if (this._needsResize) {
  583. this.renderManager.setSize(...size)
  584. this._needsResize = false
  585. }
  586. }
  587. this.dispatchEvent({...event, type: 'preFrame', target: this}) // event will have time, deltaTime and xrFrame
  588. const dirtyPlugins = Object.values(this.plugins).filter(value => value.dirty)
  589. if (dirtyPlugins.length > 0) {
  590. // console.log('dirty plugins', dirtyPlugins)
  591. this.setDirty(dirtyPlugins)
  592. }
  593. if (this._needsReset) {
  594. this.renderManager.reset()
  595. this._needsReset = false
  596. }
  597. // Check if the renderManger is dirty, which happens when it's reset above or if any pass in the composer is dirty
  598. const needsRender = this.renderManager.needsRender
  599. if (needsRender) {
  600. for (let j = 0; j < this.rendersPerFrame; j++) {
  601. this.dispatchEvent({type: 'preRender', target: this})
  602. try {
  603. const cam = this._scene.mainCamera
  604. this._scene.renderCamera = cam
  605. if (cam.visible) this.renderManager.render(this._scene, this.renderManager.defaultRenderToScreen)
  606. } catch (e) {
  607. this.console.error(e)
  608. if (this.debug) throw e
  609. // this.enabled = false
  610. }
  611. this.dispatchEvent({type: 'postRender', target: this})
  612. }
  613. }
  614. this.dispatchEvent({type: 'postFrame', target: this})
  615. this.renderManager.onPostFrame()
  616. if (!needsRender) // break if no frame rendered
  617. break
  618. }
  619. this.renderStats.end()
  620. this._isRenderingFrame = false
  621. }
  622. /**
  623. * Get the Plugin by a constructor type or by the string type.
  624. * Use string type if the plugin is not a dependency and you don't want to bundle the plugin.
  625. * @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
  626. * @returns {T | undefined} - The plugin of the specified type.
  627. */
  628. getPlugin<T extends IViewerPlugin>(type: Class<T>|string): T | undefined {
  629. return this.plugins[typeof type === 'string' ? type : (type as any).PluginType] as T | undefined
  630. }
  631. /**
  632. * Get the Plugin by a constructor type or add a new plugin of the specified type if it doesn't exist.
  633. * @param type
  634. * @param args - arguments for the constructor of the plugin, used when a new plugin is created.
  635. */
  636. async getOrAddPlugin<T extends IViewerPlugin>(type: Class<T>, ...args: ConstructorParameters<Class<T>>): Promise<T> {
  637. const plugin = this.getPlugin(type)
  638. if (plugin) return plugin
  639. return this.addPlugin(type, ...args)
  640. }
  641. /**
  642. * Get the Plugin by a constructor type or add a new plugin to the viewer of the specified type if it doesn't exist(sync).
  643. * @param type
  644. * @param args - arguments for the constructor of the plugin, used when a new plugin is created.
  645. */
  646. getOrAddPluginSync<T extends IViewerPluginSync>(type: Class<T>, ...args: ConstructorParameters<Class<T>>): T {
  647. const plugin = this.getPlugin(type)
  648. if (plugin) return plugin
  649. return this.addPluginSync(type, ...args)
  650. }
  651. /**
  652. * Add a plugin to the viewer.
  653. * @param plugin - The instance of the plugin to add or the class of the plugin to add.
  654. * @param args - Arguments for the constructor of the plugin, in case a class is passed.
  655. * @returns {Promise<T>} - The plugin added.
  656. */
  657. async addPlugin<T extends IViewerPlugin>(plugin: T | Class<T>, ...args: ConstructorParameters<Class<T>>): Promise<T> {
  658. const p = this._resolvePluginOrClass(plugin, ...args)
  659. const type = p.constructor.PluginType
  660. if (!p.constructor.PluginType) {
  661. this.console.error('PluginType is not defined for', p)
  662. return p
  663. }
  664. for (const d of p.dependencies || []) {
  665. await this.getOrAddPlugin(d)
  666. }
  667. if (this.plugins[type]) {
  668. 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)
  669. await this.removePlugin(this.plugins[type])
  670. }
  671. this.plugins[type] = p
  672. const oldType = p.constructor.OldPluginType
  673. if (oldType && this.plugins[oldType]) this.console.error('Plugin type mismatch')
  674. if (oldType) this.plugins[oldType] = p
  675. await p.onAdded(this)
  676. this.dispatchEvent({type: 'addPlugin', target: this, plugin: p})
  677. this.setDirty(p)
  678. return p
  679. }
  680. /**
  681. * Add a plugin to the viewer(sync).
  682. * @param plugin
  683. * @param args
  684. */
  685. addPluginSync<T extends IViewerPluginSync>(plugin: T|Class<T>, ...args: ConstructorParameters<Class<T>>): T {
  686. const p = this._resolvePluginOrClass(plugin, ...args)
  687. const type = p.constructor.PluginType
  688. if (!p.constructor.PluginType) {
  689. this.console.error('PluginType is not defined for', p)
  690. return p
  691. }
  692. for (const d of p.dependencies || []) {
  693. this.getOrAddPluginSync(d)
  694. }
  695. if (this.plugins[type]) {
  696. 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)
  697. this.removePluginSync(this.plugins[type])
  698. }
  699. this.plugins[type] = p
  700. const oldType = p.constructor.OldPluginType
  701. if (oldType && this.plugins[oldType]) this.console.error('Plugin type mismatch')
  702. if (oldType) this.plugins[oldType] = p
  703. p.onAdded(this)
  704. this.dispatchEvent({type: 'addPlugin', target: this, plugin: p})
  705. this.setDirty(p)
  706. return p
  707. }
  708. /**
  709. * Add multiple plugins to the viewer.
  710. * @param plugins - List of plugin instances or classes
  711. */
  712. async addPlugins(plugins: (IViewerPlugin | Class<IViewerPlugin>)[]): Promise<void> {
  713. for (const p of plugins) await this.addPlugin(p)
  714. }
  715. /**
  716. * Add multiple plugins to the viewer(sync).
  717. * @param plugins - List of plugin instances or classes
  718. */
  719. addPluginsSync(plugins: (IViewerPluginSync | Class<IViewerPluginSync>)[]): void {
  720. for (const p of plugins) this.addPluginSync(p)
  721. }
  722. /**
  723. * Remove a plugin instance or a plugin class. Works similar to {@link ThreeViewer.addPlugin}
  724. * @param p
  725. * @param dispose
  726. * @returns {Promise<void>}
  727. */
  728. async removePlugin(p: IViewerPlugin<ThreeViewer, false>, dispose = true): Promise<void> {
  729. const type = p.constructor.PluginType
  730. if (!this.plugins[type]) return
  731. await p.onRemove(this)
  732. this.dispatchEvent({type: 'removePlugin', target: this, plugin: p})
  733. delete this.plugins[type]
  734. if (dispose) await p.dispose() // todo await?
  735. this.setDirty(p)
  736. }
  737. /**
  738. * Remove a plugin instance or a plugin class(sync). Works similar to {@link ThreeViewer.addPluginSync}
  739. * @param p
  740. * @param dispose
  741. */
  742. removePluginSync(p: IViewerPluginSync, dispose = true): void {
  743. const type = p.constructor.PluginType
  744. if (!this.plugins[type]) return
  745. p.onRemove(this)
  746. this.dispatchEvent({type: 'removePlugin', target: this, plugin: p})
  747. delete this.plugins[type]
  748. if (dispose) p.dispose()
  749. this.setDirty(p)
  750. }
  751. /**
  752. * Set size of the canvas and update the renderer.
  753. * If no size or width/height is passed, canvas is set to 100% of the container.
  754. *
  755. * See also {@link ThreeViewer.setRenderSize} to set the size of the render target by automatically calculating the renderScale and fitting in container.
  756. *
  757. * Note: Apps using this should ideally set `max-width: 100%` for the canvas in css.
  758. * @param size
  759. */
  760. setSize(size?: {width?: number, height?: number}) {
  761. this._canvas.style.width = size?.width ? size.width + 'px' : '100%'
  762. this._canvas.style.height = size?.height ? size.height + 'px' : '100%'
  763. // this._canvas.style.maxWidth = '100%' // this is upto the app to do.
  764. // this._canvas.style.maxHeight = '100%'
  765. this.resize()
  766. }
  767. // todo make an example for this.
  768. // todo make a constructor parameter for renderSize
  769. // todo make getRenderSize or get renderSize
  770. /**
  771. * Set the render size of the viewer to fit in the container according to the specified mode, maintaining aspect ratio.
  772. * Changes the renderScale accordingly.
  773. * Note: the canvas needs to be centered in the container to work properly, this can be done with the following css on the container:
  774. * ```css
  775. * display: flex;
  776. * justify-content: center;
  777. * align-items: center;
  778. * ```
  779. * or in js:
  780. * ```js
  781. * viewer.container.style.display = 'flex';
  782. * viewer.container.style.justifyContent = 'center';
  783. * viewer.container.style.alignItems = 'center';
  784. * ```
  785. * Modes:
  786. * 'contain': The canvas is scaled to fit within the container while maintaining its aspect ratio. The canvas will be fully visible, but there may be empty space around it.
  787. * 'cover': The canvas is scaled to fill the entire container while maintaining its aspect ratio. Part of the canvas may be clipped to fit the container.
  788. * 'fill': The canvas is stretched to completely fill the container, ignoring its aspect ratio.
  789. * 'scale-down': The canvas is scaled down to fit within the container while maintaining its aspect ratio, but it won't be scaled up if it's smaller than the container.
  790. * 'none': container size is ignored, but devicePixelRatio is used
  791. * @param size - The size to set the render to. The canvas will render to this size.
  792. * @param mode - 'contain', 'cover', 'fill', 'scale-down' or 'none'. Default is 'contain'.
  793. * @param devicePixelRatio - typically set to `window.devicePixelRatio`, or `Math.min(1.5, window.devicePixelRatio)` for performance. Use this only when size is derived from dom elements.
  794. * @param containerSize - (optional) The size of the container, if not passed, the bounding client rect of the container is used.
  795. */
  796. setRenderSize(size: {width: number, height: number},
  797. mode: 'contain' | 'cover' | 'fill' | 'scale-down' | 'none' = 'contain',
  798. devicePixelRatio = 1,
  799. containerSize?: {width: number, height: number}) {
  800. // todo what about container resize?
  801. const containerRect = containerSize || this.container.getBoundingClientRect()
  802. const containerHeight = containerRect.height
  803. const containerWidth = containerRect.width
  804. const width = size.width
  805. const height = size.height
  806. const aspect = width / height
  807. const containerAspect = containerWidth / containerHeight
  808. const dpr = devicePixelRatio
  809. let renderWidth, renderHeight
  810. switch (mode) {
  811. case 'contain':
  812. if (containerAspect > aspect) {
  813. renderWidth = containerHeight * aspect
  814. renderHeight = containerHeight
  815. } else {
  816. renderWidth = containerWidth
  817. renderHeight = containerWidth / aspect
  818. }
  819. break
  820. case 'cover':
  821. if (containerAspect > aspect) {
  822. renderWidth = containerWidth
  823. renderHeight = containerWidth / aspect
  824. } else {
  825. renderWidth = containerHeight * aspect
  826. renderHeight = containerHeight
  827. }
  828. break
  829. case 'fill':
  830. renderWidth = containerWidth
  831. renderHeight = containerHeight
  832. break
  833. case 'scale-down':
  834. if (width < containerWidth && height < containerHeight) {
  835. renderWidth = width
  836. renderHeight = height
  837. } else if (containerAspect > aspect) {
  838. renderWidth = containerHeight * aspect
  839. renderHeight = containerHeight
  840. } else {
  841. renderWidth = containerWidth
  842. renderHeight = containerWidth / aspect
  843. }
  844. break
  845. case 'none':
  846. renderWidth = width
  847. renderHeight = height
  848. break
  849. default:
  850. throw new Error(`Invalid mode: ${mode}`)
  851. }
  852. this.setSize({width: renderWidth, height: renderHeight})
  853. this.renderManager.renderScale = dpr * height / renderHeight
  854. }
  855. /**
  856. * Traverse all objects in scene model root.
  857. * @param callback
  858. */
  859. traverseSceneObjects<T extends IObject3D = IObject3D>(callback: (o: T)=>void): void {
  860. this._scene.modelRoot.traverse(callback)
  861. }
  862. /**
  863. * Add an object to the scene model root.
  864. * If an imported scene model root is passed, it will be loaded with viewer configuration, unless importConfig is false
  865. * @param imported
  866. * @param options
  867. */
  868. async addSceneObject<T extends IObject3D|Object3D|RootSceneImportResult = RootSceneImportResult>(imported: T, options?: AddObjectOptions): Promise<T> {
  869. if (imported.userData?.rootSceneModelRoot) {
  870. const obj = <RootSceneImportResult>imported
  871. this._scene.loadModelRoot(obj, options)
  872. if (obj.importedViewerConfig && options?.importConfig !== false) await this.importConfig(obj.importedViewerConfig)
  873. return this._scene.modelRoot as T
  874. }
  875. this._scene.addObject(imported, options)
  876. return imported
  877. }
  878. /**
  879. * Serialize all the plugins and their settings to save or create presets. Used in {@link toJSON}.
  880. * @param meta - The meta object.
  881. * @param filter - List of PluginType for the to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
  882. * @returns {any[]}
  883. */
  884. serializePlugins(meta: SerializationMetaType, filter?: string[]): any[] {
  885. if (filter && filter.length === 0) return []
  886. return Object.entries(this.plugins).map(p=> {
  887. if (filter && !filter.includes(p[1].constructor.PluginType)) return
  888. // if (!p[1].toJSON) this.console.log(`Plugin of type ${p[0]} is not serializable`)
  889. return p[1].serializeWithViewer !== false ? p[1].toJSON?.(meta) : undefined
  890. }).filter(p=> !!p)
  891. }
  892. /**
  893. * Deserialize all the plugins and their settings from a preset. Used in {@link fromJSON}.
  894. * @param plugins - The output of {@link serializePlugins}.
  895. * @param meta - The meta object.
  896. * @returns {this}
  897. */
  898. deserializePlugins(plugins: any[], meta?: SerializationMetaType): this {
  899. plugins.forEach(p=>{
  900. if (!p.type) {
  901. this.console.warn('Invalid plugin to import ', p)
  902. return
  903. }
  904. const plugin = this.getPlugin(p.type)
  905. if (!plugin) {
  906. // this.console.warn(`Plugin of type ${p.type} is not added, cannot deserialize`)
  907. return
  908. }
  909. plugin.fromJSON?.(p, meta)
  910. })
  911. return this
  912. }
  913. /**
  914. * Serialize a single plugin settings.
  915. */
  916. exportPluginConfig(plugin?: string|Class<IViewerPlugin>|IViewerPlugin): ISerializedConfig | Record<string, never> {
  917. if (plugin && typeof plugin === 'string' || (plugin as any).PluginType) plugin = this.getPlugin(plugin as any)
  918. if (!plugin) return {}
  919. const meta = getEmptyMeta()
  920. const data = (<IViewerPlugin>plugin).toJSON?.(meta)
  921. if (!data) return {}
  922. data.resources = metaToResources(meta)
  923. return data
  924. }
  925. /**
  926. * Deserialize and import a single plugin settings.
  927. * Can also use {@link ThreeViewer.importConfig} to import only plugin config.
  928. * @param json
  929. * @param plugin
  930. */
  931. async importPluginConfig(json: ISerializedConfig, plugin?: IViewerPlugin) {
  932. // this.console.log('importing plugin preset', json, plugin)
  933. const type = json.type
  934. plugin = plugin || this.getPlugin(type)
  935. if (!plugin) {
  936. this.console.warn(`No plugin found for type ${type} to import config`)
  937. return undefined
  938. }
  939. if (!plugin.fromJSON) {
  940. this.console.warn(`Plugin ${type} does not support importing presets`)
  941. return undefined
  942. }
  943. const resources = json.resources || {}
  944. if (json.resources) delete json.resources
  945. const meta = await this.loadConfigResources(resources)
  946. await plugin.fromJSON(json, meta)
  947. if (meta) json.resources = meta
  948. return plugin
  949. }
  950. /**
  951. * Serialize multiple plugin settings.
  952. * @param filter - List of PluginType to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
  953. */
  954. exportPluginsConfig(filter?: string[]): ISerializedViewerConfig {
  955. const meta = getEmptyMeta()
  956. const plugins = this.serializePlugins(meta, filter)
  957. convertArrayBufferToStringsInMeta(meta) // assuming not binary
  958. return {
  959. ...this._defaultConfig,
  960. plugins, resources: metaToResources(meta),
  961. }
  962. }
  963. /**
  964. * Serialize all the viewer and plugin settings.
  965. * @param binary - Indicate that the output will be converted and saved as binary data. (default: false)
  966. * @param pluginFilter - List of PluginType to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
  967. */
  968. exportConfig(binary = false, pluginFilter?: string[]) {
  969. return this.toJSON(binary, pluginFilter)
  970. }
  971. /**
  972. * Deserialize and import all the viewer and plugin settings, exported with {@link exportConfig}.
  973. */
  974. async importConfig(json: ISerializedConfig|ISerializedViewerConfig) {
  975. if (json.type !== this.type && <string>json.type !== 'ViewerApp') {
  976. if (this.getPlugin(json.type)) {
  977. return this.importPluginConfig(json)
  978. } else {
  979. this.console.error(`Unknown config type ${json.type} to import`)
  980. return undefined
  981. }
  982. }
  983. const resources = await this.loadConfigResources(json.resources || {})
  984. this.fromJSON(<ISerializedViewerConfig>json, resources)
  985. }
  986. /**
  987. * Serialize all the viewer and plugin settings and versions.
  988. * @param binary - Indicate that the output will be converted and saved as binary data. (default: true)
  989. * @param pluginFilter - List of PluginType to include. If empty, no plugins will be serialized. If undefined/not-passed, all plugins will be serialized.
  990. * @returns {any} - Serializable JSON object.
  991. */
  992. toJSON(binary = true, pluginFilter?: string[]): ISerializedViewerConfig {
  993. const meta = getEmptyMeta()
  994. const data: ISerializedViewerConfig = Object.assign({
  995. ...this._defaultConfig,
  996. plugins: this.serializePlugins(meta, pluginFilter),
  997. }, ThreeSerialization.Serialize(this, meta, true))
  998. // this.console.log(dat)
  999. if (!binary) convertArrayBufferToStringsInMeta(meta)
  1000. data.resources = metaToResources(meta)
  1001. return data
  1002. }
  1003. /**
  1004. * Deserialize all the viewer and plugin settings.
  1005. * @note use async {@link ThreeViewer.importConfig} to import a json/config exported with {@link ThreeViewer.exportConfig} or {@link ThreeViewer.toJSON}.
  1006. * @param data - The serialized JSON object retured from {@link toJSON}.
  1007. * @param meta - The meta object
  1008. * @returns {this}
  1009. */
  1010. fromJSON(data: ISerializedViewerConfig, meta?: SerializationMetaType): this|null {
  1011. const data2: Partial<ISerializedViewerConfig> = {...data} // shallow copy
  1012. // region legacy
  1013. if (data2.backgroundIntensity !== undefined && data2.scene?.backgroundIntensity === undefined) {
  1014. this.console.warn('old file format, backgroundIntensity moved to RootScene')
  1015. this._scene.backgroundIntensity = data2.backgroundIntensity
  1016. delete data2.backgroundIntensity
  1017. }
  1018. if (data2.useLegacyLights !== undefined && data2.renderManager?.useLegacyLights === undefined) {
  1019. this.console.warn('old file format, useLegacyLights moved to RenderManager')
  1020. this.renderManager.useLegacyLights = data2.useLegacyLights
  1021. delete data2.useLegacyLights
  1022. }
  1023. if (data2.background !== undefined && data2.scene?.background === undefined) {
  1024. this.console.warn('old file format, background moved to RootScene')
  1025. if (data2.background === 'envMapBackground') data2.background = 'environment'
  1026. else if (typeof data2.background === 'number')
  1027. data2.background = new Color().setHex(data2.background, LinearSRGBColorSpace)
  1028. else if (typeof data2.background === 'string')
  1029. data2.background = new Color().setStyle(data2.background, LinearSRGBColorSpace)
  1030. else if (data2.background?.isColor) data2.background = new Color(data2.background)
  1031. if (data2.background?.isColor) { // color
  1032. this._scene.backgroundColor = data2.background
  1033. this._scene.background = null
  1034. } else if (!data2.background) { // null
  1035. this._scene.backgroundColor = null
  1036. this._scene.background = null
  1037. } else { // texture or 'environment'
  1038. this._scene.backgroundColor = new Color('#ffffff')
  1039. if (!data2.scene) data2.scene = {}
  1040. data2.scene.background = data2.background
  1041. }
  1042. delete data2.background
  1043. }
  1044. // endregion
  1045. if (!meta && data2.resources && data2.resources.__isLoadedResources) {
  1046. meta = data2.resources as SerializationMetaType
  1047. delete data2.resources
  1048. }
  1049. if (!meta?.__isLoadedResources) {
  1050. this.console.error('meta in fromJSON is not available or is not loaded resources, call viewer.loadConfigResources first, or directly use viewer.importConfig')
  1051. return null
  1052. }
  1053. if (Array.isArray(data2.plugins)) {
  1054. this.deserializePlugins(data2.plugins, meta)
  1055. delete data2.plugins
  1056. }
  1057. // meta = meta || data.resources
  1058. ThreeSerialization.Deserialize(data2, this, meta, true)
  1059. // todo: handle
  1060. // __useCount set in ThreeSerialization while deserializing resources
  1061. // for (const mat of Object.values(resources.materials) as any) {
  1062. // if (!mat.__useCount) this.materialManager?.unregisterMaterial(mat) // todo: also dispose?
  1063. // else delete mat.__useCount
  1064. // }
  1065. // for (const tex of Object.values(resources.textures) as any) {
  1066. // if (!tex.__useCount) {
  1067. // // todo: dispose?
  1068. // } else {
  1069. // delete tex.__useCount
  1070. // }
  1071. // }
  1072. return this
  1073. }
  1074. loadConfigResources = async(json: Partial<SerializationMetaType>, extraResources?: Partial<SerializationResourcesType>): Promise<any> => {
  1075. // this.console.log(json)
  1076. if (json.__isLoadedResources) return json
  1077. const meta = metaFromResources(json, this)
  1078. return await MetaImporter.ImportMeta(meta, extraResources)
  1079. }
  1080. async doOnce<TRet>(event: IViewerEventTypes, func?: (...args: any[]) => TRet): Promise<TRet|undefined> {
  1081. return new Promise((resolve) => {
  1082. const listener = async(...args: any[]) => {
  1083. this.removeEventListener(event, listener)
  1084. resolve(await func?.(...args))
  1085. }
  1086. this.addEventListener(event, listener)
  1087. })
  1088. }
  1089. dispatchEvent(event: IViewerEvent) {
  1090. super.dispatchEvent(event)
  1091. super.dispatchEvent({...event, type: '*', eType: event.type})
  1092. }
  1093. /**
  1094. * Uses the {@link FileTransferPlugin} to export a Blob/File. If the plugin is not available, it will download the blob.
  1095. * {@link FileTransferPlugin} can be configured by other plugins to export the blob to a specific location like local file system, cloud storage, etc.
  1096. * @param blob - The blob or file to export/download
  1097. * @param name - name of the file, if not provided, the name of the file is used if it's a file.
  1098. */
  1099. async exportBlob(blob: Blob|File, name?: string) {
  1100. const tr = this.getPlugin<FileTransferPlugin>('FileTransferPlugin')
  1101. name = name ?? (blob as File).name ?? 'file'
  1102. if (!tr) {
  1103. downloadBlob(blob, name)
  1104. return
  1105. }
  1106. await tr.exportFile(blob, name)
  1107. }
  1108. private _setActiveCameraView(event: any = {}): void {
  1109. if (event.type === 'setView') {
  1110. if (!event.camera) {
  1111. this.console.warn('Cannot find camera', event)
  1112. return
  1113. }
  1114. const camera = this._scene.mainCamera
  1115. camera.setViewFromCamera(event.camera) // default is worldSpace
  1116. } else if (event.type === 'activateMain')
  1117. this._scene.mainCamera = event.camera || undefined // event.camera should have been upgraded when added to the scene.
  1118. }
  1119. private _resolvePluginOrClass<T extends IViewerPlugin>(plugin: T | Class<T>, ...args: ConstructorParameters<Class<T>>): T {
  1120. let p: T
  1121. if ((plugin as Class<IViewerPlugin>).prototype) {
  1122. const p1 = this.getPlugin(plugin as Class<T>)
  1123. if (p1) {
  1124. this.console.error(`Plugin of type ${p1.constructor.PluginType} already exists, no new plugin created`, p1)
  1125. return p1
  1126. }
  1127. p = new (plugin as Class<T>)(...args)
  1128. } else p = plugin as T
  1129. return p
  1130. }
  1131. private _renderEnabledChanged(): void {
  1132. this.dispatchEvent({type: this.renderEnabled ? 'renderEnabled' : 'renderDisabled'})
  1133. }
  1134. private readonly _defaultConfig: ISerializedViewerConfig = {
  1135. assetType: 'config',
  1136. type: this.type,
  1137. version: ThreeViewer.VERSION,
  1138. metadata: {
  1139. generator: 'ThreePipe',
  1140. version: 1,
  1141. },
  1142. plugins: [],
  1143. }
  1144. // todo: find a better fix for context loss and restore?
  1145. private _lastSize = new Vector2()
  1146. private _onContextRestore = (_: Event) => {
  1147. this.enabled = true
  1148. this._canvas.width = this._lastSize.width
  1149. this._canvas.height = this._lastSize.height
  1150. this.resize()
  1151. this._scene.setDirty({refreshScene: true, frameFade: false})
  1152. }
  1153. private _onContextLost = (_: Event) => {
  1154. this._lastSize.set(this._canvas.width, this._canvas.height)
  1155. this._canvas.width = 2
  1156. this._canvas.height = 2
  1157. this.resize()
  1158. this.enabled = false
  1159. }
  1160. // private _addSceneObject = (e: IEvent<any>) => {
  1161. // if (!e || !e.object) return
  1162. // 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.
  1163. // if (!config) return
  1164. // this.fromJSON(config, config.resources)
  1165. // }
  1166. public async fitToView(selected?: Object3D, distanceMultiplier = 1.5, duration?: number, ease?: Easing|EasingFunctionType) {
  1167. const camViews = this.getPlugin<CameraViewPlugin>('CameraViews')
  1168. if (!camViews) {
  1169. this.console.error('CameraViewPlugin (CameraViews) is required for fitToView to work')
  1170. return
  1171. }
  1172. await camViews?.animateToFitObject(selected, distanceMultiplier, duration, ease, {min: ((<OrbitControls3> this.scene.mainCamera.controls)?.minDistance ?? 0.5) + 0.5, max: 1000.0})
  1173. }
  1174. private _canvasTexture?: CanvasTexture&ITexture
  1175. /**
  1176. * Create and get a three.js CanvasTexture from the viewer's canvas.
  1177. */
  1178. get canvasTexture(): CanvasTexture {
  1179. if (!this._canvas) throw new Error('Canvas not found')
  1180. if (!this._canvasTexture) {
  1181. this._canvasTexture = new CanvasTexture(this._canvas)
  1182. this._canvasTexture.flipY = false
  1183. this._canvasTexture.needsUpdate = true
  1184. }
  1185. return this._canvasTexture
  1186. }
  1187. // todo: create/load texture utils
  1188. // region legacy creation functions
  1189. // /**
  1190. // * Converts a three.js Camera instance to be used in the viewer.
  1191. // * @param camera - The three.js OrthographicCamera or PerspectiveCamera instance
  1192. // * @returns {CameraController} - A wrapper around the camera with some useful methods and properties.
  1193. // */
  1194. // createCamera(camera: OrthographicCamera | PerspectiveCamera): CameraController {
  1195. // const cam: CameraController = camera.userData.iCamera ?? new CameraController(camera, {
  1196. // controlsMode: '',
  1197. // controlsEnabled: false,
  1198. // }, this._canvas)
  1199. // if (camera.userData.autoLookAtTarget === undefined) {
  1200. // cam.autoLookAtTarget = false
  1201. // camera.userData.autoLookAtTarget = false
  1202. // } else {
  1203. // cam.autoLookAtTarget = camera.userData.autoLookAtTarget
  1204. // }
  1205. // return cam
  1206. // }
  1207. // /**
  1208. // * Create a new empty object in the scene or add an existing three.js object to the scene.
  1209. // * @param object
  1210. // */
  1211. // async createObject3D(object?: Object3D): Promise<Object3DModel | undefined> {
  1212. // return this.getManager()?.addImportedSingle<Object3DModel>(object || new Object3D(), {autoScale: false, pseudoCenter: false})
  1213. // }
  1214. // /**
  1215. // * 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.
  1216. // * @param material
  1217. // */
  1218. // createPhysicalMaterial(material?: Material|MeshPhysicalMaterialParameters): MeshStandardMaterial2 | undefined {
  1219. // return this.createMaterial<MeshStandardMaterial2>('standard', material)
  1220. // }
  1221. // /**
  1222. // * 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.
  1223. // * @param template - template name registered in MaterialManager
  1224. // * @param material - three.js material object or material params to create a new material
  1225. // */
  1226. // createMaterial<T extends IMaterial<any>>(template: 'standard' | 'basic' | 'diamond' | string, material?: Material|any): T | undefined {
  1227. // if ((material as Material)?.isMaterial) {
  1228. // const f = this.getManager()?.materials?.findMaterial((material as Material).uuid)
  1229. // if (f) return f as T
  1230. // }
  1231. // return this.getManager()?.materials?.generateFromTemplate(template, material) as T
  1232. // }
  1233. // endregion
  1234. /**
  1235. * 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
  1236. * @deprecated - use {@link renderManager} instead
  1237. */
  1238. get renderer(): ViewerRenderManager {
  1239. this.console.error('renderer is deprecated, use renderManager instead')
  1240. return this.renderManager
  1241. }
  1242. /**
  1243. * @deprecated use {@link assetManager} instead.
  1244. * Gets the Asset manager, contains useful functions for managing, loading and inserting assets.
  1245. */
  1246. getManager(): AssetManager|undefined {
  1247. return this.assetManager
  1248. }
  1249. /**
  1250. * Get the Plugin by the string type.
  1251. * @deprecated - Use {@link getPlugin} instead.
  1252. * @param type
  1253. * @returns {T | undefined}
  1254. */
  1255. getPluginByType<T extends IViewerPlugin>(type: string): T | undefined {
  1256. return this.plugins[type] as T | undefined
  1257. }
  1258. }