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

ThreeViewer.ts 51KB

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