| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380 |
- import {
- BaseEvent,
- CanvasTexture,
- Color,
- Event,
- EventDispatcher,
- LinearSRGBColorSpace,
- Object3D,
- Quaternion,
- Scene,
- Vector2,
- Vector3,
- } from 'three'
- import {Class, createCanvasElement, downloadBlob, onChange, serialize, ValOrArr} from 'ts-browser-helpers'
- import {TViewerScreenShader} from '../postprocessing'
- import {
- AddObjectOptions,
- IAnimationLoopEvent,
- IMaterial,
- IObject3D,
- IObjectProcessor,
- ITexture,
- PerspectiveCamera2,
- RootScene,
- TCameraControlsMode,
- } from '../core'
- import {ViewerRenderManager} from './ViewerRenderManager'
- import {
- convertArrayBufferToStringsInMeta,
- EasingFunctionType,
- getEmptyMeta,
- GLStatsJS,
- IDialogWrapper,
- jsonToBlob,
- metaFromResources,
- MetaImporter,
- metaToResources,
- SerializationMetaType,
- SerializationResourcesType,
- ThreeSerialization,
- windowDialogWrapper,
- } from '../utils'
- import {
- AssetManager,
- AssetManagerOptions,
- BlobExt,
- ExportFileOptions,
- IAsset,
- ImportAddOptions,
- ImportAssetOptions,
- ImportResult,
- RootSceneImportResult,
- } from '../assetmanager'
- import {IViewerPlugin, IViewerPluginSync} from './IViewerPlugin'
- import {uiConfig, UiObjectConfig, uiPanelContainer} from 'uiconfig.js'
- import {IRenderTarget} from '../rendering'
- import type {CanvasSnapshotPlugin, FileTransferPlugin} from '../plugins'
- import {CameraViewPlugin, ProgressivePlugin} from '../plugins'
- // noinspection ES6PreferShortImport
- import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin'
- // noinspection ES6PreferShortImport
- import {TonemapPlugin} from '../plugins/postprocessing/TonemapPlugin'
- import {VERSION} from './version'
- import {Easing} from 'popmotion'
- import {OrbitControls3} from '../three'
-
- export interface IViewerEvent extends BaseEvent, Partial<IAnimationLoopEvent> {
- type: '*'|'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'renderEnabled'|'renderDisabled'
- eType?: '*'|'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'renderEnabled'|'renderDisabled'
- [p: string]: any
- }
- export type IViewerEventTypes = IViewerEvent['type']
- export interface ISerializedConfig {
- assetType: 'config',
- type: string,
- metadata?: {
- generator: string,
- version: number,
- [key: string]: any
- },
- [key: string]: any
- }
- export interface ISerializedViewerConfig extends ISerializedConfig{
- type: 'ThreeViewer'|'ViewerApp',
- version: string,
- plugins: ISerializedConfig[],
- resources?: Partial<SerializationResourcesType> | SerializationMetaType
- renderManager?: any // todo
- scene?: any
-
- [key: string]: any
- }
-
- export type IConsoleWrapper = Partial<Console> & Pick<Console, 'log'|'warn'|'error'>
-
- /**
- * Options for the ThreeViewer creation.
- * @category Viewer
- */
- export interface ThreeViewerOptions {
- /**
- * The canvas element to use for rendering. Only one of container and canvas must be specified.
- */
- canvas?: HTMLCanvasElement,
- /**
- * The container for the canvas. A new canvas will be created in this container. Only one of container and canvas must be specified.
- */
- container?: HTMLElement,
- /**
- * The fragment shader snippet to render on screen.
- */
- screenShader?: TViewerScreenShader,
- /**
- * Use MSAA.
- */
- msaa?: boolean,
- /**
- * Use Uint8 RGBM HDR Render Pipeline.
- * Provides better performance with post-processing.
- * RenderManager Uses Half-float if set to false.
- */
- rgbm?: boolean
- /**
- * Use rendered gbuffer as depth-prepass / z-prepass. (Requires DepthBufferPlugin/GBufferPlugin)
- * todo: It will be disabled when there are any transparent/transmissive objects with render to depth buffer enabled.
- */
- zPrepass?: boolean
- /**
- * Force z-prepass even if there are transparent/transmissive objects with render to depth buffer enabled.
- */
- forceZPrepass?: boolean // todo
-
- /*
- * Render scale, 1 = full resolution, 0.5 = half resolution, 2 = double resolution.
- * Same as pixelRatio in three.js
- * Can be set to `window.devicePixelRatio` to render at device resolution in browsers.
- * An optimal value is `Math.min(2, window.devicePixelRatio)` to prevent issues on mobile. This is set when 'auto' is passed.
- */
- renderScale?: number | 'auto'
-
- debug?: boolean
-
- /**
- * Add initial plugins.
- */
- plugins?: (IViewerPluginSync | Class<IViewerPluginSync>)[]
-
- load?: {
- /**
- * Load one or more source files
- */
- src?: ValOrArr<string | IAsset | null>
-
- /**
- * Load environment map
- */
- environment?: string | IAsset | ITexture | undefined | null
-
- /**
- * Load background map
- */
- background?: string | IAsset | ITexture | undefined | null
-
- }
- onLoad?: (results: any) => void
-
-
- /**
- * TonemapPlugin is added to the viewer if this is true.
- * @default true
- */
- tonemap?: boolean
-
- camera?: {
- controlsMode?: TCameraControlsMode,
- position?: Vector3,
- target?: Vector3,
-
- }
-
- /**
- * Options for the asset manager.
- */
- assetManager?: AssetManagerOptions
-
- /**
- * Add the dropzone plugin to the viewer, allowing to drag and drop files into the viewer over the canvas/container.
- * Set to true/false to enable/disable the plugin, or pass options to configure the plugin. Assuming true if options are passed.
- * @default - false
- */
- dropzone?: boolean|DropzonePluginOptions
-
- /**
- * @deprecated use {@link msaa} instead
- */
- isAntialiased?: boolean,
- /**
- * @deprecated use {@link rgbm} instead
- */
- useRgbm?: boolean
- /**
- * @deprecated use {@link zPrepass} instead
- */
- useGBufferDepth?: boolean
- }
-
- /**
- * Three Viewer
- *
- * The ThreeViewer is the main class in the framework to manage a scene, render and add plugins to it.
- * @category Viewer
- */
- @uiPanelContainer('Viewer')
- export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes> {
- public static readonly VERSION = VERSION
- public static readonly ConfigTypeSlug = 'vjson'
- uiConfig!: UiObjectConfig
-
- static Console: IConsoleWrapper = {
- log: console.log.bind(console),
- warn: console.warn.bind(console),
- error: console.error.bind(console),
- }
- static Dialog: IDialogWrapper = windowDialogWrapper
-
- /**
- * If the viewer is enabled. Set this `false` to disable RAF loop.
- * @type {boolean}
- */
- enabled = true
- /**
- * Enable or disable all rendering, Animation loop including any frame/render events won't be fired when this is false.
- */
- @onChange(ThreeViewer.prototype._renderEnabledChanged)
- renderEnabled = true
- renderStats: GLStatsJS
- readonly assetManager: AssetManager
- @uiConfig() @serialize('renderManager')
- readonly renderManager: ViewerRenderManager
- get materialManager() {
- return this.assetManager.materials
- }
- public readonly plugins: Record<string, IViewerPlugin> = {}
- /**
- * Scene with object hierarchy used for rendering
- */
- get scene(): RootScene&Scene {
- return this._scene as RootScene&Scene
- }
- /**
- * Specifies how many frames to render in a single request animation frame. Keep to 1 for realtime rendering.
- * Note: should be max (screen refresh rate / animation frame rate) like 60Hz / 30fps
- * @type {number}
- */
- public maxFramePerLoop = 1
- readonly debug: boolean
-
- /**
- * Number of times to run composer render. If set to more than 1, preRender and postRender events will also be called multiple times.
- */
- rendersPerFrame = 1
-
- /**
- * Get the HTML Element containing the canvas
- * @returns {HTMLElement}
- */
- get container(): HTMLElement {
- // todo console.warn('container is deprecated, NOTE: subscribe to events when the canvas is moved to another container')
- if (this._canvas.parentElement !== this._container) {
- this.console.error('ThreeViewer: Canvas is not in the container, this might cause issues with some plugins.')
- }
- return this._container
- }
-
- /**
- * Get the HTML Canvas Element where the viewer is rendering
- * @returns {HTMLCanvasElement}
- */
- get canvas(): HTMLCanvasElement {
- return this._canvas
- }
-
- get console(): IConsoleWrapper {
- return ThreeViewer.Console
- }
- get dialog(): IDialogWrapper {
- return ThreeViewer.Dialog
- }
- @serialize() readonly type = 'ThreeViewer'
-
- /**
- * The ResizeObserver observing the canvas element. Add more elements to this observer to resize viewer on their size change.
- * @type {ResizeObserver | undefined}
- */
- readonly resizeObserver = window?.ResizeObserver ? new window.ResizeObserver(_ => this.resize()) : undefined
-
- private readonly _canvas: HTMLCanvasElement
- // this can be used by other plugins to add ui elements alongside the canvas
- private readonly _container: HTMLElement // todo: add a way to move the canvas to a new container... and dispatch event...
- /**
- * The Scene attached to the viewer, this cannot be changed.
- * @type {RootScene}
- */
- @uiConfig() @serialize('scene')
- private readonly _scene: RootScene
- private _needsResize = false
- private _isRenderingFrame = false
- private _objectProcessor: IObjectProcessor = {
- processObject: (object: IObject3D)=>{
- if (object.material) {
- if (Array.isArray(object.material)) this.assetManager.materials.registerMaterials(object.material)
- else this.assetManager.materials.registerMaterial(object.material)
- }
- },
- }
- private _needsReset = true // renderer needs reset
-
- // Helpers for tracking main camera change and setting dirty automatically
- private _lastCameraPosition: Vector3 = new Vector3()
- private _lastCameraQuat: Quaternion = new Quaternion()
- private _lastCameraTarget: Vector3 = new Vector3()
- private _tempVec: Vector3 = new Vector3()
- private _tempQuat: Quaternion = new Quaternion()
-
- /**
- * If any of the viewers are in debug mode, this will be true.
- * This is required for debugging/logging in some cases.
- */
- public static ViewerDebugging = false // todo use in shaderReplaceString
-
- /**
- * Create a viewer instance for using the webgi viewer SDK.
- * @param options - {@link ThreeViewerOptions}
- */
- constructor({debug = false, ...options}: ThreeViewerOptions) {
- super()
- this.debug = debug
- if (debug) ThreeViewer.ViewerDebugging = true
- this._canvas = options.canvas || createCanvasElement()
- let container = options.container
- if (container && !options.canvas) container.appendChild(this._canvas)
- if (!container) container = this._canvas.parentElement ?? undefined
- if (!container) throw new Error('No container(or canvas).')
- this._container = container
- this.setDirty = this.setDirty.bind(this)
- this._animationLoop = this._animationLoop.bind(this)
- this._setActiveCameraView = this._setActiveCameraView.bind(this)
-
- this.renderStats = new GLStatsJS(this._container)
- if (debug) this.renderStats.show()
-
- if (!(window as any).threeViewers) (window as any).threeViewers = [];
- (window as any).threeViewers.push(this)
-
- // camera
-
- const camera = new PerspectiveCamera2(options.camera?.controlsMode ?? 'orbit', this._canvas)
- camera.name = 'Default Camera'
- options.camera?.position ? camera.position.copy(options.camera.position) : camera.position.set(0, 0, 5)
- options.camera?.target ? camera.target.copy(options.camera.target) : camera.target.set(0, 0, 0)
- camera.setDirty()
- camera.userData.autoLookAtTarget = true // only for when controls are disabled / not available
-
- // Update camera controls postFrame if allowed to interact
- this.addEventListener('postFrame', () => { // todo: move inside RootScene.
- const cam = this._scene.mainCamera
- if (cam && cam.canUserInteract) {
- const d = this.getPlugin<ProgressivePlugin>('ProgressivePlugin')?.postFrameConvergedRecordingDelta()
- // if (d && d > 0) delta = d
- if (d !== undefined && d === 0) return // not converged yet.
- // if d < 0 or undefined: not recording, do nothing
-
- cam.controls?.update()
- }
- })
-
- // if camera position or target changed in last frame, call setDirty on camera
- this.addEventListener('preFrame', () => { // todo: move inside RootScene.
- const cam = this._scene.mainCamera
- if (
- cam.getWorldPosition(this._tempVec).sub(this._lastCameraPosition).lengthSq() // position is in local space
- + this._tempVec.subVectors(cam.target, this._lastCameraTarget).lengthSq() // target is in world space
- + cam.getWorldQuaternion(this._tempQuat).angleTo(this._lastCameraQuat)
- > 0.000001) cam.setDirty()
- })
-
- // scene
-
- this._scene = new RootScene(camera, this._objectProcessor)
- this._scene.setBackgroundColor('#ffffff')
- // this._scene.addEventListener('addSceneObject', this._addSceneObject)
- this._scene.addEventListener('setView', this._setActiveCameraView)
- this._scene.addEventListener('activateMain', this._setActiveCameraView)
- this._scene.addEventListener('materialUpdate', (e) => this.setDirty(this._scene, e))
- this._scene.addEventListener('materialChanged', (e) => this.setDirty(this._scene, e))
- this._scene.addEventListener('objectUpdate', (e) => this.setDirty(this._scene, e))
- this._scene.addEventListener('textureUpdate', (e) => this.setDirty(this._scene, e))
- this._scene.addEventListener('sceneUpdate', (e) => {
- this.setDirty(this._scene, e)
- if (e.geometryChanged === false) return
- this.renderManager.resetShadows()
- })
- this._scene.addEventListener('mainCameraUpdate', () => {
- this._scene.mainCamera.getWorldPosition(this._lastCameraPosition)
- this._lastCameraTarget.copy(this._scene.mainCamera.target)
- this._scene.mainCamera.getWorldQuaternion(this._lastCameraQuat)
- })
-
-
- // render manager
-
- if (options.isAntialiased !== undefined || options.useRgbm !== undefined || options.useGBufferDepth !== undefined) {
- this.console.warn('isAntialiased, useRgbm and useGBufferDepth are deprecated, use msaa, rgbm and zPrepass instead.')
- }
- this.renderManager = new ViewerRenderManager({
- canvas: this._canvas,
- msaa: options.msaa ?? options.isAntialiased ?? false,
- rgbm: options.rgbm ?? options.useRgbm ?? false,
- zPrepass: options.zPrepass ?? options.useGBufferDepth ?? false,
- depthBuffer: !(options.zPrepass ?? options.useGBufferDepth ?? false),
- screenShader: options.screenShader,
- renderScale: typeof options.renderScale === 'string' ? options.renderScale === 'auto' ?
- Math.min(2, window.devicePixelRatio) : parseFloat(options.renderScale) :
- options.renderScale,
- })
- this.renderManager.addEventListener('animationLoop', this._animationLoop as any)
- this.renderManager.addEventListener('resize', ()=> this._scene.mainCamera.refreshAspect())
- this.renderManager.addEventListener('update', (e) => {
- if (e.change === 'registerPass' && e.pass?.materialExtension)
- this.assetManager.materials.registerMaterialExtension(e.pass.materialExtension)
- else if (e.change === 'unregisterPass' && e.pass?.materialExtension)
- this.assetManager.materials.unregisterMaterialExtension(e.pass.materialExtension)
- this.setDirty(this.renderManager, e)
- })
-
- this.assetManager = new AssetManager(this, options.assetManager)
-
- if (this.resizeObserver) this.resizeObserver.observe(this._canvas)
- // sometimes resize observer is late, so extra check
- window && window.addEventListener('resize', this.resize)
-
- this._canvas.addEventListener('webglcontextrestored', this._onContextRestore, false)
- this._canvas.addEventListener('webglcontextlost', this._onContextLost, false)
-
- if (options.dropzone) {
- this.addPluginSync(new DropzonePlugin(typeof options.dropzone === 'object' ? options.dropzone : undefined))
- }
- if (options.tonemap !== false) {
- this.addPluginSync(new TonemapPlugin())
- }
- for (const p of options.plugins ?? []) this.addPluginSync(p)
-
- this.console.log('ThreePipe Viewer instance initialized, version: ', ThreeViewer.VERSION)
-
- if (options.load) {
- const sources = [options.load.src].flat().filter(s=> s)
- const promises: Promise<any>[] = sources.map(async s=> s && this.load(s))
- if (options.load.environment) promises.push(this.setEnvironmentMap(options.load.environment))
- if (options.load.background) promises.push(this.setBackgroundMap(options.load.background))
- Promise.all(promises).then(options.onLoad)
- }
- }
-
- /**
- * Add an object/model/material/viewer-config/plugin-preset/... to the viewer scene from url or an {@link IAsset} object.
- * Same as {@link AssetManager.addAssetSingle}
- * @param obj
- * @param options
- */
- async load<T extends ImportResult = ImportResult>(obj: string | IAsset | File | null, options?: ImportAddOptions) {
- if (!obj) return
- return await this.assetManager.addAssetSingle<T>(obj, options)
- }
-
- /**
- * Imports an object/model/material/texture/viewer-config/plugin-preset/... to the viewer scene from url or an {@link IAsset} object.
- * Same as {@link AssetImporter.importSingle}
- * @param obj
- * @param options
- */
- async import<T extends ImportResult = ImportResult>(obj: string | IAsset | null, options?: ImportAddOptions) {
- if (!obj) return
- return await this.assetManager.importer.importSingle<T>(obj, options)
- }
-
- /**
- * Set the environment map of the scene from url or an {@link IAsset} object.
- * @param map
- * @param setBackground - Set the background image of the scene from the same map.
- * @param options - Options for importing the asset. See {@link ImportAssetOptions}
- */
- async setEnvironmentMap(map: string | IAsset | null | ITexture | undefined, {setBackground = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> {
- this._scene.environment = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null
- if (setBackground) return this.setBackgroundMap(this._scene.environment)
- return this._scene.environment
- }
-
- /**
- * Set the background image of the scene from url or an {@link IAsset} object.
- * @param map
- * @param setEnvironment - Set the environment map of the scene from the same map.
- * @param options - Options for importing the asset. See {@link ImportAssetOptions}
- */
- async setBackgroundMap(map: string | IAsset | null | ITexture | undefined, {setEnvironment = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> {
- this._scene.background = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null
- if (setEnvironment) return this.setEnvironmentMap(this._scene.background)
- return this._scene.background
- }
-
- /**
- * Exports an object/mesh/material/texture/render-target/plugin-preset/viewer to a blob.
- * If no object is given, a glb is exported with the current viewer state.
- * @param obj
- * @param options
- */
- async export(obj?: IObject3D|IMaterial|ITexture|IRenderTarget|IViewerPlugin|(typeof this), options?: ExportFileOptions) {
- if (!obj) obj = this._scene // this will export the glb with the scene and viewer config
- if ((<typeof this>obj).type === this.type) return jsonToBlob((<typeof this>obj).exportConfig())
- if ((<IViewerPlugin>obj).constructor?.PluginType) return jsonToBlob(this.exportPluginConfig(<IViewerPlugin>obj))
- return await this.assetManager.exporter.exportObject(<IObject3D|IMaterial|ITexture|IRenderTarget>obj, options)
- }
-
- /**
- * Export the scene to a file (default: glb with viewer config) and return a blob
- * @param options
- */
- async exportScene(options?: ExportFileOptions): Promise<BlobExt | undefined> {
- return this.assetManager.exporter.exportObject(this._scene.modelRoot, options)
- }
-
- async getScreenshotBlob({mimeType = 'image/jpeg', quality = 90} = {}): Promise<Blob | null | undefined> {
- const plugin = this.getPlugin<CanvasSnapshotPlugin>('CanvasSnapshotPlugin')
- if (plugin) {
- return plugin.getFile('snapshot.' + mimeType.split('/')[1], {mimeType, quality, waitForProgressive: true})
- }
- const blobPromise = async()=> new Promise<Blob|null>((resolve) => {
- this._canvas.toBlob((blob) => {
- resolve(blob)
- }, mimeType, quality)
- })
- if (!this.renderEnabled) return blobPromise()
- return await this.doOnce('postFrame', async() => {
- this.renderEnabled = false
- const blob = await blobPromise()
- this.renderEnabled = true
- return blob
- })
- }
-
- async getScreenshotDataUrl({mimeType = 'image/jpeg', quality = 0.9} = {}): Promise<string | null | undefined> {
- if (!this.renderEnabled) return this._canvas.toDataURL(mimeType, quality)
- return await this.doOnce('postFrame', () => this._canvas.toDataURL(mimeType, quality))
- }
-
- /**
- * Disposes the viewer and frees up all resource and events. Do not use the viewer after calling dispose.
- * @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()`
- * This function is not fully implemented yet. There might be some memory leaks.
- * @todo - return promise?
- */
- public dispose(): void {
- // todo: dispose stuff from constructor etc
- for (const plugin of [...Object.values(this.plugins)]) {
- this.removePlugin(plugin, true)
- }
-
- this._scene.dispose()
- this.renderManager.dispose()
-
- this._canvas.removeEventListener('webglcontextrestored', this._onContextRestore, false)
- this._canvas.removeEventListener('webglcontextlost', this._onContextLost, false)
-
- ;(window as any).threeViewers?.splice((window as any).threeViewers.indexOf(this), 1)
-
- if (this.resizeObserver) this.resizeObserver.unobserve(this._canvas)
- else window.removeEventListener('resize', this.resize)
-
- this.dispatchEvent({type: 'dispose'})
- }
-
- /**
- * 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.
- */
- resize = () => {
- this._needsResize = true
- this.setDirty()
- }
-
- /**
- * Set the viewer to dirty and trigger render of the next frame.
- * @param source - The source of the dirty event. like plugin or 3d object
- * @param event - The event that triggered the dirty event.
- */
- setDirty(source?: any, event?: Event): void {
- this._needsReset = true
- source = source ?? this
- this.dispatchEvent({...event ?? {}, type: 'update', source})
- }
-
- protected _animationLoop(event: IAnimationLoopEvent): void {
- if (!this.enabled || !this.renderEnabled) return
- if (this._isRenderingFrame) {
- this.console.warn('animation loop: frame skip') // not possible actually, since this is not async
- return
- }
- this._isRenderingFrame = true
-
- this.renderStats.begin()
-
- for (let i = 0; i < this.maxFramePerLoop; i++) {
-
- if (this._needsReset) {
- this.renderManager.reset()
- this._needsReset = false
- }
-
- if (this._needsResize) {
- const size = [this._canvas.clientWidth, this._canvas.clientHeight]
- if (event.xrFrame) { // todo: find a better way to resize for XR.
- const cam = this.renderManager.webglRenderer.xr.getCamera()?.cameras[0]?.viewport
- if (cam) {
- if (cam.x !== 0 || cam.y !== 0) {
- this.console.warn('x and y must be 0?')
- }
- size[0] = cam.width
- size[1] = cam.height
- this.console.log('resize for xr', size)
- } else {
- this._needsResize = false
- }
- }
- if (this._needsResize) {
- this.renderManager.setSize(...size)
- this._needsResize = false
- }
- }
-
- this.dispatchEvent({...event, type: 'preFrame', target: this}) // event will have time, deltaTime and xrFrame
-
- const dirtyPlugins = Object.values(this.plugins).filter(value => value.dirty)
- if (dirtyPlugins.length > 0) {
- // console.log('dirty plugins', dirtyPlugins)
- this.setDirty(dirtyPlugins)
- }
-
- if (this._needsReset) {
- this.renderManager.reset()
- this._needsReset = false
- }
-
- // Check if the renderManger is dirty, which happens when it's reset above or if any pass in the composer is dirty
- const needsRender = this.renderManager.needsRender
- if (needsRender) {
- for (let j = 0; j < this.rendersPerFrame; j++) {
- this.dispatchEvent({type: 'preRender', target: this})
-
- try {
- const cam = this._scene.mainCamera
- this._scene.renderCamera = cam
- if (cam.visible) this.renderManager.render(this._scene, this.renderManager.defaultRenderToScreen)
- } catch (e) {
- this.console.error(e)
- if (this.debug) throw e
- // this.enabled = false
- }
-
- this.dispatchEvent({type: 'postRender', target: this})
- }
-
- }
-
- this.dispatchEvent({type: 'postFrame', target: this})
- this.renderManager.onPostFrame()
-
- if (!needsRender) // break if no frame rendered
- break
-
- }
-
- this.renderStats.end()
-
- this._isRenderingFrame = false
-
- }
-
- /**
- * Get the Plugin by a constructor type or by the string type.
- * Use string type if the plugin is not a dependency and you don't want to bundle the plugin.
- * @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
- * @returns {T | undefined} - The plugin of the specified type.
- */
- getPlugin<T extends IViewerPlugin>(type: Class<T>|string): T | undefined {
- return this.plugins[typeof type === 'string' ? type : (type as any).PluginType] as T | undefined
- }
-
- /**
- * Get the Plugin by a constructor type or add a new plugin of the specified type if it doesn't exist.
- * @param type
- * @param args - arguments for the constructor of the plugin, used when a new plugin is created.
- */
- async getOrAddPlugin<T extends IViewerPlugin>(type: Class<T>, ...args: ConstructorParameters<Class<T>>): Promise<T> {
- const plugin = this.getPlugin(type)
- if (plugin) return plugin
- return this.addPlugin(type, ...args)
- }
-
- /**
- * 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).
- * @param type
- * @param args - arguments for the constructor of the plugin, used when a new plugin is created.
- */
- getOrAddPluginSync<T extends IViewerPluginSync>(type: Class<T>, ...args: ConstructorParameters<Class<T>>): T {
- const plugin = this.getPlugin(type)
- if (plugin) return plugin
- return this.addPluginSync(type, ...args)
- }
-
- /**
- * Add a plugin to the viewer.
- * @param plugin - The instance of the plugin to add or the class of the plugin to add.
- * @param args - Arguments for the constructor of the plugin, in case a class is passed.
- * @returns {Promise<T>} - The plugin added.
- */
- async addPlugin<T extends IViewerPlugin>(plugin: T | Class<T>, ...args: ConstructorParameters<Class<T>>): Promise<T> {
- const p = this._resolvePluginOrClass(plugin, ...args)
- const type = p.constructor.PluginType
- if (!p.constructor.PluginType) {
- this.console.error('PluginType is not defined for', p)
- return p
- }
-
- for (const d of p.dependencies || []) {
- await this.getOrAddPlugin(d)
- }
-
- if (this.plugins[type]) {
- 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)
- await this.removePlugin(this.plugins[type])
- }
- this.plugins[type] = p
- await p.onAdded(this)
- this.dispatchEvent({type: 'addPlugin', target: this, plugin: p})
- this.setDirty(p)
- return p
- }
-
- /**
- * Add a plugin to the viewer(sync).
- * @param plugin
- * @param args
- */
- addPluginSync<T extends IViewerPluginSync>(plugin: T|Class<T>, ...args: ConstructorParameters<Class<T>>): T {
- const p = this._resolvePluginOrClass(plugin, ...args)
- const type = p.constructor.PluginType
- if (!p.constructor.PluginType) {
- this.console.error('PluginType is not defined for', p)
- return p
- }
- for (const d of p.dependencies || []) {
- this.getOrAddPluginSync(d)
- }
-
- if (this.plugins[type]) {
- 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)
- this.removePluginSync(this.plugins[type])
- }
- this.plugins[type] = p
- p.onAdded(this)
- this.dispatchEvent({type: 'addPlugin', target: this, plugin: p})
- this.setDirty(p)
- return p
- }
-
- /**
- * Add multiple plugins to the viewer.
- * @param plugins - List of plugin instances or classes
- */
- async addPlugins(plugins: (IViewerPlugin | Class<IViewerPlugin>)[]): Promise<void> {
- for (const p of plugins) await this.addPlugin(p)
- }
-
- /**
- * Add multiple plugins to the viewer(sync).
- * @param plugins - List of plugin instances or classes
- */
- addPluginsSync(plugins: (IViewerPluginSync | Class<IViewerPluginSync>)[]): void {
- for (const p of plugins) this.addPluginSync(p)
- }
-
- /**
- * Remove a plugin instance or a plugin class. Works similar to {@link ThreeViewer.addPlugin}
- * @param p
- * @param dispose
- * @returns {Promise<void>}
- */
- async removePlugin(p: IViewerPlugin<ThreeViewer, false>, dispose = true): Promise<void> {
- const type = p.constructor.PluginType
- if (!this.plugins[type]) return
- await p.onRemove(this)
- delete this.plugins[type]
- if (dispose) await p.dispose() // todo await?
- this.setDirty(p)
- }
-
- /**
- * Remove a plugin instance or a plugin class(sync). Works similar to {@link ThreeViewer.addPluginSync}
- * @param p
- * @param dispose
- */
- removePluginSync(p: IViewerPluginSync, dispose = true): void {
- const type = p.constructor.PluginType
- if (!this.plugins[type]) return
- p.onRemove(this)
- delete this.plugins[type]
- if (dispose) p.dispose()
- this.setDirty(p)
- }
-
- /**
- * Set size of the canvas and update the renderer.
- * If no size or width/height is passed, canvas is set to 100% of the container.
- *
- * See also {@link ThreeViewer.setRenderSize} to set the size of the render target by automatically calculating the renderScale and fitting in container.
- *
- * Note: Apps using this should ideally set `max-width: 100%` for the canvas in css.
- * @param size
- */
- setSize(size?: {width?: number, height?: number}) {
- this._canvas.style.width = size?.width ? size.width + 'px' : '100%'
- this._canvas.style.height = size?.height ? size.height + 'px' : '100%'
- // this._canvas.style.maxWidth = '100%' // this is upto the app to do.
- // this._canvas.style.maxHeight = '100%'
- this.resize()
- }
-
- // todo make an example for this.
- // todo make a constructor parameter for renderSize
- // todo make getRenderSize or get renderSize
- /**
- * Set the render size of the viewer to fit in the container according to the specified mode, maintaining aspect ratio.
- * Changes the renderScale accordingly.
- * Note: the canvas needs to be centered in the container to work properly, this can be done with the following css on the container:
- * ```css
- * display: flex;
- * justify-content: center;
- * align-items: center;
- * ```
- * or in js:
- * ```js
- * viewer.container.style.display = 'flex';
- * viewer.container.style.justifyContent = 'center';
- * viewer.container.style.alignItems = 'center';
- * ```
- * Modes:
- * '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.
- * '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.
- * 'fill': The canvas is stretched to completely fill the container, ignoring its aspect ratio.
- * '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.
- * 'none': container size is ignored, but devicePixelRatio is used
- * @param size - The size to set the render to. The canvas will render to this size.
- * @param mode - 'contain', 'cover', 'fill', 'scale-down' or 'none'. Default is 'contain'.
- * @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.
- * @param containerSize - (optional) The size of the container, if not passed, the bounding client rect of the container is used.
- */
- setRenderSize(size: {width: number, height: number},
- mode: 'contain' | 'cover' | 'fill' | 'scale-down' | 'none' = 'contain',
- devicePixelRatio = 1,
- containerSize?: {width: number, height: number}) {
- // todo what about container resize?
- const containerRect = containerSize || this.container.getBoundingClientRect()
- const containerHeight = containerRect.height
- const containerWidth = containerRect.width
- const width = size.width
- const height = size.height
- const aspect = width / height
- const containerAspect = containerWidth / containerHeight
- const dpr = devicePixelRatio
-
- let renderWidth, renderHeight
-
- switch (mode) {
- case 'contain':
- if (containerAspect > aspect) {
- renderWidth = containerHeight * aspect
- renderHeight = containerHeight
- } else {
- renderWidth = containerWidth
- renderHeight = containerWidth / aspect
- }
- break
- case 'cover':
- if (containerAspect > aspect) {
- renderWidth = containerWidth
- renderHeight = containerWidth / aspect
- } else {
- renderWidth = containerHeight * aspect
- renderHeight = containerHeight
- }
- break
- case 'fill':
- renderWidth = containerWidth
- renderHeight = containerHeight
- break
- case 'scale-down':
- if (width < containerWidth && height < containerHeight) {
- renderWidth = width
- renderHeight = height
- } else if (containerAspect > aspect) {
- renderWidth = containerHeight * aspect
- renderHeight = containerHeight
- } else {
- renderWidth = containerWidth
- renderHeight = containerWidth / aspect
- }
- break
- case 'none':
- renderWidth = width
- renderHeight = height
- break
- default:
- throw new Error(`Invalid mode: ${mode}`)
- }
-
- this.setSize({width: renderWidth, height: renderHeight})
- this.renderManager.renderScale = dpr * height / renderHeight
- }
-
- /**
- * Traverse all objects in scene model root.
- * @param callback
- */
- traverseSceneObjects<T extends IObject3D = IObject3D>(callback: (o: T)=>void): void {
- this._scene.modelRoot.traverse(callback)
- }
-
- /**
- * Add an object to the scene model root.
- * If an imported scene model root is passed, it will be loaded with viewer configuration, unless importConfig is false
- * @param imported
- * @param options
- */
- async addSceneObject<T extends IObject3D|Object3D|RootSceneImportResult = RootSceneImportResult>(imported: T, options?: AddObjectOptions): Promise<T> {
- if (imported.userData?.rootSceneModelRoot) {
- const obj = <RootSceneImportResult>imported
- if (obj.importedViewerConfig && options?.importConfig !== false) await this.importConfig(obj.importedViewerConfig)
- this._scene.loadModelRoot(obj, options)
- return this._scene.modelRoot as T
- }
- this._scene.addObject(imported, options)
- return imported
- }
-
- /**
- * Serialize all the plugins and their settings to save or create presets. Used in {@link toJSON}.
- * @param meta - The meta object.
- * @param filter - List of PluginType for the to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
- * @returns {any[]}
- */
- serializePlugins(meta: SerializationMetaType, filter?: string[]): any[] {
- if (filter && filter.length === 0) return []
- return Object.entries(this.plugins).map(p=> {
- if (filter && !filter.includes(p[1].constructor.PluginType)) return
- // if (!p[1].toJSON) this.console.log(`Plugin of type ${p[0]} is not serializable`)
- return p[1].serializeWithViewer !== false ? p[1].toJSON?.(meta) : undefined
- }).filter(p=> !!p)
- }
-
- /**
- * Deserialize all the plugins and their settings from a preset. Used in {@link fromJSON}.
- * @param plugins - The output of {@link serializePlugins}.
- * @param meta - The meta object.
- * @returns {this}
- */
- deserializePlugins(plugins: any[], meta?: SerializationMetaType): this {
- plugins.forEach(p=>{
- if (!p.type) {
- this.console.warn('Invalid plugin to import ', p)
- return
- }
- const plugin = this.getPlugin(p.type)
- if (!plugin) {
- // this.console.warn(`Plugin of type ${p.type} is not added, cannot deserialize`)
- return
- }
- plugin.fromJSON?.(p, meta)
- })
- return this
- }
-
- /**
- * Serialize a single plugin settings.
- */
- exportPluginConfig(plugin?: string|Class<IViewerPlugin>|IViewerPlugin): ISerializedConfig | Record<string, never> {
- if (plugin && typeof plugin === 'string' || (plugin as any).PluginType) plugin = this.getPlugin(plugin as any)
- if (!plugin) return {}
- const meta = getEmptyMeta()
- const data = (<IViewerPlugin>plugin).toJSON?.(meta)
- if (!data) return {}
- data.resources = metaToResources(meta)
- return data
- }
-
- /**
- * Deserialize and import a single plugin settings.
- * Can also use {@link ThreeViewer.importConfig} to import only plugin config.
- * @param json
- * @param plugin
- */
- async importPluginConfig(json: ISerializedConfig, plugin?: IViewerPlugin) {
- // this.console.log('importing plugin preset', json, plugin)
- const type = json.type
- plugin = plugin || this.getPlugin(type)
- if (!plugin) {
- this.console.warn(`No plugin found for type ${type} to import config`)
- return undefined
- }
- if (!plugin.fromJSON) {
- this.console.warn(`Plugin ${type} does not support importing presets`)
- return undefined
- }
- const resources = json.resources || {}
- if (json.resources) delete json.resources
- const meta = await this.loadConfigResources(resources)
- await plugin.fromJSON(json, meta)
- if (meta) json.resources = meta
- return plugin
- }
-
- /**
- * Serialize multiple plugin settings.
- * @param filter - List of PluginType to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
- */
- exportPluginsConfig(filter?: string[]): ISerializedViewerConfig {
- const meta = getEmptyMeta()
- const plugins = this.serializePlugins(meta, filter)
- convertArrayBufferToStringsInMeta(meta) // assuming not binary
- return {
- ...this._defaultConfig,
- plugins, resources: metaToResources(meta),
- }
- }
-
-
- /**
- * Serialize all the viewer and plugin settings.
- * @param binary - Indicate that the output will be converted and saved as binary data. (default: false)
- * @param pluginFilter - List of PluginType to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
- */
- exportConfig(binary = false, pluginFilter?: string[]) {
- return this.toJSON(binary, pluginFilter)
- }
-
- /**
- * Deserialize and import all the viewer and plugin settings, exported with {@link exportConfig}.
- */
- async importConfig(json: ISerializedConfig|ISerializedViewerConfig) {
- if (json.type !== this.type && <string>json.type !== 'ViewerApp') {
- if (this.getPlugin(json.type)) {
- return this.importPluginConfig(json)
- } else {
- this.console.error(`Unknown config type ${json.type} to import`)
- return undefined
- }
- }
- const resources = await this.loadConfigResources(json.resources || {})
- this.fromJSON(<ISerializedViewerConfig>json, resources)
- }
-
- /**
- * Serialize all the viewer and plugin settings and versions.
- * @param binary - Indicate that the output will be converted and saved as binary data. (default: true)
- * @param pluginFilter - List of PluginType to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
- * @returns {any} - Serializable JSON object.
- */
- toJSON(binary = true, pluginFilter?: string[]): ISerializedViewerConfig {
- const meta = getEmptyMeta()
- const data: ISerializedViewerConfig = Object.assign({
- ...this._defaultConfig,
- plugins: this.serializePlugins(meta, pluginFilter),
- }, ThreeSerialization.Serialize(this, meta, true))
- // this.console.log(dat)
-
- if (!binary) convertArrayBufferToStringsInMeta(meta)
-
- data.resources = metaToResources(meta)
-
- return data
- }
-
- /**
- * Deserialize all the viewer and plugin settings.
- * @note use async {@link ThreeViewer.importConfig} to import a json/config exported with {@link ThreeViewer.exportConfig} or {@link ThreeViewer.toJSON}.
- * @param data - The serialized JSON object retured from {@link toJSON}.
- * @param meta - The meta object
- * @returns {this}
- */
- fromJSON(data: ISerializedViewerConfig, meta?: SerializationMetaType): this|null {
- const data2: Partial<ISerializedViewerConfig> = {...data} // shallow copy
-
- // region legacy
- if (data2.backgroundIntensity !== undefined && data2.scene?.backgroundIntensity === undefined) {
- this.console.warn('old file format, backgroundIntensity moved to RootScene')
- this._scene.backgroundIntensity = data2.backgroundIntensity
- delete data2.backgroundIntensity
- }
- if (data2.useLegacyLights !== undefined && data2.renderManager?.useLegacyLights === undefined) {
- this.console.warn('old file format, useLegacyLights moved to RenderManager')
- this.renderManager.useLegacyLights = data2.useLegacyLights
- delete data2.useLegacyLights
- }
- if (data2.background !== undefined && data2.scene?.background === undefined) {
- this.console.warn('old file format, background moved to RootScene')
- if (data2.background === 'envMapBackground') data2.background = 'environment'
- else if (typeof data2.background === 'number')
- data2.background = new Color().setHex(data2.background, LinearSRGBColorSpace)
- else if (typeof data2.background === 'string')
- data2.background = new Color().setStyle(data2.background, LinearSRGBColorSpace)
- else if (data2.background?.isColor) data2.background = new Color(data2.background)
-
- if (data2.background?.isColor) { // color
- this._scene.backgroundColor = data2.background
- this._scene.background = null
- } else if (!data2.background) { // null
- this._scene.backgroundColor = null
- this._scene.background = null
- } else { // texture or 'environment'
- this._scene.backgroundColor = new Color('#ffffff')
- if (!data2.scene) data2.scene = {}
- data2.scene.background = data2.background
- }
- delete data2.background
- }
-
- // endregion
-
- if (!meta && data2.resources && data2.resources.__isLoadedResources) {
- meta = data2.resources as SerializationMetaType
- delete data2.resources
- }
-
- if (!meta?.__isLoadedResources) {
- this.console.error('meta in fromJSON is not available or is not loaded resources, call viewer.loadConfigResources first, or directly use viewer.importConfig')
- return null
- }
-
- if (Array.isArray(data2.plugins)) {
- this.deserializePlugins(data2.plugins, meta)
- delete data2.plugins
- }
-
- // meta = meta || data.resources
- ThreeSerialization.Deserialize(data2, this, meta, true)
-
-
- // todo: handle
- // __useCount set in ThreeSerialization while deserializing resources
- // for (const mat of Object.values(resources.materials) as any) {
- // if (!mat.__useCount) this.materialManager?.unregisterMaterial(mat) // todo: also dispose?
- // else delete mat.__useCount
- // }
- // for (const tex of Object.values(resources.textures) as any) {
- // if (!tex.__useCount) {
- // // todo: dispose?
- // } else {
- // delete tex.__useCount
- // }
- // }
-
-
- return this
- }
-
- loadConfigResources = async(json: Partial<SerializationMetaType>, extraResources?: Partial<SerializationResourcesType>): Promise<any> => {
- // this.console.log(json)
- if (json.__isLoadedResources) return json
- const meta = metaFromResources(json, this)
- return await MetaImporter.ImportMeta(meta, extraResources)
- }
-
- async doOnce<TRet>(event: IViewerEventTypes, func?: (...args: any[]) => TRet): Promise<TRet|undefined> {
- return new Promise((resolve) => {
- const listener = async(...args: any[]) => {
- this.removeEventListener(event, listener)
- resolve(await func?.(...args))
- }
- this.addEventListener(event, listener)
- })
- }
-
- dispatchEvent(event: IViewerEvent) {
- super.dispatchEvent(event)
- super.dispatchEvent({...event, type: '*', eType: event.type})
- }
-
- /**
- * Uses the {@link FileTransferPlugin} to export a blob. If the plugin is not available, it will download the blob.
- * FileTransferPlugin can be configured by other plugins to export the blob to a specific location like local file system, cloud storage, etc.
- * @param blob - The blob or file to export/download
- * @param name
- */
- async exportBlob(blob: Blob|File, name?: string) {
- const tr = this.getPlugin<FileTransferPlugin>('FileTransferPlugin')
- name = name ?? (blob as File).name ?? 'file'
- if (!tr) {
- downloadBlob(blob, name)
- return
- }
- await tr.exportFile(blob, name)
- }
-
- private _setActiveCameraView(event: any = {}): void {
- if (event.type === 'setView') {
- if (!event.camera) {
- this.console.warn('Cannot find camera', event)
- return
- }
- const camera = this._scene.mainCamera
- camera.setViewFromCamera(event.camera) // default is worldSpace
- } else if (event.type === 'activateMain')
- this._scene.mainCamera = event.camera || undefined // event.camera should have been upgraded when added to the scene.
- }
-
- private _resolvePluginOrClass<T extends IViewerPlugin>(plugin: T | Class<T>, ...args: ConstructorParameters<Class<T>>): T {
- let p: T
- if ((plugin as Class<IViewerPlugin>).prototype) {
- const p1 = this.getPlugin(plugin as Class<T>)
- if (p1) {
- this.console.error(`Plugin of type ${p1.constructor.PluginType} already exists, no new plugin created`, p1)
- return p1
- }
- p = new (plugin as Class<T>)(...args)
- } else p = plugin as T
- return p
- }
-
- private _renderEnabledChanged(): void {
- this.dispatchEvent({type: this.renderEnabled ? 'renderEnabled' : 'renderDisabled'})
- }
-
- private readonly _defaultConfig: ISerializedViewerConfig = {
- assetType: 'config',
- type: this.type,
- version: ThreeViewer.VERSION,
- metadata: {
- generator: 'ThreePipe',
- version: 1,
- },
- plugins: [],
- }
-
- // todo: find a better fix for context loss and restore?
- private _lastSize = new Vector2()
- private _onContextRestore = (_: Event) => {
- this.enabled = true
- this._canvas.width = this._lastSize.width
- this._canvas.height = this._lastSize.height
- this.resize()
- this._scene.setDirty({refreshScene: true, frameFade: false})
- }
- private _onContextLost = (_: Event) => {
- this._lastSize.set(this._canvas.width, this._canvas.height)
- this._canvas.width = 2
- this._canvas.height = 2
- this.resize()
- this.enabled = false
- }
-
- // private _addSceneObject = (e: IEvent<any>) => {
- // if (!e || !e.object) return
- // 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.
- // if (!config) return
- // this.fromJSON(config, config.resources)
- // }
-
- public async fitToView(selected?: Object3D, distanceMultiplier = 1.5, duration?: number, ease?: Easing|EasingFunctionType) {
- const camViews = this.getPlugin<CameraViewPlugin>('CameraViews')
- if (!camViews) {
- this.console.error('CameraViewPlugin (CameraViews) is required for fitToView to work')
- return
- }
- await camViews?.animateToFitObject(selected, distanceMultiplier, duration, ease, {min: ((<OrbitControls3> this.scene.mainCamera.controls)?.minDistance ?? 0.5) + 0.5, max: 1000.0})
- }
-
- private _canvasTexture?: CanvasTexture&ITexture
-
- /**
- * Create and get a three.js CanvasTexture from the viewer's canvas.
- */
- get canvasTexture(): CanvasTexture {
- if (!this._canvas) throw new Error('Canvas not found')
- if (!this._canvasTexture) {
- this._canvasTexture = new CanvasTexture(this._canvas)
- this._canvasTexture.flipY = false
- this._canvasTexture.needsUpdate = true
- }
- return this._canvasTexture
- }
-
- // todo: create/load texture utils
-
- // region legacy creation functions
-
- // /**
- // * Converts a three.js Camera instance to be used in the viewer.
- // * @param camera - The three.js OrthographicCamera or PerspectiveCamera instance
- // * @returns {CameraController} - A wrapper around the camera with some useful methods and properties.
- // */
- // createCamera(camera: OrthographicCamera | PerspectiveCamera): CameraController {
- // const cam: CameraController = camera.userData.iCamera ?? new CameraController(camera, {
- // controlsMode: '',
- // controlsEnabled: false,
- // }, this._canvas)
- // if (camera.userData.autoLookAtTarget === undefined) {
- // cam.autoLookAtTarget = false
- // camera.userData.autoLookAtTarget = false
- // } else {
- // cam.autoLookAtTarget = camera.userData.autoLookAtTarget
- // }
- // return cam
- // }
-
- // /**
- // * Create a new empty object in the scene or add an existing three.js object to the scene.
- // * @param object
- // */
- // async createObject3D(object?: Object3D): Promise<Object3DModel | undefined> {
- // return this.getManager()?.addImportedSingle<Object3DModel>(object || new Object3D(), {autoScale: false, pseudoCenter: false})
- // }
-
- // /**
- // * 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.
- // * @param material
- // */
- // createPhysicalMaterial(material?: Material|MeshPhysicalMaterialParameters): MeshStandardMaterial2 | undefined {
- // return this.createMaterial<MeshStandardMaterial2>('standard', material)
- // }
-
- // /**
- // * 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.
- // * @param template - template name registered in MaterialManager
- // * @param material - three.js material object or material params to create a new material
- // */
- // createMaterial<T extends IMaterial<any>>(template: 'standard' | 'basic' | 'diamond' | string, material?: Material|any): T | undefined {
- // if ((material as Material)?.isMaterial) {
- // const f = this.getManager()?.materials?.findMaterial((material as Material).uuid)
- // if (f) return f as T
- // }
- // return this.getManager()?.materials?.generateFromTemplate(template, material) as T
- // }
-
- // endregion
-
- /**
- * 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
- * @deprecated - use {@link renderManager} instead
- */
- get renderer(): ViewerRenderManager {
- this.console.error('renderer is deprecated, use renderManager instead')
- return this.renderManager
- }
-
- /**
- * @deprecated use {@link assetManager} instead.
- * Gets the Asset manager, contains useful functions for managing, loading and inserting assets.
- */
- getManager(): AssetManager|undefined {
- return this.assetManager
- }
-
- /**
- * Get the Plugin by the string type.
- * @deprecated - Use {@link getPlugin} instead.
- * @param type
- * @returns {T | undefined}
- */
- getPluginByType<T extends IViewerPlugin>(type: string): T | undefined {
- return this.plugins[type] as T | undefined
- }
-
- }
|