#827. Implemented non-numeric filter-by properties support. Added this functionality to the sandbox as well.

This commit is contained in:
AlexandruPopovici
2022-07-22 15:35:34 +03:00
parent 92009018fc
commit 30e985be4a
6 changed files with 266 additions and 75 deletions
+86 -44
View File
@@ -9,8 +9,7 @@ export default class Sandbox {
private viewer: IViewer
private pane: Pane
private tabs
private minVolumeControl
private maxVolumeControl
private filterControls
public static urlParams = {
url: 'https://latest.speckle.dev/streams/010b3af4c3/objects/a401baf38fe5809d0eb9d3c902a36e8f'
@@ -31,6 +30,7 @@ export default class Sandbox {
public static filterParams = {
filterBy: 'Volume',
numericProperty: true,
data: {},
minValue: 0,
maxValue: 10000
@@ -235,58 +235,100 @@ export default class Sandbox {
title: 'Filtering',
expanded: true
})
filteringFolder.addInput(Sandbox.filterParams, 'filterBy', {
options: {
Volume: 'Volume',
Area: 'Area',
SpeckleType: 'speckle_type'
}
})
filteringFolder
.addInput(Sandbox.filterParams, 'filterBy', {
options: {
Volume: 'Volume',
Area: 'Area',
SpeckleType: 'speckle_type'
}
})
.on('change', () => {
switch (Sandbox.filterParams.filterBy) {
case 'Volume':
case 'Area':
Sandbox.filterParams.numericProperty = true
break
case 'speckle_type':
Sandbox.filterParams.numericProperty = false
}
})
filteringFolder
.addButton({
title: 'Apply Filter'
})
.on('click', () => {
Sandbox.filterParams.data = this.viewer.debugGetFilterByPropetyNodes(
Sandbox.filterParams.filterBy
)
Sandbox.filterParams.minValue = Sandbox.filterParams.data.min
Sandbox.filterParams.maxValue = Sandbox.filterParams.data.max
if (Sandbox.filterParams.numericProperty) {
Sandbox.filterParams.data = this.viewer.debugGetFilterByNumericPropetyData(
Sandbox.filterParams.filterBy
)
Sandbox.filterParams.minValue = Sandbox.filterParams.data.min
Sandbox.filterParams.maxValue = Sandbox.filterParams.data.max
this.viewer.debugApplyByNumericPropetyFilter(
Sandbox.filterParams.data,
Sandbox.filterParams.filterBy
)
this.viewer.debugApplyByPropetyFilter(
Sandbox.filterParams.data,
Sandbox.filterParams.filterBy
)
if (this.maxVolumeControl) this.maxVolumeControl.dispose()
if (this.minVolumeControl) this.minVolumeControl.dispose()
if (this.filterControls) this.filterControls.dispose()
this.filterControls = this.tabs.pages[2].addFolder({
title: 'Filter Options',
expanded: true
})
this.minVolumeControl = filteringFolder
.addInput(Sandbox.filterParams, 'minValue', {
min: Sandbox.filterParams.minValue,
max: Sandbox.filterParams.maxValue
this.filterControls
.addInput(Sandbox.filterParams, 'minValue', {
min: Sandbox.filterParams.minValue,
max: Sandbox.filterParams.maxValue
})
.on('change', () => {
this.viewer.debugApplyByNumericPropetyFilter(
Sandbox.filterParams.data,
Sandbox.filterParams.filterBy,
Sandbox.filterParams.minValue,
Sandbox.filterParams.maxValue
)
})
this.filterControls
.addInput(Sandbox.filterParams, 'maxValue', {
min: Sandbox.filterParams.minValue,
max: Sandbox.filterParams.maxValue
})
.on('change', () => {
this.viewer.debugApplyByNumericPropetyFilter(
Sandbox.filterParams.data,
Sandbox.filterParams.filterBy,
Sandbox.filterParams.minValue,
Sandbox.filterParams.maxValue
)
})
} else {
Sandbox.filterParams.data = this.viewer.debugGetFilterByNonNumericPropetyData(
Sandbox.filterParams.filterBy
)
this.viewer.debugApplyByNonNumericPropetyFilter(Sandbox.filterParams.data)
if (this.filterControls) this.filterControls.dispose()
this.filterControls = this.tabs.pages[2].addFolder({
title: 'Filter Options',
expanded: true
})
.on('change', () => {
this.viewer.debugApplyByPropetyFilter(
Sandbox.filterParams.data,
Sandbox.filterParams.filterBy,
Sandbox.filterParams.minValue,
Sandbox.filterParams.maxValue
)
})
this.maxVolumeControl = filteringFolder
.addInput(Sandbox.filterParams, 'maxValue', {
min: Sandbox.filterParams.minValue,
max: Sandbox.filterParams.maxValue
})
.on('change', () => {
this.viewer.debugApplyByPropetyFilter(
Sandbox.filterParams.data,
Sandbox.filterParams.filterBy,
Sandbox.filterParams.minValue,
Sandbox.filterParams.maxValue
)
const categories = Object.values(Sandbox.filterParams.data)
categories.forEach((category) => {
this.filterControls
.addInput(category, 'color', {
view: 'color',
label: category.name
})
.on('change', () => {
this.viewer.debugApplyByNonNumericPropetyFilter(
Sandbox.filterParams.data
)
})
})
}
this.pane.refresh()
})
}
+11
View File
@@ -134,6 +134,17 @@ export class Assets {
const texture = new DataTexture(data, width, height)
texture.needsUpdate = true
/** In case we want to see what gets generated */
// const canvas = document.createElement('canvas')
// canvas.width = width
// canvas.height = height
// const context = canvas.getContext('2d')
// const imageData = new ImageData(width, height)
// imageData.data.set(data)
// context.putImageData(imageData, 0, 0)
// console.log('SRC:', canvas.toDataURL())
return texture
}
}
+118 -15
View File
@@ -8,7 +8,7 @@ import InteractionHandler from './legacy/InteractionHandler'
import CameraHandler from './context/CameraHanlder'
import SectionBox from './SectionBox'
import { Clock, Texture, Vector3 } from 'three'
import { Clock, Color, Texture, Vector3 } from 'three'
import { Assets } from './Assets'
import { Optional } from '../helpers/typeHelper'
import { DefaultViewerParams, IViewer, ViewerParams } from '../IViewer'
@@ -322,22 +322,22 @@ export class Viewer extends EventEmitter implements IViewer {
}
}
public debugGetFilterByPropetyNodes(propertyName: string): {
public debugGetFilterByNumericPropetyData(propertyName: string): {
min: number
max: number
nodes: TreeNode[]
} {
const volumeNodes = []
let minVolume = Infinity
let maxVolume = 0
let min = Infinity
let max = 0
WorldTree.getInstance().walk((node: TreeNode) => {
const params = node.model.raw.parameters
if (params) {
for (const k in params) {
if (!(params[k] instanceof Object)) continue
if (params[k].name === propertyName) {
minVolume = Math.min(minVolume, params[k].value)
maxVolume = Math.max(maxVolume, params[k].value)
min = Math.min(min, params[k].value)
max = Math.max(max, params[k].value)
volumeNodes.push(node)
}
}
@@ -346,13 +346,13 @@ export class Viewer extends EventEmitter implements IViewer {
})
return {
min: minVolume,
max: maxVolume,
min,
max,
nodes: volumeNodes
}
}
public debugApplyByPropetyFilter(
public debugApplyByNumericPropetyFilter(
data: { min: number; max: number; nodes: TreeNode[] },
propertyName: string,
min?: number,
@@ -369,17 +369,17 @@ export class Viewer extends EventEmitter implements IViewer {
for (const k in params) {
if (!(params[k] instanceof Object)) continue
if (params[k].name === propertyName) {
const volumeValue = params[k].value
const pasMin = min !== undefined ? volumeValue >= min : true
const pasMax = max !== undefined ? volumeValue <= max : true
const propertyValue = params[k].value
const passMin = min !== undefined ? propertyValue >= min : true
const passMax = max !== undefined ? propertyValue <= max : true
if (
data.nodes.includes(node) &&
pasMin &&
pasMax &&
passMin &&
passMax &&
!nodesGradient.includes(node)
) {
nodesGradient.push(node)
values.push(volumeValue)
values.push(propertyValue)
}
} else {
if (!nodesGhost.includes(node)) nodesGhost.push(node)
@@ -413,6 +413,109 @@ export class Viewer extends EventEmitter implements IViewer {
this.speckleRenderer.endFilter()
}
public debugGetFilterByNonNumericPropetyData(propertyName: string): {
color?: { name: string; color: string; colorIndex: number; nodes: [] }
} {
// OG implementation
const getColorHash = (objValue) => {
const objValueAsString = '' + objValue
let hash = 0
for (let i = 0; i < objValueAsString.length; i++) {
const chr = objValueAsString.charCodeAt(i)
hash = (hash << 5) - hash + chr
hash |= 0 // Convert to 32bit integer
}
hash = Math.abs(hash)
const colorHue = hash % 360
const rgb = new Color(`hsl(${colorHue}, 50%, 30%)`)
return rgb.getHex()
}
const data: {
color?: { name: string; color: string; colorIndex: number; nodes: [] }
} = {}
let colorCount = 0
/** This is the lazy approach */
WorldTree.getInstance().walk((node: TreeNode) => {
const propertyValue = node.model.raw[propertyName]
if (propertyValue !== null) {
const color = getColorHash(propertyValue.split('.').reverse()[0])
if (data[color] === undefined) {
data[color] = {
name: propertyValue.split('.').reverse()[0],
color,
colorIndex: colorCount,
nodes: []
}
colorCount++
}
if (!data[color].nodes.includes(node)) data[color].nodes.push(node)
}
return true
})
return data
}
public debugApplyByNonNumericPropetyFilter(data: {
color?: { name: string; color: string; colorIndex: number; nodes: [] }
}) {
const colors = Object.values(data)
colors.sort((a, b) => a.colorIndex - b.colorIndex)
const rampTexture = Assets.generateDiscreetRampTexture(
colors.map((val) => val.color)
)
this.speckleRenderer.clearFilter()
this.speckleRenderer.beginFilter()
for (let k = 0; k < colors.length; k++) {
if (colors[k].name === 'Mesh' || colors[k].name === 'Base') continue
const nodes = colors[k].nodes
let ids = []
for (let i = 0; i < nodes.length; i++) {
ids = ids.concat(
WorldTree.getRenderTree()
.getRenderViewsForNode(nodes[i], nodes[i])
.map((value) => value.renderData.id)
)
}
this.speckleRenderer.applyFilter(ids, {
filterType: FilterMaterialType.COLORED,
rampIndex: colors[k].colorIndex / colors.length,
rampTexture
})
}
this.speckleRenderer.endFilter()
}
// private isObject(value) {
// return !!(value && typeof value === 'object' && !Array.isArray(value))
// }
// private findObjectProperty(object = {}, keyToMatch = '') {
// if (this.isObject(object)) {
// const entries = Object.entries(object)
// for (let i = 0; i < entries.length; i += 1) {
// const [objectKey, objectValue] = entries[i]
// if (objectKey === keyToMatch) {
// return object[objectKey]
// }
// if (this.isObject(objectValue)) {
// const child = this.findObjectProperty(objectValue, keyToMatch)
// if (child !== null) {
// return child
// }
// }
// }
// }
// return null
// }
public dispose() {
// TODO: currently it's easier to simply refresh the page :)
}
@@ -99,13 +99,17 @@ export default class Batcher {
ids: string[],
filterMaterial: FilterMaterial
): string[] {
let rvs = []
const rvs = []
ids.forEach((val: string) => {
const views = WorldTree.getRenderTree().getRenderViewsForNodeId(val)
for (let k = 0; k < views.length; k++) {
if (rvs.includes(views[k])) return
}
rvs = rvs.concat(views)
rvs.push(WorldTree.getRenderTree().getRenderViewForNodeId(val))
/** The batcher should take the explicit IDs it's given and roll with them
* It shouldn;t try to expand the list of render views on it's own
*/
// const views = WorldTree.getRenderTree().getRenderViewsForNodeId(val)
// for (let k = 0; k < views.length; k++) {
// if (rvs.includes(views[k])) return
// }
// rvs = rvs.concat(views)
})
// console.log(ids)
// console.log(rvs)
@@ -10,6 +10,7 @@ import {
Uint32BufferAttribute
} from 'three'
import { Geometry } from '../converter/Geometry'
import SpeckleStandardColoredMaterial from '../materials/SpeckleStandardColoredMaterial'
import { NodeRenderView } from '../tree/NodeRenderView'
import { World } from '../World'
import { Batch, BatchUpdateRange, HideAllBatchUpdateRange } from './Batch'
@@ -78,7 +79,7 @@ export default class MeshBatch implements Batch {
let maxGradientIndex = 0
for (let k = 0; k < sortedRanges.length; k++) {
if (sortedRanges[k].materialOptions) {
if (sortedRanges[k].materialOptions.gradientIndex) {
if (sortedRanges[k].materialOptions.rampIndex) {
const start = sortedRanges[k].offset
const len = sortedRanges[k].offset + sortedRanges[k].count
const minMaxIndices = this.updateGradientIndexBufferData(
@@ -86,11 +87,16 @@ export default class MeshBatch implements Batch {
sortedRanges[k].count === Infinity
? this.geometry.attributes['gradientIndex'].array.length
: len,
sortedRanges[k].materialOptions.gradientIndex
sortedRanges[k].materialOptions.rampIndex
)
minGradientIndex = Math.min(minGradientIndex, minMaxIndices.minIndex)
maxGradientIndex = Math.max(maxGradientIndex, minMaxIndices.maxIndex)
}
if (sortedRanges[k].materialOptions.rampTexture) {
;(
sortedRanges[k].material as SpeckleStandardColoredMaterial
).setGradientTexture(sortedRanges[k].materialOptions.rampTexture)
}
}
const collidingGroup = this.getDrawRangeCollision(sortedRanges[k])
if (collidingGroup) {
@@ -1,4 +1,4 @@
import { Color, DoubleSide, Material, MathUtils, Vector2 } from 'three'
import { Color, DoubleSide, Material, MathUtils, Texture, Vector2 } from 'three'
import { GeometryType } from '../batching/Batch'
// import { getConversionFactor } from '../converter/Units'
import { TreeNode } from '../tree/WorldTree'
@@ -13,7 +13,8 @@ import { Assets } from '../Assets'
import { FilterMaterial } from '../FilteringManager'
export interface MaterialOptions {
gradientIndex?: number
rampIndex?: number
rampTexture?: Texture
}
export default class Materials {
@@ -25,6 +26,7 @@ export default class Materials {
private pointCloudHighlightMaterial: Material = null
private pointHighlightMaterial: Material = null
private meshGradientMaterial: Material = null
private meshColoredMaterial: Material = null
public static renderMaterialFromNode(node: TreeNode): RenderMaterial {
if (!node) return null
@@ -143,7 +145,6 @@ export default class Materials {
this.meshGradientMaterial = new SpeckleStandardColoredMaterial(
{
color: 0x0000ff,
side: DoubleSide,
transparent: false,
opacity: 1,
@@ -155,6 +156,16 @@ export default class Materials {
await Assets.getTexture(defaultGradient)
)
this.meshColoredMaterial = new SpeckleStandardColoredMaterial(
{
side: DoubleSide,
transparent: false,
opacity: 1,
wireframe: false
},
['USE_RTE']
)
this.materialMap[NodeRenderView.NullRenderMaterialHash] =
new SpeckleStandardMaterial(
{
@@ -318,6 +329,19 @@ export default class Materials {
}
}
public getColoredMaterial(renderView: NodeRenderView): Material {
switch (renderView.geometryType) {
case GeometryType.MESH:
return this.meshColoredMaterial
case GeometryType.LINE:
return this.meshColoredMaterial // TO DO
case GeometryType.POINT:
return this.meshColoredMaterial // TO DO
case GeometryType.POINT_CLOUD:
return this.meshColoredMaterial // TO DO
}
}
public getDebugBatchMaterial(renderView: NodeRenderView) {
const color = new Color(MathUtils.randInt(0, 0xffffff))
color.convertSRGBToLinear()
@@ -381,15 +405,16 @@ export default class Materials {
return this.getGhostMaterial(renderView)
case FilterMaterialType.GRADIENT:
return this.getGradientMaterial(renderView)
case FilterMaterialType.COLORED:
return this.getColoredMaterial(renderView)
}
}
public getFilterMaterialOptions(filterMaterial: FilterMaterial) {
return filterMaterial.rampIndex
? {
gradientIndex: filterMaterial.rampIndex
}
: null
return {
rampIndex: filterMaterial.rampIndex ? filterMaterial.rampIndex : undefined,
rampTexture: filterMaterial.rampTexture ? filterMaterial.rampTexture : undefined
}
}
public purge() {