import * as THREE from 'three' import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils' import debounce from 'lodash.debounce' import { filterAndColorObject } from './Filtering' /** * Container for the scene objects, to allow loading/unloading/filtering/coloring/grouping */ export default class SceneObjects { constructor( viewer ) { this.viewer = viewer this.scene = viewer.scene this.allObjects = new THREE.Group() this.allObjects.name = 'allObjects' this.allSolidObjects = new THREE.Group() this.allSolidObjects.name = 'allSolidObjects' this.allSolidObjects.visible = false // these are grouped later, we never want to display them individually this.allObjects.add( this.allSolidObjects ) this.allTransparentObjects = new THREE.Group() this.allTransparentObjects.name = 'allTransparentObjects' this.allObjects.add( this.allTransparentObjects ) this.allLineObjects = new THREE.Group() this.allLineObjects.name = 'allLineObjects' this.allObjects.add( this.allLineObjects ) this.allPointObjects = new THREE.Group() this.allPointObjects.name = 'allPointObjects' this.allObjects.add( this.allPointObjects ) // Grouped solid objects, generated from `allSolidObjects` this.groupedSolidObjects = new THREE.Group() this.groupedSolidObjects.name = 'groupedSolidObjects' this.allObjects.add( this.groupedSolidObjects ) this.filteredObjects = null this.appliedFilter = null // When the `appliedFilter` is null, scene will contain `allObjects`. Otherwise, `filteredObjects` // This is to optimize the no-filter usecase, so we don't make an unnecessary clone of all the objects this.objectsInScene = this.allObjects this.scene.add( this.allObjects ) this.isBusy = true this.lastAsyncPause = Date.now() } async asyncPause() { // Don't freeze the UI when doing all those traversals if ( Date.now() - this.lastAsyncPause >= 100 ) { // if (Date.now() - this.lastAsyncPause > 200 ) console.log("FREEZED for ", Date.now() - this.lastAsyncPause) await new Promise( resolve => setTimeout( resolve, 0 ) ) this.lastAsyncPause = Date.now() } } getObjectsProperties() { let flattenObject = function( obj ) { let flatten = {} for ( let k in obj ) { if ( [ 'id', '__closure', 'bbox', 'totalChildrenCount' ].includes( k ) ) continue let v = obj[ k ] if ( Array.isArray( v ) ) continue if ( v.constructor === Object ) { let flattenProp = flattenObject( v ) for ( let pk in flattenProp ) { flatten[ `${k}.${pk}` ] = flattenProp[ pk ] } continue } if ( [ 'string', 'number', 'boolean' ].includes( typeof v ) ) flatten[ k ] = v } return flatten } let propValues = {} for ( let objGroup of this.objectsInScene.children ) { for ( let threeObj of objGroup.children ) { let obj = flattenObject( threeObj.userData ) for ( let prop of Object.keys( obj ) ) { if ( !( prop in propValues ) ) { propValues[ prop ] = [] } propValues[ prop ].push( obj[ prop ] ) } } } let propInfo = {} for ( let prop in propValues ) { let pinfo = { type: typeof propValues[ prop ][ 0 ], objectCount: propValues[ prop ].length, allValues: propValues[ prop ], uniqueValues: {}, minValue: propValues[ prop ][ 0 ], maxValue: propValues[ prop ][ 0 ] } for ( let v of propValues[ prop ] ) { if ( v < pinfo.minValue ) pinfo.minValue = v if ( v > pinfo.maxValue ) pinfo.maxValue = v if ( !( v in pinfo.uniqueValues ) ) { pinfo.uniqueValues[ v ] = 0 } pinfo.uniqueValues[ v ] += 1 } propInfo[ prop ] = pinfo } return propInfo } async setFilteredView() { if ( !this.isInitialLoading ) { await this.applyFilter() return } this.isInitialLoading = false await this.applyFilter() this.scene.add( this.filteredObjects ) this.scene.remove( this.allObjects ) } async applyFilterToGroup( threejsGroup, filter ) { let ret = new THREE.Group() ret.name = 'filtered_' + threejsGroup.name for ( let obj of threejsGroup.children ) { await this.asyncPause() let filteredObj = filterAndColorObject( obj, filter ) if ( filteredObj ) ret.add( filteredObj ) } return ret } disposeAndClearGroup( threejsGroup, disposeGeometry = true ) { let t0 = Date.now() for ( let child of threejsGroup.children ) { if ( child.type === 'Group' ) { this.disposeAndClearGroup( child, disposeGeometry ) } if ( child.material ) child.material.dispose() if ( disposeGeometry && child.geometry ) child.geometry.dispose() } threejsGroup.clear() console.log("Dispose in: ", Date.now() - t0) } async applyFilter( filter ) { // eslint-disable-next-line no-param-reassign if ( filter === undefined ) filter = this.appliedFilter if ( filter === null ) { // Remove filters, use allObjects let newGoupedSolidObjects = await this.groupSolidObjects( this.allSolidObjects ) if ( this.groupedSolidObjects !== null ) { this.disposeAndClearGroup( this.groupedSolidObjects ) this.allObjects.remove( this.groupedSolidObjects ) } this.groupedSolidObjects = newGoupedSolidObjects this.allObjects.add( this.groupedSolidObjects ) if ( this.filteredObjects !== null ) { this.disposeAndClearGroup( this.filteredObjects ) this.filteredObjects = null } this.scene.remove( this.objectsInScene ) this.scene.add( this.allObjects ) this.objectsInScene = this.allObjects } else { // A filter is to be applied let newFilteredObjects = new THREE.Group() newFilteredObjects.name = 'FilteredObjects' let filteredSolidObjects = await this.applyFilterToGroup( this.allSolidObjects, filter ) filteredSolidObjects.visible = false newFilteredObjects.add( filteredSolidObjects ) let filteredLineObjects = await this.applyFilterToGroup( this.allLineObjects, filter ) newFilteredObjects.add( filteredLineObjects ) let filteredTransparentObjects = await this.applyFilterToGroup( this.allTransparentObjects, filter ) newFilteredObjects.add( filteredTransparentObjects ) let filteredPointObjects = await this.applyFilterToGroup( this.allPointObjects, filter ) newFilteredObjects.add( filteredPointObjects ) // group solid objects let groupedFilteredSolidObjects = await this.groupSolidObjects( filteredSolidObjects ) newFilteredObjects.add( groupedFilteredSolidObjects ) // Sync update scene if ( this.filteredObjects !== null ) { this.disposeAndClearGroup( this.filteredObjects ) } this.filteredObjects = newFilteredObjects this.scene.remove( this.objectsInScene ) this.scene.add( this.filteredObjects ) this.objectsInScene = this.filteredObjects } this.appliedFilter = filter this.viewer.needsRender = true } async groupSolidObjects( threejsGroup ) { let materialIdToBufferGeometry = {} let materialIdToMaterial = {} let materialIdToMeshes = {} for ( let mesh of threejsGroup.children ) { let m = mesh.material let materialId = `${m.type}/${m.vertexColors}/${m.color.toJSON()}/${m.side}/${m.transparent}/${m.opactiy}/${m.emissive}/${m.metalness}/${m.roughness}` if ( !( materialId in materialIdToBufferGeometry ) ) { materialIdToBufferGeometry[ materialId ] = [] materialIdToMaterial[ materialId ] = m materialIdToMeshes[ materialId ] = [] } materialIdToBufferGeometry[ materialId ].push( mesh.geometry ) materialIdToMeshes[ materialId ].push( mesh ) // Max 1024 objects per group (mergeBufferGeometries is sync and can freeze for large data) if ( materialIdToBufferGeometry[ materialId ].length >= 1024 ) { let archivedMaterialId = `arch//${materialId}//${mesh.id}` materialIdToBufferGeometry[ archivedMaterialId ] = materialIdToBufferGeometry[ materialId ] materialIdToMaterial[ archivedMaterialId ] = materialIdToMaterial[ materialId ] materialIdToMeshes[ archivedMaterialId ] = materialIdToMeshes[ materialId ] delete materialIdToBufferGeometry[ materialId ] delete materialIdToMaterial[ materialId ] delete materialIdToMeshes[ materialId ] } } let groupedObjects = new THREE.Group() groupedObjects.name = 'GroupedSolidObjects' await this.asyncPause() for ( let materialId in materialIdToBufferGeometry ) { await this.asyncPause() // TODO: does this handle transforms well ? let groupGeometry = BufferGeometryUtils.mergeBufferGeometries( materialIdToBufferGeometry[ materialId ] ) await this.asyncPause() let groupMaterial = materialIdToMaterial[ materialId ] let groupMesh = new THREE.Mesh( groupGeometry, groupMaterial ) groupMesh.userData = null groupedObjects.add( groupMesh ) } return groupedObjects } }