UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

924 lines (810 loc) 37.4 kB
import { BufferGeometry, Color, EquirectangularReflectionMapping, Euler, EventListener, EventListener2, IUniform, Object3D, Scene, UVMapping, Vector3, } from 'three' import type {IObject3D, IObject3DEventMap, IObjectProcessor} from '../IObject' import {type ICamera} from '../ICamera' import {autoGPUInstanceMeshes, Box3B} from '../../three' import {AnyOptions, onChange2, onChange3, serialize} from 'ts-browser-helpers' import {PerspectiveCamera2} from '../camera/PerspectiveCamera2' import {addModelProcess, centerAllGeometries, ThreeSerialization} from '../../utils' import {ITexture} from '../ITexture' import {AddObjectOptions, IScene, ISceneEventMap, ISceneSetDirtyOptions, IWidget} from '../IScene' import {iObjectCommons} from './iObjectCommons' import {RootSceneImportResult} from '../../assetmanager' import { uiButton, uiColor, uiConfig, uiFolderContainer, uiImage, UiObjectConfig, uiSlider, uiToggle, uiVector, } from 'uiconfig.js' import {getFittingDistance} from '../../three/utils/camera' import {iCameraCommons} from './iCameraCommons' @uiFolderContainer('Root Scene') export class RootScene<TE extends ISceneEventMap = ISceneEventMap> extends Scene<TE&ISceneEventMap> implements IScene<TE> { readonly isRootScene = true assetType = 'model' as const declare uiConfig: UiObjectConfig // private _processors = new ObjectProcessorMap<'environment' | 'background'>() // private _sceneObjects: ISceneObject[] = [] private _mainCamera: ICamera | null = null /** * The root object where all imported objects are added. */ readonly modelRoot: IObject3D // readonly lightsRoot: IObject3D // todo this can be added before modelRoot to add extra lights before the model root. @serialize() @onChange2(RootScene.prototype.onBackgroundChange) backgroundColor: Color | null = null // read in three.js WebGLBackground @uiColor<RootScene>('Background Color', (s)=>({ hidden: ()=>s.backgroundColor === null || s.backgroundColor === undefined || s.background === 'environment', })) protected get _backgroundColorUi() { return '#' + (this.backgroundColor?.getHexString() ?? '000000') } protected set _backgroundColorUi(v) { this.setBackgroundColor(v) } @onChange3(RootScene.prototype.onBackgroundChange) @serialize() @uiImage<RootScene>('Background Image', (s)=>({ hidden: ()=>s.backgroundColor === null || s.backgroundColor === undefined || s.background === 'environment', })) background: null | Color | ITexture | 'environment' = null /** * Toggle the background between color and transparent. */ @uiButton<RootScene>(undefined, (s)=>({ label: ()=>!s.backgroundColor ? 'Set Color Background' : 'Set Transparent BG', tags: ['context-menu'], })) toggleTransparentBackground() { if (!this.backgroundColor) { this.backgroundColor = new Color(0xffffff) // todo save last color and image? } else { this.background = null this.backgroundColor = null } this.refreshUi?.() this.setDirty() } /** * Toggle the background between texture and environment map. */ @uiButton<RootScene>(undefined, (s)=>({ label: ()=>s.background === 'environment' ? 'Remove Env Background' : 'Set Env Background', disabled: ()=>!s.environment, tags: ['context-menu'], })) toggleEnvironmentBackground() { if (this.background === 'environment') { this.background = null } else { this.background = 'environment' } } /** * The intensity for the background color and map. */ @serialize() @onChange3(RootScene.prototype.setDirty) @uiSlider('Background Intensity', [0, 10], 0.01) backgroundIntensity = 1 /** * Enable/Disable tonemapping selectively for the background. * Note - This requires both TonemapPlugin and GBufferPlugin or DepthBufferPlugin to be in the viewer to work. */ @uiToggle('Background Tonemap'/* , (e)=>({e._viewer?.renderManager.gbufferTarget})*/) // todo let scene access the viewer @onChange3(RootScene.prototype.setDirty) @serialize() backgroundTonemap = true private _environment: ITexture | null = null /** * The default environment map used when rendering materials in the scene */ @uiImage('Environment') @serialize() // Note: getter/setter defined in constructor using Object.defineProperty declare environment: ITexture | null /** * The intensity for the environment light. */ @uiSlider('Environment Intensity', [0, 10], 0.01) @serialize() @onChange3(RootScene.prototype.setDirty) environmentIntensity = 1 @serialize() @uiVector<RootScene>('Environment Rotation', undefined, undefined, (t)=>({disabled: ()=>t.fixedEnvMapDirection || !t.environment})) declare environmentRotation: Euler @serialize() @uiVector<RootScene>('Background Rotation', undefined, undefined, (t)=>({hidden: ()=>!t.background || t.background === 'environment' || !(t.background as any).isTexture || (t.background as any).mapping !== EquirectangularReflectionMapping})) declare backgroundRotation: Euler // @uiSlider('Environment Rotation', [-Math.PI, Math.PI], 0.01) // @bindToValue({obj: 'environment', key: 'rotation', onChange: RootScene.prototype.setDirty, onChangeParams: false}) // envMapRotation = 0 /** * Extra textures/envmaps that can be used by objects/materials/plugins and will be serialized. */ @serialize() public textureSlots: Record<string, ITexture> = {} /** * Fixed direction environment reflections irrespective of camera position. */ @uiToggle('Fixed Env Direction') @serialize() @onChange3(RootScene.prototype.setDirty) fixedEnvMapDirection = false /** * The default camera in the scene. This camera is always in the scene and used by default if no camera is set as main. * It is also saved along with the scene JSON and shown in the UI. This is added to the scene root, hence not saved in the glTF when a scene glb is exported. */ @uiConfig() @serialize() readonly defaultCamera: ICamera /** * Calls dispose on current old environment map, background map when it is changed. * Runtime only (not serialized) */ autoDisposeSceneMaps = true // private _environmentLight?: IEnvironmentLight // required just because we don't want activeCamera to be null. private _dummyCam = new PerspectiveCamera2('') as ICamera get mainCamera(): ICamera { return this._mainCamera || this._dummyCam } set mainCamera(camera: ICamera | undefined) { const cam = this.mainCamera if (!camera) camera = this.defaultCamera if (cam === camera) return if (cam) { cam.deactivateMain(undefined, true) cam.removeEventListener('cameraUpdate', this._mainCameraUpdate) } if (camera) { this._mainCamera = camera camera.addEventListener('cameraUpdate', this._mainCameraUpdate) camera.activateMain(undefined, true) if (!camera._canvas && camera !== this.defaultCamera) { console.warn('RootScene: mainCamera does not have a canvas set, some controls might not work properly.') } } else { this._mainCamera = null } this.dispatchEvent({type: 'activeCameraChange', lastCamera: cam, camera}) // deprecated this.dispatchEvent({type: 'mainCameraChange', lastCamera: cam, camera}) this.setDirty() } private _renderCamera: ICamera | undefined get renderCamera() { return this._renderCamera ?? this.mainCamera } set renderCamera(camera: ICamera) { const cam = this._renderCamera this._renderCamera = camera this.dispatchEvent({type: 'renderCameraChange', lastCamera: cam, camera}) } objectProcessor?: IObjectProcessor /** * Create a scene instance. This is done automatically in the {@link ThreeViewer} and must not be created separately. * @param camera * @param objectProcessor */ constructor(camera: ICamera, objectProcessor?: IObjectProcessor) { super() this.setDirty = this.setDirty.bind(this) this.name = 'RootScene' this.objectProcessor = objectProcessor iObjectCommons.upgradeObject3D.call(this) this.objectProcessor?.processObject(this) // this is called from parentDispatch since scene is a parent. this.addEventListener('materialUpdate', (e: any)=>this.dispatchEvent({...e, type: 'sceneMaterialUpdate'})) this.addEventListener('objectUpdate', this.refreshScene) this.addEventListener('geometryUpdate', this.refreshScene) this.addEventListener('geometryChanged', this.refreshScene) this.environmentRotation?._onChange(()=>{ this.setDirty({key: 'environmentRotation', value: this.environmentRotation}) }) this.backgroundRotation?._onChange(()=>{ this.setDirty({key: 'backgroundRotation', value: this.backgroundRotation}) }) this.defaultCamera = camera this.modelRoot = new Object3D() as IObject3D this.modelRoot.userData.rootSceneModelRoot = true this.modelRoot.name = 'Scene' // for the UI // this.modelRoot.addEventListener('update', this.setDirty) // todo: where was this dispatched from/used ? // eslint-disable-next-line deprecation/deprecation this.add(this.modelRoot as any) // this.addSceneObject(this.modelRoot as any, {addToRoot: true, autoScale: false}) // eslint-disable-next-line deprecation/deprecation this.add(this.defaultCamera) this.mainCamera = this.defaultCamera Object.defineProperty(this, 'environment', { configurable: true, enumerable: true, get: () => { // Return override environment if we're in render step and override is set if (this._isMainRendering && this.overrideRenderEnvironment !== null) { return this.overrideRenderEnvironment } return this._environment }, set: (value: ITexture | null) => { const oldValue = this._environment this._environment = value this._onEnvironmentChange({key: 'environment', value, oldValue, target: this}) }, }) } /** * Add any object to the scene. * @param imported * @param options */ addObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T&IObject3D { if (options?.clearSceneObjects || options?.disposeSceneObjects) { this.clearSceneModels(options.disposeSceneObjects) } if (!imported) return imported if (!imported.isObject3D) { console.error('Invalid object, cannot add to scene.', imported) return imported as T&IObject3D } this._addObject3D(<IObject3D>imported, options) this.dispatchEvent({type: 'addSceneObject', object: <IObject3D>imported, options}) return imported as T&IObject3D } /** * Load model root scene exported to GLTF format. Used internally by {@link ThreeViewer.addSceneObject}. * @param obj * @param options */ loadModelRoot(obj: RootSceneImportResult, options?: AddObjectOptions) { if (options?.clearSceneObjects || options?.disposeSceneObjects) { this.clearSceneModels(options.disposeSceneObjects) } if (!obj.userData?.rootSceneModelRoot) { console.error('RootScene: Invalid model root scene object. Trying to add anyway.', obj) } if (obj.userData) { // todo deep merge all userdata? if (obj.userData.__importData) // this is with `__` as it is not automatically serialized, but it can be read in gltf exporter extensions and serialized manually this.modelRoot.userData.__importData = { ...this.modelRoot.userData.__importData, ...obj.userData.__importData, } if (obj.userData.gltfAsset) { this.modelRoot.userData.gltfAsset = { // todo: why are we merging values? ...this.modelRoot.userData.gltfAsset, ...obj.userData.gltfAsset, extras: { ...this.modelRoot.userData.gltfAsset?.extras, ...obj.userData.gltfAsset.extras, }, } } if (obj.userData.gltfExtras) this.modelRoot.userData.gltfExtras = { ...this.modelRoot.userData.gltfExtras, ...obj.userData.gltfExtras, } } if (obj.userData?.gltfAsset?.copyright) obj.children.forEach(c => !c.userData.license && (c.userData.license = obj.userData.gltfAsset?.copyright)) if (obj.animations) { if (!this.modelRoot.animations) this.modelRoot.animations = [] for (const animation of obj.animations) { if (this.modelRoot.animations.includes(animation)) continue this.modelRoot.animations.push(animation) } } if (obj._loadingPromise) { if (this.modelRoot._loadingPromise) { this.modelRoot._loadingPromise = Promise.allSettled([this.modelRoot._loadingPromise, obj._loadingPromise]) } else { this.modelRoot._loadingPromise = obj._loadingPromise } } const children = obj._childrenCopy || [...obj.children] return children.map(c=>this.addObject(c, {...options, clearSceneObjects: false, disposeSceneObjects: false})) } private _addObject3D(model: IObject3D|null, {addToRoot = false, ...options}: AddObjectOptions = {}): void { const obj = model if (!obj || !obj.isObject3D) { console.error('RootScene: Invalid object, cannot add to scene.') return } const target = addToRoot ? this : this.modelRoot target.add(obj) if (options.indexInParent !== undefined) { const newIndex = options.indexInParent const newIndex2 = target.children.indexOf(obj) if (newIndex >= 0 && newIndex2 >= 0 && newIndex !== newIndex2 && newIndex < target.children.length) { target.children.splice(newIndex2, 1) target.children.splice(newIndex, 0, obj) // add at new index } } addModelProcess(obj, options) this.setDirty({refreshScene: true}) } @uiButton('Center All Geometries', {sendArgs: false, tags: ['context-menu']}) centerAllGeometries(keepPosition = true, obj?: IObject3D) { return centerAllGeometries(obj ?? this.modelRoot, keepPosition) } clearSceneModels(dispose = false, setDirty = true): void { if (dispose) return this.disposeSceneModels(setDirty) this.modelRoot.clear() this.modelRoot.children = [] setDirty && this.setDirty({refreshScene: true}) } disposeSceneModels(setDirty = true, clear = true) { if (clear) { for (const child of [...this.modelRoot.children]) { child.dispose ? child.dispose() : child.removeFromParent() } this.modelRoot.clear() if (setDirty) this.setDirty({refreshScene: true}) } else { for (const child of this.modelRoot.children) { child.dispose && child.dispose(false) } } } private _onEnvironmentChange(ev?: {value: ITexture|null, oldValue: ITexture|null, key?: string, target?: any}) { if (ev?.oldValue && ev.oldValue !== ev.value) { if (this.autoDisposeSceneMaps && typeof ev.oldValue.dispose === 'function') ev.oldValue.dispose() } // console.warn('environment changed') if (this.environment?.mapping === UVMapping) { this.environment.mapping = EquirectangularReflectionMapping // for PMREMGenerator this.environment.needsUpdate = true } // todo dispatch texturesChanged also this.dispatchEvent({ type: 'environmentChanged', oldTexture: ev?.oldValue?.isTexture ? ev.oldValue : null, texture: this.environment?.isTexture ? this.environment : null, environment: this.environment, }) this.setDirty({refreshScene: true, geometryChanged: false}) this.refreshUi?.() } onBackgroundChange(ev?: {value: ITexture|null, oldValue: ITexture|null}) { if (ev?.oldValue && ev.oldValue !== ev.value) { if (this.autoDisposeSceneMaps && typeof ev.oldValue.dispose === 'function') ev.oldValue.dispose() } // todo dispatch texturesChanged also this.dispatchEvent({ type: 'backgroundChanged', oldTexture: ev?.oldValue && ev.oldValue.isTexture ? ev.oldValue : null, texture:(this.background as ITexture)?.isTexture ? (this.background as ITexture) : null, background: this.background, backgroundColor: this.backgroundColor, }) this.setDirty({refreshScene: true, geometryChanged: false}) this.refreshUi?.() } /** * @deprecated Use {@link addObject} */ add(...object: Object3D[]): this { const filter = object.filter(o=>o.parent !== this) filter.length && super.add(...filter) // to prevent multiple event dispatch // this._onSceneUpdate() // this is not needed, since it will be bubbled up from the object3d and we will get event objectUpdate return this } /** * Sets the backgroundColor property from a string, number or Color, and updates the scene. * Note that when setting a `Color` object, it will be cloned. * @param color */ setBackgroundColor(color: string | number | Color | null) { const col = color || typeof color === 'number' ? new Color(color) : null if (col && this.backgroundColor && !col.equals(this.backgroundColor) || (!col || !this.backgroundColor) && col !== this.backgroundColor ) this.backgroundColor = col } /** * Mark the scene dirty, and force render in the next frame. * @param options - set `refreshScene` to true to mark that any object transformations have changed. It might trigger effects like frame fade depening on plugins. * @returns {this} */ setDirty(options?: ISceneSetDirtyOptions): this { // todo: for onChange calls -> check options.key for specific key that's changed and use it to determine refreshScene if (options?.sceneUpdate) { console.warn('sceneUpdate is deprecated, use refreshScene instead.') options.refreshScene = true } this.dispatchEvent({type: 'update', bubbleToParent: false, object: this}) // todo remove iObjectCommons.setDirty.call(this, {...options, scene: this}) return this } private _mainCameraUpdate: EventListener2<'cameraUpdate', IObject3DEventMap, ICamera> = (e) => { if (!this._mainCamera?.parent) this.setDirty({refreshScene: false}) this.dispatchEvent({...e, type: 'mainCameraUpdate'}) this.dispatchEvent({...e, type: 'activeCameraUpdate'}) // deprecated if (e.source !== 'RootScene') { if (e.key === 'fov' && this.dollyActiveCameraFov()) return if (this.refreshActiveCameraNearFar(!e.projectionUpdated)) { // it will call mainCameraUpdate twice, that's fine first without projectionUpdated, then with projectionUpdated return } } } // cached values private _sceneBounds: Box3B = new Box3B private _sceneBoundingRadius = 0 refreshScene(event?: Partial<(ISceneEventMap['objectUpdate']|ISceneEventMap['geometryUpdate']|ISceneEventMap['geometryChanged'])> & ISceneSetDirtyOptions & {type?: keyof ISceneEventMap}): this { const fromSelf = event && event.type === 'objectUpdate' && (event.object === this || (event as any).target === this) // todo test the isCamera here. this is for animation object plugin if (event?.sceneUpdate === false || event?.refreshScene === false || event?.object?.isCamera) return fromSelf ? this : this.setDirty(event) // so that it doesn't trigger frame fade, shadow refresh etc // console.warn(event) this.refreshActiveCameraNearFar() // this.dollyActiveCameraFov() this._sceneBounds = this.getBounds(false, true) this._sceneBoundingRadius = this._sceneBounds.getSize(new Vector3()).length() / 2. this.dispatchEvent({...event, type: 'sceneUpdate', hierarchyChanged: ['addedToParent', 'removedFromParent'].includes(event?.change || '')}) if (!fromSelf) iObjectCommons.setDirty.call(this, {...event, scene: this}) return this } refreshUi = iObjectCommons.refreshUi.bind(this) traverseModels = iObjectCommons.traverseModels.bind(this) /** * Dispose the scene and clear all resources. */ dispose(clear = true): void { this.disposeSceneModels(false, clear) if (clear) { [...this.children].forEach(child => child.dispose ? child.dispose() : child.removeFromParent()) this.clear() } // todo: dispose more stuff? this.disposeTextures(clear) return } /** * Dispose and optionally remove all textures set directly on this scene. * @param clear */ disposeTextures(clear = true) { this.environment?.dispose() if ((this.background as ITexture)?.isTexture) (this.background as ITexture)?.dispose?.() if (clear) { this.environment = null this.background = null } } /** * Returns the bounding box of the whole scene (model root and other meta objects). * To get the bounds of just the objects added by the user(not by plugins) use `new Box3B().expandByObject(scene.modelRoot)` * @param precise * @param ignoreInvisible * @param ignoreWidgets * @param ignoreObject * @returns {Box3B} */ getBounds(precise = false, ignoreInvisible = true, ignoreWidgets = true, ignoreObject?: (obj: Object3D)=>boolean): Box3B { // See bboxVisible in userdata in Box3B return new Box3B().expandByObject(this, precise, ignoreInvisible, (o: any)=>{ if (ignoreWidgets && ((o as IWidget).isWidget || o.assetType === 'widget')) return true return ignoreObject?.(o) ?? false }) } /** * Similar to {@link getBounds}, but returns the bounding box of just the {@link modelRoot}. * @param precise * @param ignoreInvisible * @param ignoreWidgets * @param ignoreObject * @returns {Box3B} */ getModelBounds(precise = false, ignoreInvisible = true, ignoreWidgets = true, ignoreObject?: (obj: Object3D)=>boolean): Box3B { if (this.modelRoot == undefined) return new Box3B() return new Box3B().expandByObject(this.modelRoot, precise, ignoreInvisible, (o: any)=>{ if (ignoreWidgets && o.assetType === 'widget') return true return ignoreObject?.(o) ?? false }) } @uiButton('Auto GPU Instance Meshes', {tags: ['context-menu']}) autoGPUInstanceMeshes() { const geoms = new Set<BufferGeometry>() this.modelRoot.traverseModels!((o) => {o.geometry && geoms.add(o.geometry)}, {visible: false, widgets: false}) geoms.forEach((g: any) => autoGPUInstanceMeshes(g)) } private _v1 = new Vector3() private _v2 = new Vector3() private _autoNearFarDisabled = new Set<string>() /** * For Programmatically toggling autoNearFar. This property is not supposed to be in the UI or serialized. * Use camera.userData.autoNearFar for UI and serialization * This is used in PickingPlugin, editor plugins * autoNearFar will still be disabled if this is true and camera.userData.autoNearFar is false */ disableAutoNearFar(id = 'default') { const enabled = this._autoNearFarDisabled.size === 0 this._autoNearFarDisabled.add(id) const camera = this.mainCamera as ICamera if (enabled && camera.userData.autoNearFar !== false) { let near = camera.near, far = camera.far near = camera.userData.minNearPlane ?? iCameraCommons.defaultMinNear far = camera.userData.maxFarPlane ?? iCameraCommons.defaultMaxFar iCameraCommons.setNearFar(camera, near, far, true, 'RootScene') } } enableAutoNearFar(id = 'default') { if (!this._autoNearFarDisabled.has(id)) return this._autoNearFarDisabled.delete(id) const camera = this.mainCamera as ICamera if (this._autoNearFarDisabled.size === 0 && camera) { this.setDirty() } } /** * Refreshes the scene active camera near far values, based on the scene bounding box. * This is called automatically every time the camera is updated. */ refreshActiveCameraNearFar(setDirty = true): boolean { const camera = this.mainCamera as ICamera if (!camera) return false let near = camera.near, far = camera.far if (camera.userData.minNearPlane !== undefined) { near = camera.userData.minNearPlane } if (camera.userData.maxFarPlane !== undefined) { far = camera.userData.maxFarPlane } // console.log(this.autoNearFarEnabled, camera.userData.autoNearFar, camera.userData.maxFarPlane, camera.far) if (this._autoNearFarDisabled.size !== 0 || camera.userData.autoNearFar === false) { return iCameraCommons.setNearFar(camera, near, far, setDirty, 'RootScene') } // todo check if this takes too much time with large scenes(when moving the camera and not animating), but we also need to support animations const bbox = this.getBounds(false) // todo: can we use this._sceneBounds or will it have some issue with animation? const size = bbox.getSize(this._v2).length() if (size < 0.001) { return iCameraCommons.setNearFar(camera, near, far, setDirty, 'RootScene') } camera.getWorldPosition(this._v1).sub(bbox.getCenter(this._v2)) const radius = 1.5 * Math.max(0.25, size) / 2. const dist = this._v1.length() // new way const dist1 = Math.max(0.1, -this._v1.normalize().dot(camera.getWorldDirection(new Vector3()))) near = Math.max(Math.max(camera.userData.minNearPlane ?? iCameraCommons.defaultMinNear, 0.001), dist1 * (dist - radius)) far = Math.min(Math.max(near + radius, dist1 * (dist + radius)), camera.userData.maxFarPlane ?? iCameraCommons.defaultMaxFar) // old way, has issues when panning very far from the camera target // const near = Math.max(camera.userData.minNearPlane ?? 0.2, dist - radius) // const far = Math.min(Math.max(near + 1, dist + radius), camera.userData.maxFarPlane ?? 1000) if (far < near || far - near < 0.1) { far = near + 0.1 } return iCameraCommons.setNearFar(camera, near, far, setDirty, 'RootScene') // todo try using minimum of all 6 endpoints of bbox. // camera.near = 3 // camera.far = 20 } /** * Refreshes the scene active camera near far values, based on the scene bounding box. * This is called automatically every time the camera fov is updated. */ dollyActiveCameraFov(): boolean { const camera = this.mainCamera as ICamera if (!camera) return false if (!camera.userData.dollyFov) { return false } const bbox = this.getModelBounds(false, true, true) // todo this is not exact because of 1.5, this needs to be calculated based on current position and last fov const cameraZ = getFittingDistance(camera, bbox) * 1.5 const direction = new Vector3().subVectors(camera.target, camera.position).normalize() camera.position.copy(direction.multiplyScalar(-cameraZ).add(camera.target)) camera.setDirty({change: 'position', source: 'RootScene'}) return true } updateShaderProperties(material: {defines: Record<string, string|number|undefined>, uniforms: {[name: string]: IUniform}}): this { if (material.uniforms.sceneBoundingRadius) material.uniforms.sceneBoundingRadius.value = this._sceneBoundingRadius else console.warn('RootScene: no uniform: sceneBoundingRadius') return this } /** * Serialize the scene properties * @param meta * @returns {any} */ toJSON(meta?: any): any { const o = ThreeSerialization.Serialize(this, meta, true) o.envMapIntensity = o.environmentIntensity // for backward compatibility, remove later return o } /** * Deserialize the scene properties * @param json - object from {@link toJSON} * @param meta * @returns {this<ICamera>} */ fromJSON(json: any, meta?: any): this { const env = json.environment if (env !== undefined) { this.environment = ThreeSerialization.Deserialize(env, this.environment, meta, false) delete json.environment if (meta?._configMetadata && meta._configMetadata.version < 2) { // legacy - files saved pre three.js < r162, threepipe < v0.4.0 if (this.environment?.rotation) { // old files used to save y rotation inside the texture. this.environmentRotation.y = this.environment.rotation this.environment.rotation = 0 // for next save } } } // some files have both for backwards compatibility, prefer environmentIntensity if (json.environmentIntensity !== undefined && json.envMapIntensity !== undefined) { json = {...json} delete json.envMapIntensity } ThreeSerialization.Deserialize(json, this, meta, true) json.environment = env return this } addEventListener<T extends keyof ISceneEventMap>(type: T, listener: EventListener<ISceneEventMap[T], T, this>): void { if (type === 'activeCameraChange') console.error('activeCameraChange is deprecated. Use mainCameraChange instead.') if (type === 'activeCameraUpdate') console.error('activeCameraUpdate is deprecated. Use mainCameraUpdate instead.') if (type === 'sceneMaterialUpdate') console.error('sceneMaterialUpdate is deprecated. Use materialUpdate instead.') if (type === 'update') console.error('update is deprecated. Use sceneUpdate instead.') super.addEventListener(type, listener) } /** * Override environment map to use during rendering. * When set and _isMainRendering is true, this will be returned instead of the normal environment. */ overrideRenderEnvironment: ITexture | null = null; /** * Flag to indicate if we're currently in the render step. * Set by ViewerApp during rendering. * @internal */ ['_isMainRendering'] = false // region inherited type fixes // re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936 declare traverse: (callback: (object: IObject3D) => void) => void declare traverseVisible: (callback: (object: IObject3D) => void) => void declare traverseAncestors: (callback: (object: IObject3D) => void) => void declare getObjectById: (id: number) => IObject3D | undefined declare getObjectByName: (name: string) => IObject3D | undefined declare getObjectByProperty: (name: string, value: string) => IObject3D | undefined // dispatchEvent: (event: ISceneEvent) => void declare parent: IObject3D | null declare children: IObject3D[] // endregion // region deprecated /** * Find objects by name exact match in the complete hierarchy. * @deprecated Use {@link getObjectByName} instead. * @param name - name * @param parent - optional root node to start search from * @returns Array of found objects */ public findObjectsByName(name: string, parent?: IObject3D, upgradedOnly = false): IObject3D[] { const o: IObject3D[] = [] const fn = (object: IObject3D) => { if (object.name === name) o.push(object) } const obj: IObject3D = parent ?? this if (upgradedOnly && obj.traverseModels) obj.traverseModels(fn, {visible: false, widgets: true}) else obj.traverse(fn) return o } /** * @deprecated * Sets the camera pointing towards the object at a specific distance. * @param rootObject - The object to point at. * @param centerOffset - The distance offset from the object to point at. * @param targetOffset - The distance offset for the target from the center of object to point at. */ resetCamera(rootObject:Object3D|undefined = undefined, centerOffset = new Vector3(1, 1, 1), targetOffset = new Vector3(0, 0, 0)): void { if (this._mainCamera) { this.matrixWorldNeedsUpdate = true this.updateMatrixWorld(true) const bounds = rootObject ? new Box3B().expandByObject(rootObject, true, true) : this.getBounds(true) const center = bounds.getCenter(new Vector3()) const radius = bounds.getSize(new Vector3()).length() * 0.5 center.add(targetOffset.clone().multiplyScalar(radius)) this._mainCamera.position = new Vector3( // todo: for nested cameras? center.x + centerOffset.x * radius, center.y + centerOffset.y * radius, center.z + centerOffset.z * radius, ) this._mainCamera.target = center // this.scene.mainCamera.controls?.targetOffset.set(0, 0, 0) this.setDirty() } } /** * Minimum Camera near plane * @deprecated - use camera.minNearPlane instead */ get minNearDistance(): number { console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead') return this.mainCamera.userData.minNearPlane ?? 0.02 } /** * @deprecated - use camera.minNearPlane instead */ set minNearDistance(value: number) { console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead') if (this.mainCamera) this.mainCamera.userData.minNearPlane = value } /** * @deprecated */ get activeCamera(): ICamera { console.error('activeCamera is deprecated. Use mainCamera instead.') return this.mainCamera } /** * @deprecated */ set activeCamera(camera: ICamera | undefined) { console.error('activeCamera is deprecated. Use mainCamera instead.') this.mainCamera = camera } /** * Get the threejs scene object * @deprecated */ get modelObject(): this { return this as any } /** * Add any processed scene object to the scene. * @deprecated renamed to {@link addObject} * @param imported * @param options */ addSceneObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T { return this.addObject(imported, options) } /** * Equivalent to setDirty({refreshScene: true}), dispatches 'sceneUpdate' event with the specified options. * @deprecated use refreshScene * @param options */ updateScene(options?: AnyOptions): this { console.warn('updateScene is deprecated. Use refreshScene instead') return this.refreshScene(options || {}) } /** * @deprecated renamed to {@link clearSceneModels} */ removeSceneModels() { this.clearSceneModels() } /** * @deprecated use {@link enableAutoNearFar} and {@link disableAutoNearFar} instead. */ get autoNearFarEnabled() { return this._autoNearFarDisabled.size === 0 } /** * @deprecated use {@link enableAutoNearFar} and {@link disableAutoNearFar} instead. */ set autoNearFarEnabled(v) { if (v) this.enableAutoNearFar('default') else this.disableAutoNearFar('default') } /** * @deprecated Use environmentIntensity instead. */ get envMapIntensity() { console.warn('RootScene.envMapIntensity is deprecated, use environmentIntensity instead.') return this.environmentIntensity } set envMapIntensity(value: number) { console.warn('RootScene.envMapIntensity is deprecated, use environmentIntensity instead.') this.environmentIntensity = value } // endregion }