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

ThreeViewer.ts 63KB

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