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 61KB

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