#827. Implemented non-numeric filter-by properties support. Added this functionality to the sandbox as well.
This commit is contained in:
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user