Files
speckle-server/packages/viewer/src/modules/SceneObjects.js
T
2021-11-03 13:11:15 +02:00

271 lines
9.2 KiB
JavaScript

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
}
}