From 18ab05b14009d1b9d1e136b046393a3da05b33c5 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Wed, 17 Jul 2024 17:13:55 +0300 Subject: [PATCH 1/4] First draft of DUI3 instancing --- packages/viewer-sandbox/src/main.ts | 7 +- packages/viewer/src/IViewer.ts | 1 + .../src/modules/loaders/GeometryConverter.ts | 1 + .../loaders/Speckle/SpeckleConverter.ts | 107 +++++++++++++++++- .../Speckle/SpeckleGeometryConverter.ts | 7 ++ .../modules/loaders/Speckle/SpeckleLoader.ts | 2 + packages/viewer/src/modules/tree/WorldTree.ts | 8 +- 7 files changed, 129 insertions(+), 4 deletions(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 07f8b15e7..21614dedb 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -391,7 +391,12 @@ const getStream = () => { // Rhino sRGB vertex colors // 'https://app.speckle.systems/projects/47bbaf594f/models/ef78e94f72' // qGIS sRGB vertex colors - 'https://latest.speckle.systems/projects/5a6609a4b9/models/10f4931e8c' + // 'https://latest.speckle.systems/projects/5a6609a4b9/models/10f4931e8c' + // DUI3 blocks + // 'https://latest.speckle.systems/projects/126cd4b7bb/models/c6f3f309a2' + 'https://latest.speckle.systems/projects/126cd4b7bb/models/6b62c61a22' + // 'https://latest.speckle.systems/projects/126cd4b7bb/models/0a6a715a0a' + // 'https://latest.speckle.systems/projects/126cd4b7bb/models/4dc5265453' ) } diff --git a/packages/viewer/src/IViewer.ts b/packages/viewer/src/IViewer.ts index e6d7cacb0..8b77845d6 100644 --- a/packages/viewer/src/IViewer.ts +++ b/packages/viewer/src/IViewer.ts @@ -25,6 +25,7 @@ export type SpeckleObject = { name?: string referencedId?: string units?: string + applicationId?: string } export interface ViewerParams { diff --git a/packages/viewer/src/modules/loaders/GeometryConverter.ts b/packages/viewer/src/modules/loaders/GeometryConverter.ts index 424aed286..906626d29 100644 --- a/packages/viewer/src/modules/loaders/GeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/GeometryConverter.ts @@ -19,6 +19,7 @@ export enum SpeckleType { RevitInstance = 'RevitInstance', Text = 'Text', Transform = 'Transform', + InstanceProxy = 'InstanceProxy', Unknown = 'Unknown' } diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts index bcf4c3e65..28920af01 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { MathUtils } from 'three' +import { MathUtils, Matrix4 } from 'three' import { type TreeNode, WorldTree } from '../../tree/WorldTree.js' import { NodeMap } from '../../tree/NodeMap.js' -import type { SpeckleObject } from '../../../index.js' +import { SpeckleType, type SpeckleObject } from '../../../index.js' import type ObjectLoader from '@speckle/objectloader' import Logger from '../../utils/Logger.js' @@ -22,6 +22,8 @@ export default class SpeckleConverter { private spoofIDs = false private tree: WorldTree private typeLookupTable: { [type: string]: string } = {} + private instanceDefinitionLookupTable: { [id: string]: TreeNode } = {} + private instancedObjectsLookupTable: { [id: string]: SpeckleObject } = {} private instanceCounter = 0 private readonly NodeConverterMapping: { @@ -44,6 +46,8 @@ export default class SpeckleConverter { RevitInstance: this.RevitInstanceToNode.bind(this), Text: this.TextToNode.bind(this), Dimension: this.DimensionToNode.bind(this), + InstanceDefinitionProxy: this.InstanceDefinitionProxyToNode.bind(this), + InstanceProxy: this.InstanceProxyToNode.bind(this), Parameter: null } @@ -521,6 +525,105 @@ export default class SpeckleConverter { } } + private async InstanceDefinitionProxyToNode(obj: SpeckleObject, node: TreeNode) { + if (!obj.applicationId) { + Logger.warn(`Instance Definition Proxy ${obj.id} has no applicationId`) + return + } + this.instanceDefinitionLookupTable[obj.applicationId] = node + } + + private getInstanceProxyDefinitionId(obj: SpeckleObject): string { + return (obj.DefinitionId || obj.definitionId) as string + } + + private getInstanceProxyTransform(obj: SpeckleObject): Array { + if (!(obj.transform || obj.Transform)) { + return new Matrix4().toArray() + } + return (obj.transform || obj.Transform) as Array + } + + private createTransformNode(obj: SpeckleObject) { + const transformNodeId = MathUtils.generateUUID() + const transformData = this.getEmptyTransformData(transformNodeId) + transformData.units = obj.units as string + transformData.matrix = this.getInstanceProxyTransform(obj) + return this.tree.parse({ + id: transformNodeId, + raw: transformData, + atomic: false, + children: [] + }) + } + + private async InstanceProxyToNode(obj: SpeckleObject, node: TreeNode) { + return + } + + private async ConvertInstanceProxyToNode(obj: SpeckleObject, node: TreeNode) { + const definitionId = this.getInstanceProxyDefinitionId(obj) + if (!definitionId) { + Logger.warn(`Instance Proxy ${obj.id} has no definitionId`) + return + } + const definition = this.instanceDefinitionLookupTable[definitionId] + const transformNode = this.createTransformNode(obj) + + this.tree.addNode(transformNode, node) + const objectApplicationIds = definition.model.raw.Objects + for (const objectApplicationId of objectApplicationIds) { + const speckleData = this.instancedObjectsLookupTable[objectApplicationId] + const instancedNode = this.tree.parse({ + id: this.getCompoundId(speckleData.id, this.instanceCounter++), + raw: speckleData, + atomic: false, + children: [], + instanced: true + }) + this.tree.addNode(instancedNode, transformNode) + await this.convertToNode(speckleData, instancedNode) + } + } + + public async convertInstances() { + // uh, oh + this.NodeConverterMapping.InstanceProxy = this.ConvertInstanceProxyToNode.bind(this) + for (const k in this.instanceDefinitionLookupTable) { + const definition = this.instanceDefinitionLookupTable[k] + const objectApplicationIds = definition.model.raw.Objects + for (const objectApplicationId of objectApplicationIds) { + const objectNodes = this.tree.findAll((node: TreeNode) => { + // String vs int + return ( + node.model.raw.applicationId === Number.parseInt(objectApplicationId) || + node.model.raw.applicationId === objectApplicationId + ) + }) + const objectNode = objectNodes[0] + this.instancedObjectsLookupTable[objectApplicationId] = objectNode.model.raw + + this.tree.removeNode(objectNode, true) + } + } + const plm: TreeNode[] = [] + await this.tree.walkAsync((node: TreeNode) => { + const type = this.getSpeckleType(node.model.raw) + if (type !== SpeckleType.InstanceProxy) { + return true + } + plm.push(node) + return true + }) + for (let i = 0; i < plm.length; i++) { + await this.convertToNode(plm[i].model.raw, plm[i]) + } + // for (let k = 0; k < this.instanceProxies.length; k++) { + // const node = this.instanceProxies[k] + // await this.convertToNode(node.model.raw, node) + // } + } + private async PointcloudToNode(obj: SpeckleObject, node: TreeNode) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts index 229e5cdbc..bff206219 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts @@ -70,6 +70,8 @@ export class SpeckleGeometryConverter extends GeometryConverter { return this.TextToGeometryData(node) case SpeckleType.Transform: return this.TransformToGeometryData(node) + case SpeckleType.InstanceProxy: + return this.InstanceProxyToGeometyData(node) case SpeckleType.Unknown: // console.warn(`Skipping geometry conversion for ${type}`) return null @@ -176,6 +178,11 @@ export class SpeckleGeometryConverter extends GeometryConverter { return null } + private InstanceProxyToGeometyData(node: NodeData): GeometryData | null { + node + return null + } + /** * POINT CLOUD */ diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts index 398079f2c..ac01ffa64 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts @@ -128,6 +128,8 @@ export class SpeckleLoader extends Loader { return Promise.resolve(false) } + await this.converter.convertInstances() + const t0 = performance.now() const geometryConverter = new SpeckleGeometryConverter() diff --git a/packages/viewer/src/modules/tree/WorldTree.ts b/packages/viewer/src/modules/tree/WorldTree.ts index 9bc101965..b58d8f1d7 100644 --- a/packages/viewer/src/modules/tree/WorldTree.ts +++ b/packages/viewer/src/modules/tree/WorldTree.ts @@ -108,8 +108,14 @@ export class WorldTree { if (this.nodeMaps[parent.model.subtreeId]?.addNode(node)) parent.addChild(node) } - public removeNode(node: TreeNode): void { + public removeNode(node: TreeNode, removeChildren: boolean): void { + const children = node.children + this.nodeMaps[node.model.subtreeId]?.removeNode(node) node.drop() + if (!removeChildren || !children) return + for (let k = 0; k < children.length; k++) { + this.removeNode(children[k], removeChildren) + } } public findAll(predicate: SearchPredicate, node?: TreeNode): Array { From 2bd3d5843ed78105903fb7a9fcb432e09f3193c8 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Wed, 17 Jul 2024 19:19:18 +0300 Subject: [PATCH 2/4] More efficient and straightforward DUI3 instancing --- packages/viewer-sandbox/src/main.ts | 1 - .../loaders/Speckle/SpeckleConverter.ts | 78 ++++++++++++------- packages/viewer/src/modules/tree/WorldTree.ts | 3 +- 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 21614dedb..62277189a 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -395,7 +395,6 @@ const getStream = () => { // DUI3 blocks // 'https://latest.speckle.systems/projects/126cd4b7bb/models/c6f3f309a2' 'https://latest.speckle.systems/projects/126cd4b7bb/models/6b62c61a22' - // 'https://latest.speckle.systems/projects/126cd4b7bb/models/0a6a715a0a' // 'https://latest.speckle.systems/projects/126cd4b7bb/models/4dc5265453' ) } diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts index 28920af01..f9c495a5f 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts @@ -24,6 +24,7 @@ export default class SpeckleConverter { private typeLookupTable: { [type: string]: string } = {} private instanceDefinitionLookupTable: { [id: string]: TreeNode } = {} private instancedObjectsLookupTable: { [id: string]: SpeckleObject } = {} + private instanceProxies: { [id: string]: TreeNode } = {} private instanceCounter = 0 private readonly NodeConverterMapping: { @@ -533,6 +534,15 @@ export default class SpeckleConverter { this.instanceDefinitionLookupTable[obj.applicationId] = node } + private async InstanceProxyToNode(obj: SpeckleObject, node: TreeNode) { + if (!obj.applicationId) { + Logger.warn(`Instance proxy ${obj.id} has no application id`) + return + } + this.instanceProxies[obj.applicationId] = node + return + } + private getInstanceProxyDefinitionId(obj: SpeckleObject): string { return (obj.DefinitionId || obj.definitionId) as string } @@ -557,10 +567,6 @@ export default class SpeckleConverter { }) } - private async InstanceProxyToNode(obj: SpeckleObject, node: TreeNode) { - return - } - private async ConvertInstanceProxyToNode(obj: SpeckleObject, node: TreeNode) { const definitionId = this.getInstanceProxyDefinitionId(obj) if (!definitionId) { @@ -587,41 +593,55 @@ export default class SpeckleConverter { } public async convertInstances() { - // uh, oh + /** uh, oh */ this.NodeConverterMapping.InstanceProxy = this.ConvertInstanceProxyToNode.bind(this) + + /** Find the nodes that need to be 'consumed' */ + const consumeApplicationIds: { [id: string]: TreeNode | null } = {} + let consumeApplicationIdsCount = 0 for (const k in this.instanceDefinitionLookupTable) { const definition = this.instanceDefinitionLookupTable[k] - const objectApplicationIds = definition.model.raw.Objects - for (const objectApplicationId of objectApplicationIds) { - const objectNodes = this.tree.findAll((node: TreeNode) => { - // String vs int - return ( - node.model.raw.applicationId === Number.parseInt(objectApplicationId) || - node.model.raw.applicationId === objectApplicationId - ) - }) - const objectNode = objectNodes[0] - this.instancedObjectsLookupTable[objectApplicationId] = objectNode.model.raw - - this.tree.removeNode(objectNode, true) + const objects = definition.model.raw.Objects as string[] + for (let i = 0; i < objects.length; i++) { + consumeApplicationIds[objects[i].toString()] = null + consumeApplicationIdsCount++ } } - const plm: TreeNode[] = [] + /** Do a short async walk */ await this.tree.walkAsync((node: TreeNode) => { - const type = this.getSpeckleType(node.model.raw) - if (type !== SpeckleType.InstanceProxy) { - return true + if (!node.model.raw.applicationId) return true + const applicationId = node.model.raw.applicationId.toString() + if (consumeApplicationIds[applicationId] !== undefined) { + consumeApplicationIds[applicationId] = node + consumeApplicationIdsCount-- } - plm.push(node) + /** Break out when all applicationIds are accounted for*/ + if (consumeApplicationIdsCount === 0) return false return true }) - for (let i = 0; i < plm.length; i++) { - await this.convertToNode(plm[i].model.raw, plm[i]) + + /** Consume them */ + for (const k in consumeApplicationIds) { + const objectNode = consumeApplicationIds[k] + if (!objectNode) { + Logger.error(`Consumable applicationId ${k} could not be found`) + continue + } + + /** Store the speckle object data */ + this.instancedObjectsLookupTable[k] = objectNode.model.raw + /** Remove the instance from the list (if needed) */ + delete this.instanceProxies[k] + /** Remove the node from the world tree */ + this.tree.removeNode(objectNode, true) + } + + /** Remaining instance proxies should all be valid */ + for (const k in this.instanceProxies) { + const node = this.instanceProxies[k] + /** Create the final instances */ + await this.convertToNode(node.model.raw, node) } - // for (let k = 0; k < this.instanceProxies.length; k++) { - // const node = this.instanceProxies[k] - // await this.convertToNode(node.model.raw, node) - // } } private async PointcloudToNode(obj: SpeckleObject, node: TreeNode) { diff --git a/packages/viewer/src/modules/tree/WorldTree.ts b/packages/viewer/src/modules/tree/WorldTree.ts index b58d8f1d7..946516a5c 100644 --- a/packages/viewer/src/modules/tree/WorldTree.ts +++ b/packages/viewer/src/modules/tree/WorldTree.ts @@ -203,7 +203,8 @@ export class WorldTree { if (subtreeNode) { this.nodeMaps[subtreeNode[0].model.subtreeId].purge() delete this.nodeMaps[subtreeNode[0].model.subtreeId] - this.removeNode(subtreeNode[0]) + // Potentially true? + this.removeNode(subtreeNode[0], false) } return } From 393d08d7af667abfd743773cdd9cf41da8012dfd Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Wed, 17 Jul 2024 19:31:13 +0300 Subject: [PATCH 3/4] FIxed linting error --- packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts index f9c495a5f..88b3c33f9 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts @@ -2,7 +2,7 @@ import { MathUtils, Matrix4 } from 'three' import { type TreeNode, WorldTree } from '../../tree/WorldTree.js' import { NodeMap } from '../../tree/NodeMap.js' -import { SpeckleType, type SpeckleObject } from '../../../index.js' +import { type SpeckleObject } from '../../../index.js' import type ObjectLoader from '@speckle/objectloader' import Logger from '../../utils/Logger.js' From 17795259ef219aecc75e3fe8efa5da0da08a6f11 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Thu, 18 Jul 2024 10:39:44 +0300 Subject: [PATCH 4/4] Support for lower case 'objects' --- .../src/modules/loaders/Speckle/SpeckleConverter.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts index 88b3c33f9..334e2cc32 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts @@ -554,6 +554,10 @@ export default class SpeckleConverter { return (obj.transform || obj.Transform) as Array } + private getInstanceProxyDefinitionObjects(obj: SpeckleObject): Array { + return (obj.Objects || obj.objects) as Array + } + private createTransformNode(obj: SpeckleObject) { const transformNodeId = MathUtils.generateUUID() const transformData = this.getEmptyTransformData(transformNodeId) @@ -577,7 +581,9 @@ export default class SpeckleConverter { const transformNode = this.createTransformNode(obj) this.tree.addNode(transformNode, node) - const objectApplicationIds = definition.model.raw.Objects + const objectApplicationIds = this.getInstanceProxyDefinitionObjects( + definition.model.raw + ) for (const objectApplicationId of objectApplicationIds) { const speckleData = this.instancedObjectsLookupTable[objectApplicationId] const instancedNode = this.tree.parse({ @@ -601,7 +607,7 @@ export default class SpeckleConverter { let consumeApplicationIdsCount = 0 for (const k in this.instanceDefinitionLookupTable) { const definition = this.instanceDefinitionLookupTable[k] - const objects = definition.model.raw.Objects as string[] + const objects = this.getInstanceProxyDefinitionObjects(definition.model.raw) for (let i = 0; i < objects.length; i++) { consumeApplicationIds[objects[i].toString()] = null consumeApplicationIdsCount++