| @@ -8,7 +8,6 @@ import { | |||
| ImportResult, | |||
| LoadFileOptions, | |||
| ProcessRawOptions, | |||
| RootSceneImportResult, | |||
| } from './IAssetImporter' | |||
| import {IAsset, IFile} from './IAsset' | |||
| import {IImporter, ILoader} from './IImporter' | |||
| @@ -138,21 +137,22 @@ export class AssetImporter extends EventDispatcher<IAssetImporterEvent, IAssetIm | |||
| // console.log(result) | |||
| if (!options.forceImport && result) { | |||
| const results = await this.processRaw<T>(result, options) // just in case its not processed. Internal check is done to ensure it's not processed twice | |||
| let isDisposed = false // if any of the objects is disposed | |||
| for (const r of results) { | |||
| // todo: check if this is still required. | |||
| if ((r as RootSceneImportResult)?.userData?.rootSceneModelRoot) { // in case processImported is false we need a special case check here | |||
| if (r?.children?.find((c: any) => c.__disposed)) { | |||
| isDisposed = true | |||
| break | |||
| } | |||
| } | |||
| if (r && !r.__disposed) continue // todo add __disposed to object, material, texture, etc | |||
| isDisposed = true | |||
| break | |||
| } | |||
| // let isDisposed = false // if any of the objects is disposed | |||
| // for (const r of results) { | |||
| // // todo: check if this is still required. | |||
| // if ((r as RootSceneImportResult)?.userData?.rootSceneModelRoot) { // in case processImported is false we need a special case check here | |||
| // if (r?.children?.find((c: any) => c.__disposed)) { | |||
| // isDisposed = true | |||
| // break | |||
| // } | |||
| // } | |||
| // if (r && !r.__disposed) continue // todo add __disposed to object, material, texture, etc | |||
| // isDisposed = true | |||
| // break | |||
| // } | |||
| // todo: should we check if any of it's children is disposed ? | |||
| if (!isDisposed || options.reimportDisposed === false) return results | |||
| // if (!isDisposed || options.reimportDisposed === false) | |||
| return results | |||
| } | |||
| // todo: add support to get cloned asset? if we want to import multiple times and everytime return a cloned asset | |||
| @@ -170,7 +170,7 @@ export class AssetImporter extends EventDispatcher<IAssetImporterEvent, IAssetIm | |||
| else arrs.push(result) | |||
| } | |||
| // remove preImportedRaw when any of the assets is disposed. This is to prevent memory leaks | |||
| arrs.forEach(r=>r.addEventListener?.('dispose', () => { | |||
| arrs.forEach(r=>r.addEventListener?.('dispose', () => { // todo: recheck after dispose logic change | |||
| if (asset?.preImportedRaw) asset.preImportedRaw = undefined | |||
| if (asset?.preImported) asset.preImported = undefined | |||
| })) | |||
| @@ -293,6 +293,9 @@ export class AssetImporter extends EventDispatcher<IAssetImporterEvent, IAssetIm | |||
| if (file) { | |||
| file.__loadedAsset = res | |||
| // todo: recheck below code after dispose logic change | |||
| // Clear the reference __loadedAsset when any one asset is disposed. | |||
| // it's a bit hacky to do this here, but it works for now. todo: move to a better place | |||
| let ress: any[] = [] | |||
| @@ -30,12 +30,9 @@ export interface ImportResultExtras { | |||
| userData?: IImportResultUserData | |||
| // eslin t-disable-next-line @typescript-eslint/naming-convention | |||
| __rootPath?: string | |||
| // eslin t-disable-next-line @typescript-eslint/naming-convention | |||
| __rootBlob?: IFile | |||
| // eslin t-disable-next-line @typescript-eslint/naming-convention | |||
| __disposed?: boolean | |||
| // __disposed?: boolean | |||
| [key: string]: any | |||
| } | |||
| @@ -12,7 +12,7 @@ import { | |||
| } from '../core' | |||
| import {downloadFile} from 'ts-browser-helpers' | |||
| import {MaterialExtension} from '../materials' | |||
| import {generateUUID} from '../three/utils/misc' | |||
| import {generateUUID, isInScene} from '../three' | |||
| export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> { | |||
| @@ -104,14 +104,11 @@ export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> { | |||
| const mat = e.target | |||
| if (!mat || mat.assetType !== 'material') return | |||
| mat.setDirty() | |||
| const maps = this._getMapsForMaterial(mat) | |||
| maps.forEach(map=>{ | |||
| const mats = map.userData.__appliedMaterials! | |||
| mats?.delete(mat) | |||
| if (!mats || map.userData.disposeOnIdle === false) return | |||
| if (mats.size === 0) map.dispose() | |||
| }) | |||
| this.unregisterMaterial(mat) | |||
| this._getMapsForMaterial(mat) | |||
| .forEach(map=> | |||
| !map.isRenderTargetTexture && map.userData.disposeOnIdle !== false && | |||
| map.dispose && !isInScene(map) && map.dispose()) | |||
| // this.unregisterMaterial(mat) // todo | |||
| } | |||
| private _materialMaps = new Map<string, Set<ITexture>>() | |||
| @@ -5,7 +5,11 @@ import {IObject3D} from './IObject' | |||
| import {IImportResultUserData} from '../assetmanager' | |||
| export interface IGeometryUserData extends IImportResultUserData{ | |||
| disposeOnIdle?: boolean // default: true | |||
| /** | |||
| * Automatically dispose geometry when not used by any object in the scene | |||
| * @default true | |||
| */ | |||
| disposeOnIdle?: boolean | |||
| // [key: string]: any // commented for noe | |||
| } | |||
| export interface IGeometry<Attributes extends NormalOrGLBufferAttributes = NormalBufferAttributes> extends BufferGeometry<Attributes, IGeometryEvent, IGeometryEventTypes>, IUiConfigContainer { | |||
| @@ -18,6 +22,13 @@ export interface IGeometry<Attributes extends NormalOrGLBufferAttributes = Norma | |||
| // Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation | |||
| userData: IGeometryUserData | |||
| /** | |||
| * Disposes the geometry from the GPU. | |||
| * Set force to false if not sure the geometry is used by any object in the scene. | |||
| * // todo add check for visible in scene also? or is that overkill | |||
| * @param force - when true, same as three.js dispose. when false, only disposes if disposeOnIdle not false and not used by any object in the scene. default: true | |||
| */ | |||
| dispose(force?: boolean): void | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| _uiConfig?: UiObjectConfig | |||
| @@ -41,8 +41,11 @@ export interface IMaterialSetDirtyOptions { | |||
| } | |||
| export interface IMaterialUserData extends IImportResultUserData{ | |||
| uuid?: string // adding to userdata also, so that its saved in gltf | |||
| disposeOnIdle?: boolean // default: true | |||
| /** | |||
| * Automatically dispose material when not used by any object in the scene | |||
| * @default true | |||
| */ | |||
| disposeOnIdle?: boolean | |||
| renderToGBuffer?: boolean | |||
| /** | |||
| @@ -61,7 +64,7 @@ export interface IMaterialUserData extends IImportResultUserData{ | |||
| inverseAlphaMap?: boolean // only for physical material right now | |||
| // [key: string]: any // commented for noe | |||
| [key: string]: any // commented for noe | |||
| // legacy, to be removed | |||
| @@ -110,6 +113,14 @@ export interface IMaterial<E extends IMaterialEvent = IMaterialEvent, ET = IMate | |||
| // Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation | |||
| userData: IMaterialUserData | |||
| /** | |||
| * Disposes the material from the GPU. | |||
| * Set force to false if not sure the material is used by any object in the scene. | |||
| * // todo add check for visible in scene also? or is that overkill | |||
| * @param force - when true, same as three.js dispose. when false, only disposes if disposeOnIdle not false and not used by any object in the scene. default: true | |||
| */ | |||
| dispose(force?: boolean): void | |||
| // optional from subclasses, added here for autocomplete | |||
| flatShading?: boolean | |||
| map?: ITexture | null | |||
| @@ -90,8 +90,17 @@ export interface IObject3DUserData extends IImportResultUserData { | |||
| * @deprecated | |||
| */ | |||
| dispose?: any | |||
| /** | |||
| * @deprecated | |||
| */ | |||
| setMaterial?: any | |||
| /** | |||
| * @deprecated | |||
| */ | |||
| setGeometry?: any | |||
| /** | |||
| * @deprecated | |||
| */ | |||
| setDirty?: any | |||
| /** | |||
| @@ -173,8 +182,12 @@ export interface IObject3D<E extends Event = IObject3DEvent, ET = IObject3DEvent | |||
| objectProcessor?: IObjectProcessor | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| __disposed?: boolean | |||
| // __disposed?: boolean | |||
| /** | |||
| * | |||
| * @param removeFromParent - remove from parent. Default true | |||
| */ | |||
| dispose(removeFromParent?: boolean): void; | |||
| // region inherited type fixes | |||
| @@ -1,11 +1,16 @@ | |||
| import {IMaterial} from './IMaterial' | |||
| import {Event, Texture} from 'three' | |||
| import {ChangeEvent} from 'uiconfig.js' | |||
| import {IRenderTarget} from '../rendering' | |||
| export interface ITextureUserData{ | |||
| mimeType?: string | |||
| embedUrlImagePreviews?: boolean | |||
| disposeOnIdle?: boolean // automatically dispose when added to a material and then not used in any material | |||
| /** | |||
| * Automatically dispose texture when not used by any material that's applied to some object in the scene. | |||
| * Works only after it's applied to a material once. | |||
| */ | |||
| disposeOnIdle?: boolean | |||
| __appliedMaterials?: Set<IMaterial> | |||
| } | |||
| export type ITextureEventTypes = 'dispose' | 'update' | |||
| @@ -28,6 +33,7 @@ export interface ITexture extends Texture { | |||
| setDirty?(): void | |||
| _target?: IRenderTarget // for internal use only. refers to the render target that this texture is attached to | |||
| } | |||
| export function upgradeTexture(this: ITexture) { | |||
| @@ -1,6 +1,7 @@ | |||
| import {UiObjectConfig} from 'uiconfig.js' | |||
| import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry' | |||
| import {toIndexedGeometry} from '../../three' | |||
| import {isInScene, toIndexedGeometry} from '../../three' | |||
| import {BufferGeometry} from 'three' | |||
| export const iGeometryCommons = { | |||
| setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void { | |||
| @@ -10,6 +11,11 @@ export const iGeometryCommons = { | |||
| refreshUi: function(this: IGeometry) { | |||
| this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) | |||
| }, | |||
| dispose: (superDispose: BufferGeometry['dispose']): IGeometry['dispose'] => | |||
| function(this: IGeometry, force = true): void { | |||
| if (!force && (this.userData.disposeOnIdle === false || isInScene(this))) return | |||
| superDispose.call(this) | |||
| }, | |||
| upgradeGeometry: upgradeGeometry, | |||
| makeUiConfig: function(this: IGeometry): UiObjectConfig { | |||
| if (this.uiConfig) return this.uiConfig | |||
| @@ -117,18 +123,23 @@ export const iGeometryCommons = { | |||
| }, | |||
| } | |||
| export function upgradeGeometry(this: IGeometry) { | |||
| function upgradeGeometry(this: IGeometry) { | |||
| if (this.assetType === 'geometry') return // already upgraded | |||
| if (!this.isBufferGeometry) { | |||
| console.error('Material is not a this', this) | |||
| console.error('Geometry is not a this', this) | |||
| return | |||
| } | |||
| this.assetType = 'geometry' | |||
| this.dispose = iGeometryCommons.dispose(this.dispose) | |||
| if (!this.setDirty) this.setDirty = iGeometryCommons.setDirty | |||
| if (!this.refreshUi) this.refreshUi = iGeometryCommons.refreshUi | |||
| if (!this.appliedMeshes) this.appliedMeshes = new Set() | |||
| if (!this.userData) this.userData = {} | |||
| this.uiConfig = iGeometryCommons.makeUiConfig.call(this) | |||
| // todo: dispose uiconfig on geometry dispose | |||
| // todo: add serialization? | |||
| @@ -44,6 +44,7 @@ export class PhysicalMaterial extends MeshPhysicalMaterial<IMaterialEvent, Physi | |||
| readonly appliedMeshes: Set<IObject3D> = new Set() | |||
| readonly setDirty = iMaterialCommons.setDirty | |||
| dispose(): this {return iMaterialCommons.dispose(super.dispose).call(this)} | |||
| clone(): this {return iMaterialCommons.clone(super.clone).call(this)} | |||
| dispatchEvent(event: IMaterialEvent): void {iMaterialCommons.dispatchEvent(super.dispatchEvent).call(this, event)} | |||
| @@ -44,6 +44,9 @@ export class ShaderMaterial2<E extends IMaterialEvent = IMaterialEvent, ET = IMa | |||
| readonly appliedMeshes: Set<any> = new Set() | |||
| readonly setDirty = iMaterialCommons.setDirty | |||
| dispose(): this {return iMaterialCommons.dispose(super.dispose).call(this)} | |||
| clone(): this {return iMaterialCommons.clone(super.clone).call(this)} | |||
| dispatchEvent(event: IMaterialEvent): void {iMaterialCommons.dispatchEvent(super.dispatchEvent).call(this, event)} | |||
| readonly isRawShaderMaterial: boolean | |||
| @@ -44,6 +44,7 @@ export class UnlitMaterial extends MeshBasicMaterial<IMaterialEvent, UnlitMateri | |||
| readonly appliedMeshes: Set<IObject3D> = new Set() | |||
| readonly setDirty = iMaterialCommons.setDirty | |||
| dispose(): this {return iMaterialCommons.dispose(super.dispose).call(this)} | |||
| clone(): this {return iMaterialCommons.clone(super.clone).call(this)} | |||
| dispatchEvent(event: IMaterialEvent): void {iMaterialCommons.dispatchEvent(super.dispatchEvent).call(this, event)} | |||
| @@ -19,6 +19,7 @@ import {copyMaterialUserData} from '../../utils/serialization' | |||
| import {MaterialExtender, MaterialExtension} from '../../materials' | |||
| import {IScene} from '../IScene' | |||
| import {IMaterial, IMaterialEvent, IMaterialSetDirtyOptions} from '../IMaterial' | |||
| import {isInScene} from '../../three' | |||
| /** | |||
| * Map of all material properties and their default values in three.js - Material.js | |||
| @@ -105,6 +106,11 @@ export const iMaterialCommons = { | |||
| this.setDirty?.() | |||
| return this | |||
| }, | |||
| dispose: (superDispose: Material<any, any>['dispose']): IMaterial['dispose'] => | |||
| function(this: IMaterial, force = true): void { | |||
| if (!force && (this.userData.disposeOnIdle === false || isInScene(this))) return | |||
| superDispose.call(this) | |||
| }, | |||
| clone: (superClone: Material<any, any>['clone']): IMaterial['clone'] => | |||
| function(this: IMaterial): IMaterial { | |||
| if (!this.userData.cloneId) { | |||
| @@ -202,6 +208,7 @@ export function upgradeMaterial(this: IMaterial): IMaterial { | |||
| if (this.assetType === 'material') return this // already upgraded | |||
| this.assetType = 'material' | |||
| this.setValues = iMaterialCommons.setValues(this.setValues) | |||
| this.dispose = iMaterialCommons.dispose(this.dispose) | |||
| this.clone = iMaterialCommons.clone(this.clone) | |||
| this.dispatchEvent = iMaterialCommons.dispatchEvent(this.dispatchEvent) | |||
| @@ -6,7 +6,7 @@ import {copyObject3DUserData} from '../../utils' | |||
| import {IGeometry, IGeometryEvent} from '../IGeometry' | |||
| import {Box3B} from '../../three' | |||
| import {makeIObject3DUiConfig} from './IObjectUi' | |||
| import {upgradeGeometry} from '../geometry/iGeometryCommons' | |||
| import {iGeometryCommons} from '../geometry/iGeometryCommons' | |||
| import {iMaterialCommons} from '../material/iMaterialCommons' | |||
| export const iObjectCommons = { | |||
| @@ -158,8 +158,8 @@ export const iObjectCommons = { | |||
| if (!mat) continue | |||
| if (mat.appliedMeshes) { | |||
| mat.appliedMeshes.delete(this) | |||
| if (mat.userData && mat.appliedMeshes?.size === 0 && mat.userData.disposeOnIdle !== false) | |||
| mat.dispose() // this will dispose textures(if they are idle) if the material is registered in the material manager | |||
| // if (mat.userData && mat.appliedMeshes?.size === 0 && mat.userData.disposeOnIdle !== false) | |||
| mat.dispose(false) // this will dispose textures(if they are idle) if the material is registered in the material manager | |||
| } | |||
| } | |||
| @@ -223,13 +223,13 @@ export const iObjectCommons = { | |||
| this._onGeometryUpdate && geom.removeEventListener('geometryUpdate', this._onGeometryUpdate) | |||
| if (geom.appliedMeshes) { | |||
| geom.appliedMeshes.delete(this) | |||
| if (geom.userData && geom.appliedMeshes.size === 0 && geom.userData.disposeOnIdle !== false) geom.dispose() | |||
| geom.dispose(false) | |||
| } | |||
| } | |||
| if (geometry) { | |||
| if (!geometry.assetType) { | |||
| // console.error('Geometry not upgraded') | |||
| upgradeGeometry.call(geometry) | |||
| iGeometryCommons.upgradeGeometry.call(geometry) | |||
| } | |||
| } | |||
| this._currentGeometry = geometry || null | |||
| @@ -286,27 +286,24 @@ export const iObjectCommons = { | |||
| return superAdd.call(this, ...args) | |||
| }, | |||
| dispose: (superDispose?: IObject3D['dispose']) => | |||
| function(this: IObject3D): void { | |||
| this.dispatchEvent({type: 'dispose', bubbleToParent: false}) | |||
| if (this.__disposed) { | |||
| console.warn('Object already disposed', this) | |||
| return | |||
| function(this: IObject3D, removeFromParent = true): void { | |||
| if (removeFromParent && this.parent) { | |||
| this.removeFromParent() | |||
| delete this.parentRoot | |||
| } | |||
| this.__disposed = true | |||
| // this is first so that the leaf children are removed from parent first, removed event will be fired depth first | |||
| for (const c of [...this.children]) c?.dispose?.() | |||
| this.children = [] | |||
| if (this.parent) this.removeFromParent() | |||
| this.dispatchEvent({type: 'dispose', bubbleToParent: false}) | |||
| // if (this.__disposed) { | |||
| // console.warn('Object already disposed', this) | |||
| // return | |||
| // } | |||
| // this.__disposed = true | |||
| delete this.parentRoot | |||
| // safeSetProperty(this, 'modelObject', undefined, true) // in-case modelObject is just a getter. | |||
| this.userData = {} // todo: clear only our userdata and maybe any private variables? | |||
| for (const c of [...this.children]) c?.dispose && c.dispose(false) // not removing the children from parent to preserve hierarchy | |||
| // this.children = [] | |||
| // this.uiConfig?.dispose?.() // todo: make uiConfig.dispose | |||
| this.uiConfig = undefined | |||
| superDispose?.call(this) | |||
| }, | |||
| @@ -322,10 +319,10 @@ export const iObjectCommons = { | |||
| function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectProcessor?: IObjectProcessor): void { // parent is the root Object3DModel. | |||
| if (!this) return | |||
| // console.log('upgradeObject3D', this, parent, objectProcessor) | |||
| if (this.__disposed) { | |||
| console.warn('re-init/re-add disposed object, things might not work as intended', this) | |||
| delete this.__disposed | |||
| } | |||
| // if (this.__disposed) { | |||
| // console.warn('re-init/re-add disposed object, things might not work as intended', this) | |||
| // delete this.__disposed | |||
| // } | |||
| if (!this.userData) this.userData = {} | |||
| this.userData.uuid = this.uuid | |||
| @@ -346,14 +343,14 @@ function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectPr | |||
| if (parent) this.parentRoot = parent | |||
| const oldFunctions = { | |||
| dispatchEvent: this.dispatchEvent, | |||
| clone: this.clone, | |||
| copy: this.copy, | |||
| add: this.add, | |||
| dispose: this.dispose, | |||
| } | |||
| this.addEventListener('dispose', () => Object.assign(this, oldFunctions)) // todo: is this required? | |||
| // const oldFunctions = { | |||
| // dispatchEvent: this.dispatchEvent, | |||
| // clone: this.clone, | |||
| // copy: this.copy, | |||
| // add: this.add, | |||
| // dispose: this.dispose, | |||
| // } | |||
| // this.addEventListener('dispose', () => Object.assign(this, oldFunctions)) // todo: is this required? | |||
| // typed because of type-checking | |||
| this.dispatchEvent = iObjectCommons.dispatchEvent(this.dispatchEvent) | |||
| @@ -371,10 +368,10 @@ function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectPr | |||
| this.addEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent) | |||
| this.addEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent) | |||
| this.addEventListener('dispose', ()=>{ | |||
| this.removeEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent) | |||
| this.removeEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent) | |||
| }) | |||
| // this.addEventListener('dispose', ()=>{ | |||
| // this.removeEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent) | |||
| // this.removeEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent) | |||
| // }) | |||
| if ((this.isMesh || this.isLine) && !this.userData.__meshSetup) { | |||
| this.userData.__meshSetup = true | |||
| @@ -393,22 +390,26 @@ function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectPr | |||
| } | |||
| this.addEventListener('dispose', ()=>{ | |||
| if (this.material) { | |||
| // const oldMats = Array.isArray(this.material) ? [...(this.material as IMaterial[])] : [this.material!] | |||
| this.material = undefined // this will dispose material if not used by other meshes | |||
| // delete this.material | |||
| // for (const oldMat of oldMats) { | |||
| // if (oldMat && oldMat.userData && oldMat.appliedMeshes?.size === 0 && oldMat.userData.disposeOnIdle !== false) oldMat.dispose() | |||
| // } | |||
| } | |||
| if (this.geometry) { | |||
| // const oldGeom = this.geometry | |||
| this.geometry = undefined // this will dispose geometry if not used by other meshes | |||
| // delete this.geometry | |||
| // if (oldGeom && oldGeom.userData && oldGeom.appliedMeshes?.size === 0 && oldGeom.userData.disposeOnIdle !== false) oldGeom.dispose() | |||
| } | |||
| delete this._onGeometryUpdate | |||
| (this.materials || [<IMaterial> this.material]).forEach(m => m?.dispose(false)) | |||
| this.geometry?.dispose(false) | |||
| // if (this.material) { | |||
| // // const oldMats = Array.isArray(this.material) ? [...(this.material as IMaterial[])] : [this.material!] | |||
| // this.material = undefined // this will dispose material if not used by other meshes | |||
| // // delete this.material | |||
| // // for (const oldMat of oldMats) { | |||
| // // if (oldMat && oldMat.userData && oldMat.appliedMeshes?.size === 0 && oldMat.userData.disposeOnIdle !== false) oldMat.dispose() | |||
| // // } | |||
| // } | |||
| // if (this.geometry) { | |||
| // // const oldGeom = this.geometry | |||
| // this.geometry = undefined // this will dispose geometry if not used by other meshes | |||
| // // delete this.geometry | |||
| // // if (oldGeom && oldGeom.userData && oldGeom.appliedMeshes?.size === 0 && oldGeom.userData.disposeOnIdle !== false) oldGeom.dispose() | |||
| // } | |||
| // | |||
| // delete this._onGeometryUpdate | |||
| }) | |||
| } | |||
| @@ -424,29 +425,27 @@ function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectPr | |||
| for (const c of children) upgradeObject3D.call(c, this) | |||
| // region Legacy | |||
| if (this.userData.dispose) console.warn('userData.dispose already defined') | |||
| this.userData.dispose = () => { | |||
| // eslint-disable-next-line deprecation/deprecation | |||
| !this.userData.dispose && (this.userData.dispose = () => { | |||
| console.warn('userData.dispose is deprecated, use dispose directly') | |||
| this.dispose?.() | |||
| } | |||
| if (!this.modelObject) { | |||
| Object.defineProperty(this, 'modelObject', { | |||
| get: ()=>{ | |||
| console.error('modelObject is deprecated, use object directly') | |||
| return this | |||
| }, | |||
| }) | |||
| } | |||
| if (!this.userData.setDirty) | |||
| this.userData.setDirty = (e: any)=>{ | |||
| console.error('object.userData.setDirty is deprecated, use object.setDirty directly') | |||
| this.setDirty?.(e) | |||
| } | |||
| this.dispose && this.dispose() | |||
| }) | |||
| // eslint-disable-next-line deprecation/deprecation | |||
| !this.modelObject && Object.defineProperty(this, 'modelObject', { | |||
| get: ()=>{ | |||
| console.error('modelObject is deprecated, use object directly') | |||
| return this | |||
| }, | |||
| }) | |||
| // eslint-disable-next-line deprecation/deprecation | |||
| !this.userData.setDirty && (this.userData.setDirty = (e: any)=>{ | |||
| console.error('object.userData.setDirty is deprecated, use object.setDirty directly') | |||
| this.setDirty?.(e) | |||
| }) | |||
| // endregion | |||
| // if (!this.objectProcessor) console.warn('objectProcessor not set for', this) | |||
| // else | |||
| this.objectProcessor?.processObject(this) | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| import {BufferGeometry, MathUtils} from 'three' | |||
| import {mergeVertices} from 'three/examples/jsm/utils/BufferGeometryUtils.js' | |||
| import {IGeometry, IMaterial, IObject3D, IScene, ITexture} from '../../core' | |||
| /** | |||
| * Convert geometry to BufferGeometry with indexed attributes. | |||
| @@ -11,3 +12,24 @@ export function toIndexedGeometry(geometry: BufferGeometry<any, any, any>, toler | |||
| export function generateUUID() { | |||
| return MathUtils.generateUUID() | |||
| } | |||
| /** | |||
| * Check if a single or multiple object/geometry/material/texture is in the scene. | |||
| * This is used internally to check if a material is used by any object in the scene, and if not, it can be disposed. | |||
| * @param sceneObj | |||
| */ | |||
| export function isInScene(...sceneObj: (IGeometry|IMaterial|IObject3D|ITexture)[]): boolean { | |||
| if (sceneObj.length > 1) return sceneObj.some((a)=>isInScene(a)) | |||
| const o = sceneObj[0] | |||
| if ((<ITexture>o).isTexture) return Array.from((<ITexture>o).userData.__appliedMaterials || []).some((m) => isInScene(m)) ?? false | |||
| const objects = | |||
| (<IObject3D>o).isObject3D ? [<IObject3D>o] : | |||
| (<IGeometry|IMaterial>o).appliedMeshes | |||
| for (const obj of objects) { | |||
| let inScene = false | |||
| obj.traverseAncestors((ob: IObject3D) => (<IScene>ob).isScene && (inScene = true)) | |||
| if (inScene) return true | |||
| } | |||
| return false | |||
| } | |||