import { useQuery } from '@tanstack/react-query'; import type { DMInstanceRef } from '@cognite/reveal'; import type { Node3D, CogniteClient } from '@cognite/sdk'; import { useRevealContext } from './useRevealContext'; import type { ThreeDModelFdmMappings, CadModelOptions } from '../types'; import { ASSET_VIEW, COGNITE_3D_OBJECT_VIEW, COGNITE_CAD_NODE_VIEW, } from '../utils/views'; import { unwrapProperties } from '../utils/data-mapper'; import type { CDFNode } from '../utils/cdf-types'; interface DmsUniqueIdentifier { space: string; externalId: string; } interface CogniteAssetProperties { space: string; externalId: string; object3D: DmsUniqueIdentifier; } interface CogniteCADNodeProperties { space: string; externalId: string; object3D: DmsUniqueIdentifier; model3D: DmsUniqueIdentifier; revisions: DmsUniqueIdentifier[]; treeIndexes: number[]; subTreeSizes: number[]; } /** * Fetches FDM-to-CAD mappings using Core DM connections. * This queries the data model for CAD nodes connected to assets via object3D references. */ export function useFdmAssetMappings( instances: DMInstanceRef[], models: CadModelOptions[] ) { const { sdk } = useRevealContext(); return useQuery({ queryKey: [ 'fdm-cad-connections', instances.map((i) => `${i.space}:${i.externalId}`).join(','), models.map((m) => `${m.modelId}:${m.revisionId}`).join(','), ], queryFn: async (): Promise => { if (instances.length === 0 || models.length === 0) { return []; } try { // Step 1: Query DMS for CAD connections // This traverses: Assets → object3D → CAD nodes const queryResult = await sdk.instances.query({ with: { // Start from the input instances (assets) assets: { nodes: { filter: { and: [ { in: { property: ['node', 'space'], values: [ ...new Set(instances.map((inst) => inst.space)), ], }, }, { in: { property: ['node', 'externalId'], values: instances.map((inst) => inst.externalId), }, }, ], }, }, }, // Navigate to object3D (Cognite3DObject) object_3ds: { nodes: { from: 'assets', through: { view: { type: 'view', ...ASSET_VIEW }, identifier: 'object3D', }, direction: 'outwards', filter: { hasData: [{ type: 'view', ...COGNITE_3D_OBJECT_VIEW }], }, }, }, // Navigate back to CAD nodes that reference this object3D cad_nodes: { nodes: { from: 'object_3ds', through: { view: { type: 'view', ...COGNITE_CAD_NODE_VIEW }, identifier: 'object3D', }, direction: 'inwards', }, }, }, select: { assets: { sources: [ { source: { type: 'view', ...ASSET_VIEW }, properties: ['object3D'], }, ], }, cad_nodes: { sources: [ { source: { type: 'view', ...COGNITE_CAD_NODE_VIEW }, properties: [ 'object3D', 'model3D', 'revisions', 'treeIndexes', 'subTreeSizes', ], }, ], }, }, }); // Step 2: Build mappings per model/revision const mappingsByModel = new Map>(); const cadNodes = queryResult.items.cad_nodes || []; // Group CAD nodes by which instances reference them const object3DToAssets = new Map(); for (const asset of queryResult.items.assets || []) { const props = unwrapProperties( asset as CDFNode, ASSET_VIEW ); if (props?.object3D) { const key = `${props.object3D.space}/${props.object3D.externalId}`; const existing = object3DToAssets.get(key) || []; existing.push({ space: asset.space, externalId: asset.externalId }); object3DToAssets.set(key, existing); } } // Collect all (modelId, revisionId, treeIndex) tuples for batch fetching interface NodeRequest { modelId: number; revisionId: number; treeIndex: number; assetInstances: DMInstanceRef[]; } const nodeRequests: NodeRequest[] = []; // Process CAD nodes to build Node3D mappings for (const cadNode of cadNodes) { const props = unwrapProperties( cadNode as CDFNode, COGNITE_CAD_NODE_VIEW ); if (!props) continue; const { model3D, revisions, treeIndexes, object3D } = props; if (!model3D || !revisions || !treeIndexes) continue; // Find which assets reference this CAD node const object3DKey = `${object3D.space}/${object3D.externalId}`; const relatedAssets = object3DToAssets.get(object3DKey); if (!relatedAssets) continue; // Extract modelId and match with requested models const modelId = extractModelId(model3D.externalId); // For each revision/treeIndex pair for (let i = 0; i < revisions.length; i++) { const revision = revisions[i]; const treeIndex = treeIndexes[i]; const revisionId = extractRevisionId(revision.externalId); // Check if this model/revision is in our requested list const matchingModel = models.find( (m) => m.modelId === modelId && m.revisionId === revisionId ); if (!matchingModel) continue; nodeRequests.push({ modelId, revisionId, treeIndex, assetInstances: relatedAssets, }); } } // Batch fetch nodes by revision const nodesByRevision = new Map(); for (const req of nodeRequests) { const key = `${req.modelId}/${req.revisionId}`; const existing = nodesByRevision.get(key) || []; existing.push(req); nodesByRevision.set(key, existing); } // Fetch all nodes in parallel per revision const revisionFetchPromises = Array.from(nodesByRevision.entries()).map( async ([revisionKey, requests]) => { const [modelId, revisionId] = revisionKey.split('/').map(Number); const treeIndexes = requests.map((r) => r.treeIndex); const nodes = await fetchNodesByTreeIndex( sdk, modelId, revisionId, treeIndexes ); return { revisionKey, nodes, requests }; } ); const allRevisionData = await Promise.all(revisionFetchPromises); for (const { revisionKey, nodes, requests } of allRevisionData) { const treeIndexToNode = new Map( nodes.map((node) => [node.treeIndex, node]) ); const modelMappings = mappingsByModel.get(revisionKey) ?? new Map(); mappingsByModel.set(revisionKey, modelMappings); for (const req of requests) { const node3D = treeIndexToNode.get(req.treeIndex); if (!node3D) continue; for (const instance of req.assetInstances) { const instanceKey = `${instance.space}:${instance.externalId}`; const arr = modelMappings.get(instanceKey) ?? []; arr.push(node3D); modelMappings.set(instanceKey, arr); } } } // Convert to result format const results: ThreeDModelFdmMappings[] = []; for (const model of models) { const modelKey = `${model.modelId}/${model.revisionId}`; results.push({ modelId: model.modelId, revisionId: model.revisionId, mappings: mappingsByModel.get(modelKey) ?? new Map(), }); } return results; } catch (error) { console.error('Error fetching FDM CAD connections:', error); return []; } }, enabled: !!sdk && instances.length > 0 && models.length > 0, staleTime: 5 * 60 * 1000, // 5 minutes }); } // Helper to extract numeric modelId from externalId like "model_123_space" function extractModelId(externalId: string): number { const match = externalId.match(/model_(\d+)/); return match ? parseInt(match[1], 10) : -1; } // Helper to extract numeric revisionId from externalId like "model_123_revision_456_space" function extractRevisionId(externalId: string): number { const match = externalId.match(/revision_(\d+)/); return match ? parseInt(match[1], 10) : -1; } /** * Fetch 3D nodes by their tree indices using the optimized internal IDs endpoint. * This is much more efficient than fetching all nodes and filtering. */ async function fetchNodesByTreeIndex( sdk: CogniteClient, modelId: number, revisionId: number, treeIndexes: number[] ): Promise { if (treeIndexes.length === 0) { return []; } // Deduplicate tree indices const uniqueTreeIndexes = Array.from(new Set(treeIndexes)); // Step 1: Convert tree indices to internal node IDs const nodeIdResponse = await sdk.post<{ items: number[] }>( `/api/v1/projects/${sdk.project}/3d/models/${modelId}/revisions/${revisionId}/nodes/internalids/bytreeindices`, { data: { items: uniqueTreeIndexes, }, } ); const nodeIds = nodeIdResponse.data.items; if (nodeIds.length === 0) { return []; } // Step 2: Retrieve full node details by internal IDs const nodes = await sdk.revisions3D.retrieve3DNodes( modelId, revisionId, nodeIds.map((id) => ({ id })) ); return nodes; }