From ce7d26a35bb32d24db91fa0ff6c504895570cc18 Mon Sep 17 00:00:00 2001 From: cristi8 Date: Wed, 3 Nov 2021 13:11:15 +0200 Subject: [PATCH] wip viewer --- packages/viewer/package-lock.json | 5 + packages/viewer/package.json | 1 + packages/viewer/src/modules/Filtering.js | 142 +++++++++++++++++- packages/viewer/src/modules/SceneObjects.js | 78 +++++++--- .../viewer/src/modules/SelectionHelper.js | 8 +- 5 files changed, 212 insertions(+), 22 deletions(-) diff --git a/packages/viewer/package-lock.json b/packages/viewer/package-lock.json index 9c9075e43..bd84815fe 100644 --- a/packages/viewer/package-lock.json +++ b/packages/viewer/package-lock.json @@ -7658,6 +7658,11 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "rainbowvis.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rainbowvis.js/-/rainbowvis.js-1.0.1.tgz", + "integrity": "sha1-6JmPuXFhsCVXdIx9oJeWKw1uPgA=" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", diff --git a/packages/viewer/package.json b/packages/viewer/package.json index db4d966cd..0c5fb9a65 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -56,6 +56,7 @@ "dependencies": { "camera-controls": "^1.33.0", "lodash.debounce": "^4.0.8", + "rainbowvis.js": "^1.0.1", "three": "^0.133.1" } } diff --git a/packages/viewer/src/modules/Filtering.js b/packages/viewer/src/modules/Filtering.js index 1ebdde8eb..0cd3a1b33 100644 --- a/packages/viewer/src/modules/Filtering.js +++ b/packages/viewer/src/modules/Filtering.js @@ -1,10 +1,144 @@ +import * as THREE from 'three' +import Rainbow from 'rainbowvis.js' +import { cloneUniforms } from 'three' + +const WireframeMaterial = new THREE.MeshStandardMaterial( { + color: 0x7080A0, + side: THREE.DoubleSide, + transparent: true, + opacity: 0.04, + wireframe: true +} ) + +const ColoredMaterial = new THREE.MeshStandardMaterial( { + color: 0x7080A0, + side: THREE.DoubleSide, + transparent: false +} ) + export function filterAndColorObject( obj, filter ) { if ( !filter ) return obj.clone() - - if ( filter.speckle_type && obj.userData?.speckle_type !== filter.speckle_type ) + if ( !passesAndFilter( obj.userData, filter.and ) ) + { + if ( filter.wireframeFilter && obj.type === 'Mesh' ) { + let clone = obj.clone() + // clone.material = WireframeMaterial + clone.material = obj.material.clone() + clone.material.transparent = true + clone.material.opacity = 0.05 + clone.userData = null + return clone + } return null - - return obj.clone() + } + + let clone = obj.clone() + if ( filter.colors ) { + if ( filter.colors.type === 'category' ) { + clone.material = colorWithCategory( obj.userData, filter.colors ) + } else if ( filter.colors.type === 'gradient' ) { + clone.material = colorWithGradient( obj.userData, filter.colors ) + } + } + + return clone +} + +function getObjectProperty( obj, property ) { + if ( !property ) return + let keyParts = property.split( '.' ) + let crtObj = obj + for ( let i = 0; i < keyParts.length - 1; i++ ) { + if ( !( keyParts[i] in crtObj ) ) return + crtObj = crtObj[ keyParts[i] ] + if ( crtObj.constructor !== Object ) return + } + let attributeName = keyParts[ keyParts.length - 1 ] + return crtObj[ attributeName ] +} + +function colorWithCategory( obj, colors ) { + let defaultValue = colors.default + let color = defaultValue + let objValue = getObjectProperty( obj, colors.property ) + let customPallete = colors.values || {} + if ( objValue in customPallete ) { + color = customPallete[ objValue ] + } + + if ( !color ) { + // compute value hash + let objValueAsString = '' + objValue + let hash = 0 + for( let i = 0; i < objValueAsString.length; i++ ) { + let chr = objValueAsString.charCodeAt( i ) + hash = ( ( hash << 5 ) - hash ) + chr + hash |= 0 // Convert to 32bit integer + } + hash = Math.abs( hash ) + let colorHue = hash % 360 + color = `hsl(${colorHue}, 50%, 30%)` + } + + let material = ColoredMaterial.clone() + material.color = new THREE.Color( color ) + return material +} + +function colorWithGradient( obj, colors ) { + let rainbow = new Rainbow( ) + if ( 'minValue' in colors && 'maxValue' in colors ) + rainbow.setNumberRange( colors.minValue, colors.maxValue ) + if ( 'gradientColors' in colors ) + rainbow.setSpectrum( ...colors.gradientColors ) + + let objValue = getObjectProperty( obj, colors.property ) + objValue = Number( objValue ) + if ( Number.isNaN( objValue ) ) { + // TODO: have different behaviour for missing values? + objValue = 0 + } + + let material = ColoredMaterial.clone() + material.color = new THREE.Color( `#${rainbow.colourAt( objValue )}` ) + return material +} + +function passesAndFilter( obj, andFilter ) { + if ( !andFilter ) return true + for ( let filterKey in andFilter ) { + let objValue = getObjectProperty( obj, filterKey ) + + // let keyParts = filterKey.split( '.' ) + // let crtObj = obj + // for ( let i = 0; i < keyParts.length - 1; i++ ) { + // if ( !( keyParts[i] in crtObj ) ) return false + // crtObj = crtObj[ keyParts[i] ] + // if ( crtObj.constructor !== Object ) return false + // } + // let attributeName = keyParts[ keyParts.length - 1 ] + let passesFilter = filterValue( objValue, andFilter[ filterKey ] ) + if ( !passesFilter ) return false + } + return true +} + +function filterValue( objValue, valueFilter ) { + // Array value filter means it can be any value from the array + if ( Array.isArray( valueFilter ) ) + return valueFilter.includes( objValue ) + + // Dictionary value filter can specify ranges with `lte` and `gte` fields (LowerThanOrEqual, GreaterThanOrEqual) + if ( valueFilter.constructor === Object ) { + if ( 'lte' in valueFilter && objValue > valueFilter.lte ) + return false + if ( 'gte' in valueFilter && objValue < valueFilter.gte ) + return false + return true + } + + // Can also filter by specific value + return objValue === valueFilter } diff --git a/packages/viewer/src/modules/SceneObjects.js b/packages/viewer/src/modules/SceneObjects.js index 688d8f353..7ad37a78b 100644 --- a/packages/viewer/src/modules/SceneObjects.js +++ b/packages/viewer/src/modules/SceneObjects.js @@ -39,22 +39,6 @@ export default class SceneObjects { this.filteredObjects = null - // this.filteredObjects = new THREE.Group() - // this.filteredObjects.name = 'filteredObjects' - // this.filteredSolidObjects = new THREE.Group() - // this.filteredSolidObjects.name = 'filteredSolidObjects' - // this.filteredSolidObjects.visible = false // these are grouped later, we never want to display them individually - // this.filteredTransparentObjects = new THREE.Group() - // this.filteredTransparentObjects.name = 'filteredTransparentObjects' - // this.filteredLineObjects = new THREE.Group() - // this.filteredLineObjects.name = 'filteredLineObjects' - // this.filteredPointObjects = new THREE.Group() - // this.filteredPointObjects.name = 'filteredPointObjects' - // this.filteredObjects.add( this.filteredSolidObjects ) - // this.filteredObjects.add( this.filteredTransparentObjects ) - // this.filteredObjects.add( this.filteredLineObjects ) - // this.filteredObjects.add( this.filteredPointObjects ) - this.appliedFilter = null // When the `appliedFilter` is null, scene will contain `allObjects`. Otherwise, `filteredObjects` @@ -75,6 +59,65 @@ export default class SceneObjects { } } + 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() @@ -156,7 +199,7 @@ export default class SceneObjects { newFilteredObjects.add( filteredPointObjects ) // group solid objects - let groupedFilteredSolidObjects = await this.groupedSolidObjects( filteredSolidObjects ) + let groupedFilteredSolidObjects = await this.groupSolidObjects( filteredSolidObjects ) newFilteredObjects.add( groupedFilteredSolidObjects ) // Sync update scene @@ -217,6 +260,7 @@ export default class SceneObjects { await this.asyncPause() let groupMaterial = materialIdToMaterial[ materialId ] let groupMesh = new THREE.Mesh( groupGeometry, groupMaterial ) + groupMesh.userData = null groupedObjects.add( groupMesh ) } diff --git a/packages/viewer/src/modules/SelectionHelper.js b/packages/viewer/src/modules/SelectionHelper.js index c808f0303..943ce4597 100644 --- a/packages/viewer/src/modules/SelectionHelper.js +++ b/packages/viewer/src/modules/SelectionHelper.js @@ -134,7 +134,13 @@ export default class SelectionHelper extends EventEmitter { const normalizedPosition = this._getNormalisedClickPosition( e ) this.raycaster.setFromCamera( normalizedPosition, this.viewer.camera ) - let intersectedObjects = this.raycaster.intersectObjects( this.subset ? this._getGroupChildren( this.subset ) : this.viewer.sceneManager.filteredObjects ) + let targetObjects + if ( this.subset ) { + targetObjects = this._getGroupChildren( this.subset ) + } else { + targetObjects = this.viewer.sceneManager.filteredObjects.filter( obj => !!obj.userData ) + } + let intersectedObjects = this.raycaster.intersectObjects( targetObjects ) if ( this.sectionBox && this.sectionBox.display.visible ) {