diff --git a/viewer/packages/api/visual-tests/NodeTransform.VisualTest.ts b/viewer/packages/api/visual-tests/NodeTransform.VisualTest.ts index 23dbb9d27c9..6355b1f4bc5 100644 --- a/viewer/packages/api/visual-tests/NodeTransform.VisualTest.ts +++ b/viewer/packages/api/visual-tests/NodeTransform.VisualTest.ts @@ -8,6 +8,7 @@ import { ViewerTestFixtureComponents, ViewerVisualTestFixture } from '../../../visual-tests/test-fixtures/ViewerVisualTestFixture'; +import { NumericRange } from '@reveal/utilities'; export default class NodeTransformVisualTest extends ViewerVisualTestFixture { public async setup(testFixtureComponents: ViewerTestFixtureComponents): Promise { @@ -21,17 +22,14 @@ export default class NodeTransformVisualTest extends ViewerVisualTestFixture { const translation = new THREE.Matrix4().makeTranslation(12, 10, -12); const transform = translation.multiply(rotation.multiply(scale)); - await model.setNodeTransformByTreeIndex(1, transform, false); + model.setNodeTransform(new NumericRange(1, 1), transform); - await Promise.all( - Array.from({ length: 80 - 2 }, (_, k) => k + 2).map(i => { - return model.setNodeTransformByTreeIndex( - i, - new THREE.Matrix4().makeTranslation(0, ((i % 2) * 2 - 1) * 2, 0), - false - ); - }) - ); + Array.from({ length: 80 - 2 }, (_, k) => k + 2).map(i => { + return model.setNodeTransform( + new NumericRange(i, 1), + new THREE.Matrix4().makeTranslation(0, ((i % 2) * 2 - 1) * 2, 0) + ); + }); } return Promise.resolve(); diff --git a/viewer/packages/cad-geometry-loaders/package.json b/viewer/packages/cad-geometry-loaders/package.json index b2979467416..48b826c1ed0 100644 --- a/viewer/packages/cad-geometry-loaders/package.json +++ b/viewer/packages/cad-geometry-loaders/package.json @@ -17,6 +17,7 @@ "@reveal/model-base": "workspace:*", "@reveal/rendering": "workspace:*", "@reveal/sector-loader": "workspace:*", + "@reveal/sector-parser": "workspace:*", "@reveal/utilities": "workspace:*" } } diff --git a/viewer/packages/cad-geometry-loaders/src/CadManager.ts b/viewer/packages/cad-geometry-loaders/src/CadManager.ts index 2ccc8833916..cdf559fca3e 100644 --- a/viewer/packages/cad-geometry-loaders/src/CadManager.ts +++ b/viewer/packages/cad-geometry-loaders/src/CadManager.ts @@ -14,6 +14,7 @@ import { File3dFormat, ModelIdentifier } from '@reveal/data-providers'; import { MetricsLogger } from '@reveal/metrics'; import { CadModelBudget, defaultDesktopCadModelBudget } from './CadModelBudget'; import { CadModelFactory, CadModelSectorLoadStatistics, CadNode, GeometryFilter } from '@reveal/cad-model'; +import { RevealGeometryCollectionType } from '@reveal/sector-parser'; export class CadManager { private readonly _materialManager: CadMaterialManager; @@ -102,6 +103,8 @@ export class CadManager { } this.markNeedsRedraw(); + + this.updateTreeIndexToSectorsMap(cadModel, sector); }; this._subscription.add( @@ -215,4 +218,24 @@ export class CadManager { private handleMaterialsChanged() { this.requestRedraw(); } + + private updateTreeIndexToSectorsMap(cadModel: CadNode, sector: ConsumedSector): void { + if (cadModel.treeIndexToSectorsMap.isCompleted(sector.metadata.id, RevealGeometryCollectionType.TriangleMesh)) { + return; + } + + if (sector.group?.children.length !== 1) { + return; + } + + const treeIndices = sector.group.children[0].userData?.treeIndices as Map | undefined; + if (!treeIndices) { + return; + } + + for (const treeIndex of treeIndices.keys()) { + cadModel.treeIndexToSectorsMap.set(treeIndex, sector.metadata.id); + } + cadModel.treeIndexToSectorsMap.markCompleted(sector.metadata.id, RevealGeometryCollectionType.TriangleMesh); + } } diff --git a/viewer/packages/cad-model/src/batching/MultiBufferBatchingManager.test.ts b/viewer/packages/cad-model/src/batching/MultiBufferBatchingManager.test.ts index 8e38b2b13af..5172d6692f0 100644 --- a/viewer/packages/cad-model/src/batching/MultiBufferBatchingManager.test.ts +++ b/viewer/packages/cad-model/src/batching/MultiBufferBatchingManager.test.ts @@ -6,6 +6,7 @@ import * as THREE from 'three'; import { Mock } from 'moq.ts'; import { Materials, StyledTreeIndexSets } from '@reveal/rendering'; import { MultiBufferBatchingManager } from './MultiBufferBatchingManager'; +import { TreeIndexToSectorsMap } from '../utilities/TreeIndexToSectorsMap'; import sum from 'lodash/sum'; import { IndexSet } from '@reveal/utilities'; @@ -22,7 +23,14 @@ describe(MultiBufferBatchingManager.name, () => { inFront: new IndexSet(), visible: new IndexSet() }; - manager = new MultiBufferBatchingManager(geometryGroup, materials, styledIndexSets, 1024, numberOfInstanceBuffers); + manager = new MultiBufferBatchingManager( + geometryGroup, + materials, + styledIndexSets, + new TreeIndexToSectorsMap(), + 1024, + numberOfInstanceBuffers + ); }); test('batchGeometries() first time adds new geometry to group', () => { diff --git a/viewer/packages/cad-model/src/batching/MultiBufferBatchingManager.ts b/viewer/packages/cad-model/src/batching/MultiBufferBatchingManager.ts index 339c183aad1..50d0f0e43b3 100644 --- a/viewer/packages/cad-model/src/batching/MultiBufferBatchingManager.ts +++ b/viewer/packages/cad-model/src/batching/MultiBufferBatchingManager.ts @@ -14,6 +14,7 @@ import { } from '@reveal/utilities'; import { GeometryBufferUtils } from '../utilities/GeometryBufferUtils'; import { getShaderMaterial } from '../utilities/getShaderMaterial'; +import { TreeIndexToSectorsMap } from '../utilities/TreeIndexToSectorsMap'; import { DrawCallBatchingManager } from './DrawCallBatchingManager'; /** @@ -68,6 +69,7 @@ export class MultiBufferBatchingManager implements DrawCallBatchingManager { batchGroup: Group, materials: Materials, styleTreeIndexSets: StyledTreeIndexSets, + private readonly treeIndexToSectorsMap: TreeIndexToSectorsMap, private readonly initialBufferSize = 1024, private readonly numberOfInstanceBatches = 2 ) { @@ -89,7 +91,7 @@ export class MultiBufferBatchingManager implements DrawCallBatchingManager { if (parsedGeometry.instanceId === undefined) { return; } - this.processGeometries(parsedGeometry as Required, sectorBatch); + this.processGeometries(parsedGeometry as Required, sectorBatch, sectorId); }); } @@ -160,9 +162,10 @@ export class MultiBufferBatchingManager implements DrawCallBatchingManager { } } - private processGeometries(parsedGeometry: Required, sectorBatch: SectorBatch) { + private processGeometries(parsedGeometry: Required, sectorBatch: SectorBatch, sectorId: number) { const instanceBatch = this.getOrCreateInstanceBatch(parsedGeometry); this.batchInstanceAttributes(parsedGeometry.geometryBuffer, parsedGeometry.instanceId, instanceBatch, sectorBatch); + this.updateTreeIndexToSectorsMap(parsedGeometry, sectorId); } private batchInstanceAttributes( @@ -220,6 +223,25 @@ export class MultiBufferBatchingManager implements DrawCallBatchingManager { } } + private updateTreeIndexToSectorsMap(parsedGeometry: ParsedGeometry, sectorId: number) { + const sourceInstanceAttributes = GeometryBufferUtils.getAttributes( + parsedGeometry.geometryBuffer, + InterleavedBufferAttribute + ); + const treeIndexInterleavedAttribute = this.getTreeIndexAttribute(sourceInstanceAttributes); + + if (this.treeIndexToSectorsMap.isCompleted(sectorId, parsedGeometry.type)) { + return; + } + + // Update mapping from tree indices to sector ids + for (let i = 0; i < treeIndexInterleavedAttribute.count; i++) { + const treeIndex = treeIndexInterleavedAttribute.getX(i); + this.treeIndexToSectorsMap.set(treeIndex, sectorId); + } + this.treeIndexToSectorsMap.markCompleted(sectorId, parsedGeometry.type); + } + private reallocateBufferGeometry({ buffer, mesh }: BatchBuffer) { const defragmentedBufferGeometry = this.createDefragmentedBufferGeometry(mesh.geometry, buffer); mesh.geometry.dispose(); diff --git a/viewer/packages/cad-model/src/utilities/CustomSectorBounds.test.ts b/viewer/packages/cad-model/src/utilities/CustomSectorBounds.test.ts new file mode 100644 index 00000000000..062ed3e2376 --- /dev/null +++ b/viewer/packages/cad-model/src/utilities/CustomSectorBounds.test.ts @@ -0,0 +1,408 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import * as THREE from 'three'; +import { CustomSectorBounds } from './CustomSectorBounds'; +import { CadNode } from '../wrappers/CadNode'; +import { Mock } from 'moq.ts'; +import { createV9SectorMetadata } from '../../../../test-utilities'; +import { SectorMetadata } from '@reveal/cad-parsers'; +import { traverseDepthFirst } from '@reveal/utilities'; + +/* + +The following drawing shows the sectors and nodes used in this test. The layout is "2D": All sectors and nodes span z from 0 to 1, and all transforms +are simple translations in x and y. This shouldn't decrease the likelihood of discovering a logical error in the code under test by much, but it makes +it much easier to reason about the expected result of any given test. + +(0,0) (8,0) + ┌───────────────┬───────────────┬─────────────────────────────┬────────────────────────────────────────────────────────────────┐ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ node A │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + ├───────────────┘ │ ┌──────────────┤ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ node B │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ sector 2 │ │ │ │ + ├───────────────────────────────┘ └──────────────┤ │ + │ │ │ + │ │ │ + │ │ │ + │ ┌──────────────┐ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ node C │ │ │ + │ ┌──────┼──────────────┼───────┼────────────────────────────────────────────────────────────────┤ + │ │ │ │ │ sector 3 │ + │ │ │ │ │ │ + │ │ │ │ │ │ + │ │ └──────────────┘ │ │ + │ │ │ │ + │ │ │ │ + │ sector 1 │ │ │ + ├───────────────────────────────┼─────────────────────────────┘ ┌───────────────────────────────────┤ + │ │ │sector 5 │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ┌───────────────────────────────────────────┼───────────────────────────────────┤ + │ │ │sector 4 │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ │ + │ │ │ ┌──────────────┼────────────────┐ │ + │ │ │ │ │ │ │ + │ │ │ │ │ │ │ + │ │ │ │ node D │ │ + │ │ │ │ │ │ │ + │ │ │ │ │ │ │ + │ │ │ └──────────────┼────────────────┘ │ + │ │ │ │ │ + │ │ │ │ │ + │ sector 0 │ │ │ │ + └───────────────────────────────┴──────────────┴───────────────────────────────────────────┴───────────────────────────────────┘ +(0,8) (8,8) + +*/ + +type DummyNode = { + treeIndex: number; + originalBoundingBox: THREE.Box3; +}; + +describe('CustomSectorBounds', () => { + let customSectorBounds: CustomSectorBounds; + let cadNodeMock: Mock; + let sectorMetadataRoot: SectorMetadata; + let sectorMetadataById: Map; + let originalSectorBounds: Map; + + const nodeA: DummyNode = { treeIndex: 1000, originalBoundingBox: boundsFrom(0, 0, 1, 1) }; + const nodeB: DummyNode = { treeIndex: 1001, originalBoundingBox: boundsFrom(3, 1, 4, 2) }; + const nodeC: DummyNode = { treeIndex: 1002, originalBoundingBox: boundsFrom(2.5, 2.5, 3.5, 3.5) }; + const nodeD: DummyNode = { treeIndex: 1003, originalBoundingBox: boundsFrom(5, 6.5, 7, 7.5) }; + + beforeEach(() => { + // Define sectors + sectorMetadataRoot = createV9SectorMetadata([ + 0, + [ + [1, [[2, [], boundsFrom(0, 0, 2, 2)]], boundsFrom(0, 0, 4, 4)], + [ + 3, + [ + [4, [], boundsFrom(3, 6, 8, 8)], + [5, [], boundsFrom(6, 4, 8, 8)] + ], + boundsFrom(2, 3, 8, 8) + ] + ], + boundsFrom(0, 0, 8, 8) + ]); + + // Collect sector metadata into map + sectorMetadataById = new Map(); + traverseDepthFirst(sectorMetadataRoot, x => { + sectorMetadataById.set(x.id, x); + return true; + }); + + // Store copy of sector bounds + originalSectorBounds = new Map(); + for (const [sectorId, sectorMetadata] of sectorMetadataById) { + originalSectorBounds.set(sectorId, sectorMetadata.subtreeBoundingBox.clone()); + } + + // Setup CadNode mock + cadNodeMock = new Mock(); + cadNodeMock.setup(x => x.sectorScene.getSectorById).returns((sectorId: number) => sectorMetadataById.get(sectorId)); + cadNodeMock.setup(x => x.sectorScene.getAllSectors).returns(() => [...sectorMetadataById.values()]); + + customSectorBounds = new CustomSectorBounds(cadNodeMock.object()); + }); + + test('Transform single node', () => { + expect(customSectorBounds.isRegistered(nodeA.treeIndex)).toBeFalse(); + + // Register node A + customSectorBounds.registerTransformedNode(nodeA.treeIndex, nodeA.originalBoundingBox); + customSectorBounds.recomputeSectorBounds(); // Missing sectors and transform, nothing should happen + + expect(customSectorBounds.isRegistered(nodeA.treeIndex)).toBeTrue(); + [0, 1, 2, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + + // Set transform + customSectorBounds.updateNodeTransform(nodeA.treeIndex, translation(-1, 0)); + customSectorBounds.recomputeSectorBounds(); // Still missing sectors, nothing should happen + + [0, 1, 2, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + + // Set sectors + customSectorBounds.updateNodeSectors(nodeA.treeIndex, [2]); + customSectorBounds.recomputeSectorBounds(); // Bounds will be changed now + + expectBoundsApproximatelyEqual(2, boundsFrom(-1, 0, 2, 2)); // Contains the node + expectBoundsApproximatelyEqual(1, boundsFrom(-1, 0, 4, 4)); // Should expand to contain child + expectBoundsApproximatelyEqual(0, boundsFrom(-1, 0, 8, 8)); // Should expand to contain child + [3, 4, 5].forEach(i => expectOriginalBounds(i)); + + // The transform is reset, and everything should revert back to the original values + customSectorBounds.unregisterTransformedNode(nodeA.treeIndex); + customSectorBounds.recomputeSectorBounds(); + + expect(customSectorBounds.isRegistered(nodeA.treeIndex)).toBeFalse(); + [0, 1, 2, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + }); + + test('Transform two nodes that share a sector', () => { + // Transform node A and B, in sector 1 + customSectorBounds.registerTransformedNode(nodeA.treeIndex, nodeA.originalBoundingBox); + customSectorBounds.registerTransformedNode(nodeB.treeIndex, nodeB.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeA.treeIndex, [1]); + customSectorBounds.updateNodeSectors(nodeB.treeIndex, [1]); + customSectorBounds.updateNodeTransform(nodeA.treeIndex, translation(-1, 0)); + customSectorBounds.updateNodeTransform(nodeB.treeIndex, translation(1, 0)); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(1, boundsFrom(-1, 0, 5, 4)); // Contains the nodes + expectBoundsApproximatelyEqual(0, boundsFrom(-1, 0, 8, 8)); // Should expand to contain child + [2, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + + // Transform of node A is reset + customSectorBounds.unregisterTransformedNode(nodeA.treeIndex); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(1, boundsFrom(0, 0, 5, 4)); // Still contains node B + [0, 2, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + + // Transform of node B is reset + customSectorBounds.unregisterTransformedNode(nodeB.treeIndex); + customSectorBounds.recomputeSectorBounds(); + + [0, 1, 2, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + }); + + test('Transform two nodes at different depths, affecting a sector both directly and indirectly', () => { + // Transform node A, which is known to be in sector 2 + customSectorBounds.registerTransformedNode(nodeA.treeIndex, nodeA.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeA.treeIndex, [2]); + customSectorBounds.updateNodeTransform(nodeA.treeIndex, translation(-1, 0)); + + // Transform node B, which is known to be in sector 1 + customSectorBounds.registerTransformedNode(nodeB.treeIndex, nodeB.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeB.treeIndex, [1]); + customSectorBounds.updateNodeTransform(nodeB.treeIndex, translation(1, 0)); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(2, boundsFrom(-1, 0, 2, 2)); // Contains node A + expectBoundsApproximatelyEqual(1, boundsFrom(-1, 0, 5, 4)); // Affected directly by node B, and indirectly by node A (which is in a child sector) + expectBoundsApproximatelyEqual(0, boundsFrom(-1, 0, 8, 8)); // Should expand to contain child + [3, 4, 5].forEach(i => expectOriginalBounds(i)); + + // Transform of node B is reset + customSectorBounds.unregisterTransformedNode(nodeB.treeIndex); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(2, boundsFrom(-1, 0, 2, 2)); // Contains node A + expectBoundsApproximatelyEqual(1, boundsFrom(-1, 0, 4, 4)); // Should expand to contain child + expectBoundsApproximatelyEqual(0, boundsFrom(-1, 0, 8, 8)); // Should expand to contain child + [3, 4, 5].forEach(i => expectOriginalBounds(i)); + + // Transform of node A is reset + customSectorBounds.unregisterTransformedNode(nodeA.treeIndex); + customSectorBounds.recomputeSectorBounds(); + + [0, 1, 2, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + }); + + test('Sector with no overlap with node bounding box should never be affected', () => { + // Transform node A, wrongfully said to contain geometry in sector 3 + customSectorBounds.registerTransformedNode(nodeA.treeIndex, nodeA.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeA.treeIndex, [3]); + customSectorBounds.updateNodeTransform(nodeA.treeIndex, translation(100, 0)); + customSectorBounds.recomputeSectorBounds(); + + [0, 1, 2, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + }); + + test('Sector with partial overlap with node bounding box should only be affect by intersection', () => { + // Transform node C, which is only partially inside sector 3 + customSectorBounds.registerTransformedNode(nodeC.treeIndex, nodeC.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeC.treeIndex, [3]); + customSectorBounds.updateNodeTransform(nodeC.treeIndex, translation(0, -1)); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(3, boundsFrom(2, 2, 8, 8)); // Contains part of node C + [0, 1, 2, 4, 5].forEach(i => expectOriginalBounds(i)); + }); + + test('Transform node present in both parent and child sector', () => { + // Transform node A, in sector 1 and 2 + customSectorBounds.registerTransformedNode(nodeA.treeIndex, nodeA.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeA.treeIndex, [1, 2]); + customSectorBounds.updateNodeTransform(nodeA.treeIndex, translation(-1, 0)); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(2, boundsFrom(-1, 0, 2, 2)); // Contains node A + expectBoundsApproximatelyEqual(1, boundsFrom(-1, 0, 4, 4)); // Contains node A + expectBoundsApproximatelyEqual(0, boundsFrom(-1, 0, 8, 8)); // Should expand to contain child + [3, 4, 5].forEach(i => expectOriginalBounds(i)); + }); + + test('Transform node present in two sibling sectors', () => { + // Transform node D, in sector 4 and 5 + customSectorBounds.registerTransformedNode(nodeD.treeIndex, nodeD.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeD.treeIndex, [4, 5]); + customSectorBounds.updateNodeTransform(nodeD.treeIndex, translation(-4, 0)); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(5, boundsFrom(2, 4, 8, 8)); // Contains node D + expectBoundsApproximatelyEqual(4, boundsFrom(1, 6, 8, 8)); // Contains node D + expectBoundsApproximatelyEqual(3, boundsFrom(1, 3, 8, 8)); // Should expand to contain children + [0, 1, 2].forEach(i => expectOriginalBounds(i)); + }); + + test('Transform node before knowledge of which sectors the node has geometry in', () => { + // Register node A with empty sector set + customSectorBounds.registerTransformedNode(nodeA.treeIndex, nodeA.originalBoundingBox); + customSectorBounds.updateNodeTransform(nodeA.treeIndex, translation(-1, 0)); + customSectorBounds.updateNodeSectors(nodeA.treeIndex, []); + customSectorBounds.recomputeSectorBounds(); + + [0, 1, 2, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + + // Update knowledge of sectors for node A + customSectorBounds.updateNodeSectors(nodeA.treeIndex, [2]); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(2, boundsFrom(-1, 0, 2, 2)); // Contains the node + expectBoundsApproximatelyEqual(1, boundsFrom(-1, 0, 4, 4)); // Should expand to contain child + expectBoundsApproximatelyEqual(0, boundsFrom(-1, 0, 8, 8)); // Should expand to contain child + [3, 4, 5].forEach(i => expectOriginalBounds(i)); + }); + + test('Complex sequence with multiple transforms and nodes', () => { + // Transform node A + customSectorBounds.registerTransformedNode(nodeA.treeIndex, nodeA.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeA.treeIndex, [1, 2]); + customSectorBounds.updateNodeTransform(nodeA.treeIndex, translation(0, -0.5)); + + // Transform node B + customSectorBounds.registerTransformedNode(nodeB.treeIndex, nodeB.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeB.treeIndex, [1]); + customSectorBounds.updateNodeTransform(nodeB.treeIndex, translation(0, -2)); + + // Transform node C + customSectorBounds.registerTransformedNode(nodeC.treeIndex, nodeC.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeC.treeIndex, [1]); + customSectorBounds.updateNodeTransform(nodeC.treeIndex, translation(0, -1)); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(2, boundsFrom(0, -0.5, 2, 2)); // Driven by node A + expectBoundsApproximatelyEqual(1, boundsFrom(0, -1, 4, 4)); // Driven by node B + expectBoundsApproximatelyEqual(0, boundsFrom(0, -1, 8, 8)); // Shold expand to contain sector 1 + [3, 4, 5].forEach(i => expectOriginalBounds(i)); + + // Transform node B again, making sector 1 contract slightly + customSectorBounds.updateNodeTransform(nodeB.treeIndex, translation(0, -1)); + customSectorBounds.recomputeSectorBounds(); + expectBoundsApproximatelyEqual(1, boundsFrom(0, -0.5, 4, 4)); // Should contract because node B is no longer pushing + expectBoundsApproximatelyEqual(0, boundsFrom(0, -0.5, 8, 8)); // Should contract because it can + + // Node C is disovered to have geometry in sector 3 + customSectorBounds.updateNodeSectors(nodeC.treeIndex, [3]); + customSectorBounds.recomputeSectorBounds(); + expectBoundsApproximatelyEqual(3, boundsFrom(2, 2, 8, 8)); // Contains part of node C + + // Transform node D + customSectorBounds.registerTransformedNode(nodeD.treeIndex, nodeD.originalBoundingBox); + customSectorBounds.updateNodeSectors(nodeD.treeIndex, [5]); + customSectorBounds.updateNodeTransform(nodeD.treeIndex, translation(2, 0)); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(5, boundsFrom(6, 4, 9, 8)); // Contains node D + expectBoundsApproximatelyEqual(3, boundsFrom(2, 2, 9, 8)); // Affected by node C and D + expectBoundsApproximatelyEqual(0, boundsFrom(0, -0.5, 9, 8)); // Shold expand to contain sector 3 (and 1 from before) + + // Node D is disovered to have geometry in sector 4 + customSectorBounds.updateNodeSectors(nodeD.treeIndex, [4]); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(4, boundsFrom(3, 6, 9, 8)); + + // Unregister all nodes + customSectorBounds.unregisterTransformedNode(nodeA.treeIndex); + customSectorBounds.unregisterTransformedNode(nodeB.treeIndex); + customSectorBounds.unregisterTransformedNode(nodeC.treeIndex); + customSectorBounds.unregisterTransformedNode(nodeD.treeIndex); + customSectorBounds.recomputeSectorBounds(); + + [0, 1, 2, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + }); + + test('Transform node without knowledge of node bounds', () => { + // Transform node A, without giving node bounds + customSectorBounds.registerTransformedNode(nodeA.treeIndex, undefined); + customSectorBounds.updateNodeSectors(nodeA.treeIndex, [2]); + customSectorBounds.updateNodeTransform(nodeA.treeIndex, translation(1, 0)); + customSectorBounds.recomputeSectorBounds(); + + expectBoundsApproximatelyEqual(2, boundsFrom(0, 0, 3, 2)); // Should expand to guaranteed contain node A, no matter where node A was in the sector + [0, 1, 3, 4, 5].forEach(i => expectOriginalBounds(i)); + }); + + function translation(x: number, y: number): THREE.Matrix4 { + return new THREE.Matrix4().setPosition(x, y, 0); + } + + function boundsFrom(minX: number, minY: number, maxX: number, maxY: number): THREE.Box3 { + return new THREE.Box3().setFromArray([minX, minY, 0, maxX, maxY, 1]); + } + + function expectOriginalBounds(sectorId: number) { + const sectorBounds = sectorMetadataById.get(sectorId)?.subtreeBoundingBox; + const originalBounds = originalSectorBounds.get(sectorId); + if (!sectorBounds || !originalBounds) { + fail(`Failed to get metadata or original bounds for sector ${sectorId}`); + } + + expect(sectorBounds.equals(originalBounds)).toBeTrue(); + } + + function expectBoundsApproximatelyEqual(sectorId: number, expected: THREE.Box3, precision = 3) { + const sectorBounds = sectorMetadataById.get(sectorId)?.subtreeBoundingBox; + if (!sectorBounds) { + fail(`Failed to get metadata for sector ${sectorId}`); + } + + expect(sectorBounds.min.x).toBeCloseTo(expected.min.x, precision); + expect(sectorBounds.min.y).toBeCloseTo(expected.min.y, precision); + expect(sectorBounds.min.z).toBeCloseTo(expected.min.z, precision); + expect(sectorBounds.max.x).toBeCloseTo(expected.max.x, precision); + expect(sectorBounds.max.y).toBeCloseTo(expected.max.y, precision); + expect(sectorBounds.max.z).toBeCloseTo(expected.max.z, precision); + } +}); diff --git a/viewer/packages/cad-model/src/utilities/CustomSectorBounds.ts b/viewer/packages/cad-model/src/utilities/CustomSectorBounds.ts new file mode 100644 index 00000000000..c23f62aeffd --- /dev/null +++ b/viewer/packages/cad-model/src/utilities/CustomSectorBounds.ts @@ -0,0 +1,254 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import * as THREE from 'three'; +import { SectorMetadata } from '@reveal/cad-parsers'; +import { CadNode } from '../wrappers/CadNode'; + +type TransformedNode = { + currentTransform: THREE.Matrix4; + originalBoundingBox?: THREE.Box3; + inSectors: Set; +}; + +/** + * An instance of this class is used to dynamically alter the sector bounding boxes to adapt to custom node transforms. + * The bounding box of a sector is kept equal to its original value, unless: + * - A node with geometry in the sector is transformed such that the original bounds would not fully contain the node geometry + * - Descendants of the sector have grown, and are no longer contained within the original bounds. + * The set of sectors a tree index has geometry in does not need to be known upfront. This set of sectors, and the node transform, + * are set independently of each other. + */ +export class CustomSectorBounds { + private readonly _treeIndexToTransformedNodeMap = new Map(); + private readonly _sectorIdToTransformedNodesMap = new Map>(); + private readonly _originalSectorBounds = new Map(); + private readonly _sectorsWithInvalidBounds = new Set(); + private readonly _allSectorsSortedByDepth: SectorMetadata[]; + + constructor(private readonly cadNode: CadNode) { + // Store all sectors by descending depth, used to iterate over all sectors in a "child before parent"-fashion. + this._allSectorsSortedByDepth = this.cadNode.sectorScene.getAllSectors(); + this._allSectorsSortedByDepth.sort((a, b) => b.depth - a.depth); + } + + /** + * Returns whether or not the given node is registered + * @param treeIndex Tree index of the node to check + * @returns True if node is registered, false otherwise + */ + isRegistered(treeIndex: number): boolean { + return this._treeIndexToTransformedNodeMap.has(treeIndex); + } + + /** + * Registers a node as transformed, meaning it'll be taken into account when sector bounds are recomputed + * @param treeIndex Tree index of the transformed node + * @param originalBoundingBox The original bounding box of this node + */ + registerTransformedNode(treeIndex: number, originalBoundingBox?: THREE.Box3): void { + if (this.isRegistered(treeIndex)) { + throw new Error(`Attempted to register already registered node (tree index ${treeIndex})`); + } + + // Update mapping from tree index to transformed node + this._treeIndexToTransformedNodeMap.set(treeIndex, { + currentTransform: new THREE.Matrix4(), + originalBoundingBox: originalBoundingBox?.clone(), + inSectors: new Set() + }); + } + + /** + * Updates the transform for a registered node. Sector bounds will not be changed until recomputeSectorBounds() is called + * @param treeIndex Tree index of the transformed node + * @param newTransform The transform + */ + updateNodeTransform(treeIndex: number, newTransform: THREE.Matrix4): void { + const transformedNode = this._treeIndexToTransformedNodeMap.get(treeIndex); + if (!transformedNode) { + throw new Error(`Attempted to update transform for non-registered node (tree index ${treeIndex})`); + } + + if (!transformedNode.currentTransform.equals(newTransform)) { + // Update transform + transformedNode.currentTransform.copy(newTransform); + + // Mark affected sectors as dirty, to be updated during the next recomputeSectorBounds() call + transformedNode.inSectors.forEach(sectorId => this._sectorsWithInvalidBounds.add(sectorId)); + } + } + + /** + * Updates the set of sectors a node is known to have geometry in. Addition of new sectors is the only possible operation. + * Sector bounds will not be changed until recomputeSectorBounds() is called + * @param treeIndex Tree index of the transformed node + * @param newSectors The new sector(s) that this node is discovered to have geometry in + */ + updateNodeSectors(treeIndex: number, newSectors: number[]): void { + const transformedNode = this._treeIndexToTransformedNodeMap.get(treeIndex); + if (!transformedNode) { + throw new Error(`Attempted to update node sectors for non-registered node (tree index ${treeIndex})`); + } + + for (const newSector of newSectors) { + if (transformedNode.inSectors.has(newSector)) { + continue; + } + + // Add sector to transformed node + transformedNode.inSectors.add(newSector); + + // Update mapping from sector to transformed nodes + const transformedNodesForSector = this._sectorIdToTransformedNodesMap.get(newSector); + if (transformedNodesForSector) { + transformedNodesForSector.add(transformedNode); + } else { + this._sectorIdToTransformedNodesMap.set(newSector, new Set([transformedNode])); + } + + // Mark sector as dirty, to be updated during the next recomputeSectorBounds() call + this._sectorsWithInvalidBounds.add(newSector); + } + } + + /** + * Unregisters a node, meaning it will no longer be taken into account when sector bounds are recomputed + * @param treeIndex Tree index of the node to be unregistered + */ + unregisterTransformedNode(treeIndex: number): void { + const transformedNode = this._treeIndexToTransformedNodeMap.get(treeIndex); + if (!transformedNode) { + throw new Error(`Attempted to unregister non-registered node (tree index ${treeIndex})`); + } + + // Update mapping from sector id to transformed nodes + transformedNode.inSectors.forEach(sectorId => { + this._sectorIdToTransformedNodesMap.get(sectorId)?.delete(transformedNode); + + // Mark sector as dirty + this._sectorsWithInvalidBounds.add(sectorId); + }); + + // Update mapping from tree index to transformed node + this._treeIndexToTransformedNodeMap.delete(treeIndex); + } + + /** + * Recomputes the sector bounds making all registered nodes fully contained in their respective sectors. This + * is the only time the sector bounds are actually altered. + */ + recomputeSectorBounds(): void { + this._sectorsWithInvalidBounds.forEach(sectorId => { + // Compute the bounding boxes for the transformed nodes in this sector + const boundingBoxes: THREE.Box3[] = []; + this._sectorIdToTransformedNodesMap.get(sectorId)?.forEach(transformedNode => { + // Compute the intersection between the original sector bounds and the original node bounds if available, + // otherwise just assume the node has geometry in the entire sector + const originalSectorBounds = this.getOriginalSectorBounds(sectorId); + const originalBoundingBoxInSector = + transformedNode.originalBoundingBox?.clone().intersect(originalSectorBounds) ?? originalSectorBounds.clone(); + + if (!originalBoundingBoxInSector.isEmpty()) { + // Apply the current transform to this box, giving us the relevant bounds that the sector should currently contain + const effectiveBoundingBox = originalBoundingBoxInSector + .clone() + .applyMatrix4(transformedNode.currentTransform); + + boundingBoxes.push(effectiveBoundingBox); + } + }); + + this.updateSector(sectorId, boundingBoxes); + }); + + this._sectorsWithInvalidBounds.clear(); + + // Iterate over all sectors by descending depth + for (const sector of this._allSectorsSortedByDepth) { + const containsTransformedNodes = !!this._sectorIdToTransformedNodesMap.get(sector.id)?.size; + const originalBoundingBox = this.getOriginalSectorBounds(sector.id); + const minimumBounds = containsTransformedNodes ? sector.subtreeBoundingBox.clone() : originalBoundingBox.clone(); + + sector.children.forEach(child => { + minimumBounds.expandByPoint(child.subtreeBoundingBox.min); + minimumBounds.expandByPoint(child.subtreeBoundingBox.max); + }); + + if (!minimumBounds.equals(originalBoundingBox)) { + this.setCustomSectorBounds(sector.id, minimumBounds); + } else { + this.clearCustomSectorBounds(sector.id); + } + } + } + + /** + * Sets the bounding box of a given sector by expanding the original bounds to include the given custom bounding boxes + * @param sectorId The sector id + * @param customBoundingBoxes An array of bounding boxes this sector should contain + * @returns True if the new sector bounds are different from the original values. False otherwise + */ + private updateSector(sectorId: number, customBoundingBoxes: THREE.Box3[]): void { + const originalSectorBounds = this.getOriginalSectorBounds(sectorId); + + if (customBoundingBoxes.length) { + const newSectorBounds = originalSectorBounds.clone(); + + // Expand to fit all transformed nodes that belong to this sector + customBoundingBoxes.forEach(boundingBox => { + newSectorBounds.expandByPoint(boundingBox.min); + newSectorBounds.expandByPoint(boundingBox.max); + }); + + if (!newSectorBounds.equals(originalSectorBounds)) { + this.setCustomSectorBounds(sectorId, newSectorBounds); + return; + } + } + + // Reset sector bounds back to the original bounds + this.clearCustomSectorBounds(sectorId); + } + + private getOriginalSectorBounds(sectorId: number): THREE.Box3 { + let originalSectorBounds = this._originalSectorBounds.get(sectorId); + if (!originalSectorBounds) { + originalSectorBounds = this.sectorMetadata(sectorId).subtreeBoundingBox.clone(); + } + return originalSectorBounds; + } + + private setCustomSectorBounds(sectorId: number, customBounds: THREE.Box3): void { + const sectorMetadata = this.sectorMetadata(sectorId); + + const existingOriginal = this._originalSectorBounds.get(sectorId); + if (!existingOriginal) { + // If no original is stored, the sector must currently have original bounds + this._originalSectorBounds.set(sectorId, sectorMetadata.subtreeBoundingBox.clone()); + } + + // Modify sector bounds + sectorMetadata.subtreeBoundingBox.copy(customBounds); + } + + private clearCustomSectorBounds(sectorId: number): void { + const originalBounds = this._originalSectorBounds.get(sectorId); + if (originalBounds) { + // Modify sector bounds + this.sectorMetadata(sectorId).subtreeBoundingBox.copy(originalBounds); + + // Remove copy of original bounds + this._originalSectorBounds.delete(sectorId); + } + } + + private sectorMetadata(sectorId: number): SectorMetadata { + const sectorMetadata = this.cadNode.sectorScene.getSectorById(sectorId); + if (!sectorMetadata) { + throw new Error(`Failed to get sector metadata for sector with id ${sectorId}`); + } + return sectorMetadata; + } +} diff --git a/viewer/packages/cad-model/src/utilities/TreeIndexToSectorsMap.test.ts b/viewer/packages/cad-model/src/utilities/TreeIndexToSectorsMap.test.ts new file mode 100644 index 00000000000..4ae00e25aac --- /dev/null +++ b/viewer/packages/cad-model/src/utilities/TreeIndexToSectorsMap.test.ts @@ -0,0 +1,41 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { RevealGeometryCollectionType } from '@reveal/sector-parser'; +import { TreeIndexToSectorsMap } from './TreeIndexToSectorsMap'; +import { jest } from '@jest/globals'; + +describe('TreeIndexToSectorsMap', () => { + let map: TreeIndexToSectorsMap; + + beforeEach(() => { + map = new TreeIndexToSectorsMap(); + map.onChange = jest.fn(); + }); + + test('maps from tree index to set of sectors', () => { + expect(Array.from(map.getSectorIdsForTreeIndex(100))).toIncludeSameMembers([]); + + map.set(100, 1); + map.set(100, 2); + map.set(100, 3); + + expect(Array.from(map.getSectorIdsForTreeIndex(100))).toIncludeSameMembers([1, 2, 3]); + }); + + test('fires callback when tree index is found in a new sector', () => { + map.set(100, 1); // New + expect(map.onChange).toHaveBeenLastCalledWith(100, 1); + map.set(100, 1); // Not new + expect(map.onChange).toHaveBeenCalledTimes(1); + map.set(100, 2); // New + expect(map.onChange).toHaveBeenLastCalledWith(100, 2); + }); + + test('should keep track of which geometry types that are completed for each sector', () => { + expect(map.isCompleted(1, RevealGeometryCollectionType.BoxCollection)).toBeFalse(); + map.markCompleted(1, RevealGeometryCollectionType.BoxCollection); + expect(map.isCompleted(1, RevealGeometryCollectionType.BoxCollection)).toBeTrue(); + }); +}); diff --git a/viewer/packages/cad-model/src/utilities/TreeIndexToSectorsMap.ts b/viewer/packages/cad-model/src/utilities/TreeIndexToSectorsMap.ts new file mode 100644 index 00000000000..bb1e115b69b --- /dev/null +++ b/viewer/packages/cad-model/src/utilities/TreeIndexToSectorsMap.ts @@ -0,0 +1,73 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { RevealGeometryCollectionType } from '@reveal/sector-parser'; + +/** + * Map between a tree index and the set of sectors it has geometry in. Also contains helper functions to keep track of whether or not + * a given sector has had all its tree indices added to the map. This means iterating over all tree indices in a sector will only be + * done on first load. + */ +export class TreeIndexToSectorsMap { + public onChange?: (treeIndex: number, newSectorId: number) => void; + private readonly _treeIndexToSectorIds = new Map>(); + private readonly _parsedSectors = new Map>(); + + /** + * Store the fact that a tree index is found to have geometry in a certain sector + * @param treeIndex Tree index + * @param sectorId The sector id where the tree index was found + */ + set(treeIndex: number, sectorId: number): void { + const existingSet = this._treeIndexToSectorIds.get(treeIndex); + if (!existingSet) { + this._treeIndexToSectorIds.set(treeIndex, new Set([sectorId])); + this.onChange?.(treeIndex, sectorId); + return; + } + + if (!existingSet.has(sectorId)) { + existingSet.add(sectorId); + this.onChange?.(treeIndex, sectorId); + } + } + + /** + * Get the set of sectors a tree index is known to have geometry in + * @param treeIndex Tree index + * @returns The set of sectors + */ + getSectorIdsForTreeIndex(treeIndex: number): Set { + return this._treeIndexToSectorIds.get(treeIndex) ?? new Set(); + } + + /** + * Mark a sector as completed for a given geometry type. This will make subsequent calls to isCompleted + * for this geometry type return true + * @param sectorId The sector id + * @param type The geometry type + */ + markCompleted(sectorId: number, type: RevealGeometryCollectionType): void { + const existingSet = this._parsedSectors.get(sectorId); + if (existingSet) { + existingSet.add(sectorId); + } else { + this._parsedSectors.set(sectorId, new Set([type])); + } + } + + /** + * Check whether or not a sector is completely processed, for a given geometry type + * @param sectorId The sector id + * @param type The geometry type + * @returns True if completed, false otherwise + */ + isCompleted(sectorId: number, type: RevealGeometryCollectionType): boolean { + const parsedTypes = this._parsedSectors.get(sectorId); + if (parsedTypes) { + return parsedTypes.has(type); + } + return false; + } +} diff --git a/viewer/packages/cad-model/src/wrappers/CadNode.ts b/viewer/packages/cad-model/src/wrappers/CadNode.ts index eb27ba4e1c5..d16ac0fe4da 100644 --- a/viewer/packages/cad-model/src/wrappers/CadNode.ts +++ b/viewer/packages/cad-model/src/wrappers/CadNode.ts @@ -17,6 +17,7 @@ import { import { Group, Object3D, Plane, Matrix4 } from 'three'; import { DrawCallBatchingManager } from '../batching/DrawCallBatchingManager'; import { MultiBufferBatchingManager } from '../batching/MultiBufferBatchingManager'; +import { TreeIndexToSectorsMap } from '../utilities/TreeIndexToSectorsMap'; export class CadNode extends Object3D { private readonly _cadModelMetadata: CadModelMetadata; @@ -38,6 +39,8 @@ export class CadNode extends Object3D { private _needsRedraw: boolean = false; + public readonly treeIndexToSectorsMap = new TreeIndexToSectorsMap(); + public readonly type = 'CadNode'; constructor(model: CadModelMetadata, materialManager: CadMaterialManager, sectorRepository: SectorRepository) { @@ -65,7 +68,8 @@ export class CadNode extends Object3D { this._geometryBatchingManager = new MultiBufferBatchingManager( this._batchedGeometryMeshGroup, materials, - this._styledTreeIndexSets + this._styledTreeIndexSets, + this.treeIndexToSectorsMap ); this._rootSector = new RootSectorNode(model); diff --git a/viewer/packages/cad-model/src/wrappers/CogniteCadModel.ts b/viewer/packages/cad-model/src/wrappers/CogniteCadModel.ts index 3406be34c13..69f66bf3739 100644 --- a/viewer/packages/cad-model/src/wrappers/CogniteCadModel.ts +++ b/viewer/packages/cad-model/src/wrappers/CogniteCadModel.ts @@ -16,6 +16,7 @@ import { NodeAppearance, NodeCollection, CdfModelNodeCollectionDataProvider } fr import { NodeIdAndTreeIndexMaps } from '../utilities/NodeIdAndTreeIndexMaps'; import { CadNode } from './CadNode'; import { WellKnownUnit } from '../types'; +import { CustomSectorBounds } from '../utilities/CustomSectorBounds'; /** * Represents a single 3D CAD model loaded from CDF. @@ -84,6 +85,7 @@ export class CogniteCadModel implements CdfModelNodeCollectionDataProvider { private readonly nodesApiClient: NodesApiClient; private readonly nodeIdAndTreeIndexMaps: NodeIdAndTreeIndexMaps; private readonly _styledNodeCollections: { nodeCollection: NodeCollection; appearance: NodeAppearance }[] = []; + private readonly customSectorBounds: CustomSectorBounds; /** * @param modelId @@ -100,6 +102,12 @@ export class CogniteCadModel implements CdfModelNodeCollectionDataProvider { this.nodeIdAndTreeIndexMaps = new NodeIdAndTreeIndexMaps(modelId, revisionId, this.nodesApiClient); this.cadNode = cadNode; + this.customSectorBounds = new CustomSectorBounds(this.cadNode); + this.cadNode.treeIndexToSectorsMap.onChange = (treeIndex: number, newSectorId: number) => { + if (this.customSectorBounds.isRegistered(treeIndex)) { + this.customSectorBounds.updateNodeSectors(treeIndex, [newSectorId]); + } + }; } /** @@ -200,10 +208,47 @@ export class CogniteCadModel implements CdfModelNodeCollectionDataProvider { * node isn't supported and might lead to undefined results. * @param treeIndices Tree indices of nodes to apply the transformation to. * @param transformMatrix Transformation to apply. + * @param boundingBox Optional bounding box for the nodes before any transformation is applied. If given, it is assumed that all the nodes' geometry fit inside. */ - setNodeTransform(treeIndices: NumericRange, transformMatrix: THREE.Matrix4): void { + setNodeTransform(treeIndices: NumericRange, transformMatrix: THREE.Matrix4, boundingBox?: THREE.Box3): void { MetricsLogger.trackCadNodeTransformOverridden(treeIndices.count, transformMatrix); this.nodeTransformProvider.setNodeTransform(treeIndices, transformMatrix); + + // Metadata bounding boxes are in CDF space. Precompute the necessary transformations once. + const cdfToModelTransform = this.getModelTransformation() + .clone() + .multiply(this.getCdfToDefaultModelTransformation()); + const modelToCdfTransform = cdfToModelTransform.clone().invert(); + + // Convert the transform to CDF space + const transformMatrixCdf = modelToCdfTransform.clone().multiply(transformMatrix).multiply(cdfToModelTransform); + + // Transform bounding box to CDF space, if given + let nodeBoundingBox: THREE.Box3 | undefined; + if (boundingBox) { + nodeBoundingBox = boundingBox.clone(); + nodeBoundingBox.applyMatrix4(modelToCdfTransform); + } + + // Update sector bounds + for (const treeIndex of treeIndices.toArray()) { + if (!this.customSectorBounds.isRegistered(treeIndex)) { + // Register node as transformed + this.customSectorBounds.registerTransformedNode(treeIndex, nodeBoundingBox); + + // Get the sectors that this node is currently known to have geometry in. As the mapping from tree index to sectors is built + // when sectors are loaded, this node may have geometry in more sectors than what is currently known. If new sectors with geometry + // from this node are discovered at a later point, customSectorBounds.updateNodeSectors will be called through the + // treeIndexToSectorsMap.onChange callback, which is setup in the constructor. + const sectorIds = this.cadNode.treeIndexToSectorsMap.getSectorIdsForTreeIndex(treeIndex); + if (sectorIds.size) { + this.customSectorBounds.updateNodeSectors(treeIndex, Array.from(sectorIds)); + } + } + + this.customSectorBounds.updateNodeTransform(treeIndex, transformMatrixCdf); + } + this.customSectorBounds.recomputeSectorBounds(); } /** @@ -218,7 +263,8 @@ export class CogniteCadModel implements CdfModelNodeCollectionDataProvider { applyToChildren = true ): Promise { const treeIndices = await this.determineTreeIndices(treeIndex, applyToChildren); - this.setNodeTransform(treeIndices, transform); + const boundingBox = await this.getBoundingBoxByTreeIndex(treeIndex); + await this.setNodeTransform(treeIndices, transform, boundingBox); return treeIndices.count; } @@ -228,6 +274,10 @@ export class CogniteCadModel implements CdfModelNodeCollectionDataProvider { */ resetNodeTransform(treeIndices: NumericRange): void { this.nodeTransformProvider.resetNodeTransform(treeIndices); + + // Update sector bounds + treeIndices.forEach(treeIndex => this.customSectorBounds.unregisterTransformedNode(treeIndex)); + this.customSectorBounds.recomputeSectorBounds(); } /** diff --git a/viewer/reveal.api.md b/viewer/reveal.api.md index fb5757c4143..23088e5fdec 100644 --- a/viewer/reveal.api.md +++ b/viewer/reveal.api.md @@ -473,7 +473,7 @@ export class CogniteCadModel implements CdfModelNodeCollectionDataProvider { setDefaultNodeAppearance(appearance: NodeAppearance): void; setModelClippingPlanes(clippingPlanes: THREE_2.Plane[]): void; setModelTransformation(matrix: THREE_2.Matrix4): void; - setNodeTransform(treeIndices: NumericRange, transformMatrix: THREE_2.Matrix4): void; + setNodeTransform(treeIndices: NumericRange, transformMatrix: THREE_2.Matrix4, boundingBox?: THREE_2.Box3): void; setNodeTransformByTreeIndex(treeIndex: number, transform: THREE_2.Matrix4, applyToChildren?: boolean): Promise; get styledNodeCollections(): { nodeCollection: NodeCollection; diff --git a/viewer/yarn.lock b/viewer/yarn.lock index 181c15d74d8..5be404471af 100644 --- a/viewer/yarn.lock +++ b/viewer/yarn.lock @@ -1622,6 +1622,7 @@ __metadata: "@reveal/model-base": "workspace:*" "@reveal/rendering": "workspace:*" "@reveal/sector-loader": "workspace:*" + "@reveal/sector-parser": "workspace:*" "@reveal/utilities": "workspace:*" languageName: unknown linkType: soft