Files
speckle-server/packages/viewer/src/modules/SceneObjects.js
T

346 lines
11 KiB
JavaScript

import * as THREE from 'three'
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils'
import FilteringManager from './FilteringManager'
/**
* 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.filteringManager = new FilteringManager(this.viewer)
this.filteredObjects = null
this.ghostedObjects = 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) {
await new Promise((resolve) => setTimeout(resolve, 0))
this.lastAsyncPause = Date.now()
}
}
getObjectsProperties(includeAll = true) {
const flattenObject = function (obj) {
const flatten = {}
for (const k in obj) {
if (['id', '__closure', '__parents', 'bbox', 'totalChildrenCount'].includes(k))
continue
const v = obj[k]
if (v === null || v === undefined || Array.isArray(v)) continue
if (v.constructor === Object) {
const flattenProp = flattenObject(v)
for (const pk in flattenProp) {
flatten[`${k}.${pk}`] = flattenProp[pk]
}
continue
}
if (['string', 'number', 'boolean'].includes(typeof v)) flatten[k] = v
}
return flatten
}
const targetObjects = includeAll ? this.allObjects : this.objectsInScene
const propValues = {}
for (const objGroup of targetObjects.children) {
for (const threeObj of objGroup.children) {
const obj = flattenObject(threeObj.userData)
for (const prop of Object.keys(obj)) {
if (!(prop in propValues)) {
propValues[prop] = []
}
propValues[prop].push(obj[prop])
}
}
}
const propInfo = {}
for (const prop in propValues) {
const pinfo = {
type: typeof propValues[prop][0],
objectCount: propValues[prop].length,
allValues: propValues[prop],
uniqueValues: {},
minValue: propValues[prop][0],
maxValue: propValues[prop][0]
}
for (const 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 applyFilterToGroup(threejsGroup, filter, ghostedObjectsOutput) {
const ret = new THREE.Group()
ret.name = 'filtered_' + threejsGroup.name
for (const obj of threejsGroup.children) {
await this.asyncPause()
const filteredObj = this.filteringManager.filterAndColorObject(obj, filter)
if (filteredObj) {
if (ghostedObjectsOutput && filteredObj.userData.hidden) {
ghostedObjectsOutput.add(filteredObj)
} else {
ret.add(filteredObj)
}
}
}
return ret
}
disposeAndClearGroup(threejsGroup, disposeGeometry = true) {
for (const 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()
}
async applyFilter(filter) {
// eslint-disable-next-line no-param-reassign
if (filter === undefined) filter = this.appliedFilter
if (filter === null) {
// Remove filters, use allObjects
const 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
}
if (this.ghostedObjects !== null) {
this.scene.remove(this.ghostedObjects)
this.disposeAndClearGroup(this.ghostedObjects)
this.ghostedObjects = null
}
this.scene.remove(this.objectsInScene)
this.scene.add(this.allObjects)
this.objectsInScene = this.allObjects
} else {
// A filter is to be applied
this.filteringManager.initFilterOperation()
const newFilteredObjects = new THREE.Group()
newFilteredObjects.name = 'FilteredObjects'
const newGhostedObjects = new THREE.Group()
newGhostedObjects.name = 'GhostedObjects'
const filteredSolidObjects = await this.applyFilterToGroup(
this.allSolidObjects,
filter,
newGhostedObjects
)
filteredSolidObjects.visible = false
newFilteredObjects.add(filteredSolidObjects)
const filteredLineObjects = await this.applyFilterToGroup(
this.allLineObjects,
filter,
newGhostedObjects
)
newFilteredObjects.add(filteredLineObjects)
const filteredTransparentObjects = await this.applyFilterToGroup(
this.allTransparentObjects,
filter,
newGhostedObjects
)
newFilteredObjects.add(filteredTransparentObjects)
const filteredPointObjects = await this.applyFilterToGroup(
this.allPointObjects,
filter,
newGhostedObjects
)
newFilteredObjects.add(filteredPointObjects)
// group solid objects
const groupedFilteredSolidObjects = await this.groupSolidObjects(
filteredSolidObjects
)
newFilteredObjects.add(groupedFilteredSolidObjects)
const groupedGhostedObjects = await this.groupSolidObjects(newGhostedObjects)
// Sync update scene
if (this.filteredObjects !== null) {
this.disposeAndClearGroup(this.filteredObjects)
}
this.filteredObjects = newFilteredObjects
if (this.ghostedObjects !== null) {
this.scene.remove(this.ghostedObjects)
this.disposeAndClearGroup(this.ghostedObjects)
}
this.ghostedObjects = groupedGhostedObjects
this.scene.add(this.ghostedObjects)
this.scene.remove(this.objectsInScene)
this.scene.add(this.filteredObjects)
this.objectsInScene = this.filteredObjects
}
this.appliedFilter = filter
this.viewer.needsRender = true
return { colorLegend: this.filteringManager.colorLegend }
}
flattenGroup(group) {
const acc = []
for (const child of group.children) {
if (child instanceof THREE.Group) {
acc.push(...this.flattenGroup(child))
} else {
acc.push(child.clone())
}
}
for (const element of acc) {
element.geometry = element.geometry.clone()
element.geometry.applyMatrix4(group.matrix)
}
return acc
}
async groupSolidObjects(threejsGroup) {
const materialIdToBufferGeometry = {}
const materialIdToMaterial = {}
const materialIdToMeshes = {}
const groupedObjects = new THREE.Group()
groupedObjects.name = 'GroupedSolidObjects'
for (const obj of threejsGroup.children) {
let meshes = []
if (obj instanceof THREE.Group) {
meshes = this.flattenGroup(obj)
} else {
meshes = [obj]
}
for (const mesh of meshes) {
const m = mesh.material
// Pass-through non mesh materials (blocks can contain lines, that end up here)
if (
!(
m instanceof THREE.MeshStandardMaterial ||
m instanceof THREE.MeshBasicMaterial
)
) {
// if ( mesh.type === 'Line' ) continue
// if ( groupedObjects.children.length >= 2 ) continue
const clone = mesh.clone()
groupedObjects.add(clone)
continue
}
let materialId = `${m.type}/${m.vertexColors}/${m.color.toJSON()}/${m.side}/${
m.transparent
}/${m.opacity}/${m.emissive}/${m.metalness}/${m.roughness}/${m.wireframe}`
materialId += `--${Object.keys(mesh.geometry.attributes).toString()}--${!!mesh
.geometry.index}`
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) {
const 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]
}
}
}
await this.asyncPause()
for (const materialId in materialIdToBufferGeometry) {
await this.asyncPause()
const groupGeometry = BufferGeometryUtils.mergeBufferGeometries(
materialIdToBufferGeometry[materialId]
)
await this.asyncPause()
const groupMaterial = materialIdToMaterial[materialId]
const groupMesh = new THREE.Mesh(groupGeometry, groupMaterial)
groupMesh.userData = null
groupedObjects.add(groupMesh)
}
return groupedObjects
}
}