From 30e985be4acb97c667a2a73343e5f73e5b968a7d Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Fri, 22 Jul 2022 15:35:34 +0300 Subject: [PATCH] #827. Implemented non-numeric filter-by properties support. Added this functionality to the sandbox as well. --- packages/viewer-sandbox/src/Sandbox.ts | 130 +++++++++++------ packages/viewer/src/modules/Assets.ts | 11 ++ packages/viewer/src/modules/Viewer.ts | 133 ++++++++++++++++-- .../viewer/src/modules/batching/Batcher.ts | 16 ++- .../viewer/src/modules/batching/MeshBatch.ts | 10 +- .../viewer/src/modules/materials/Materials.ts | 41 ++++-- 6 files changed, 266 insertions(+), 75 deletions(-) diff --git a/packages/viewer-sandbox/src/Sandbox.ts b/packages/viewer-sandbox/src/Sandbox.ts index 4367c5183..037d16b4e 100644 --- a/packages/viewer-sandbox/src/Sandbox.ts +++ b/packages/viewer-sandbox/src/Sandbox.ts @@ -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() }) } diff --git a/packages/viewer/src/modules/Assets.ts b/packages/viewer/src/modules/Assets.ts index 1cae882b1..e16535494 100644 --- a/packages/viewer/src/modules/Assets.ts +++ b/packages/viewer/src/modules/Assets.ts @@ -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 } } diff --git a/packages/viewer/src/modules/Viewer.ts b/packages/viewer/src/modules/Viewer.ts index 65c9cfedf..7ff90c5da 100644 --- a/packages/viewer/src/modules/Viewer.ts +++ b/packages/viewer/src/modules/Viewer.ts @@ -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 :) } diff --git a/packages/viewer/src/modules/batching/Batcher.ts b/packages/viewer/src/modules/batching/Batcher.ts index d2d75322b..f2f84fac3 100644 --- a/packages/viewer/src/modules/batching/Batcher.ts +++ b/packages/viewer/src/modules/batching/Batcher.ts @@ -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) diff --git a/packages/viewer/src/modules/batching/MeshBatch.ts b/packages/viewer/src/modules/batching/MeshBatch.ts index 8a30a673e..702847d2e 100644 --- a/packages/viewer/src/modules/batching/MeshBatch.ts +++ b/packages/viewer/src/modules/batching/MeshBatch.ts @@ -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) { diff --git a/packages/viewer/src/modules/materials/Materials.ts b/packages/viewer/src/modules/materials/Materials.ts index 0bee16d0c..76f58a80b 100644 --- a/packages/viewer/src/modules/materials/Materials.ts +++ b/packages/viewer/src/modules/materials/Materials.ts @@ -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() {