diff --git a/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts b/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts index 6c7c510168d..752820dcfa9 100644 --- a/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts +++ b/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts @@ -3,11 +3,15 @@ */ import { type Node3D, type CogniteClient, type CogniteExternalId } from '@cognite/sdk'; -import { type DmsUniqueIdentifier, type EdgeItem, type FdmSDK } from '../../utilities/FdmSDK'; +import { + type Source, + type DmsUniqueIdentifier, + type EdgeItem, + type FdmSDK +} from '../../utilities/FdmSDK'; import { RevisionFdmNodeCache } from './RevisionFdmNodeCache'; import { type FdmEdgeWithNode, - type Fdm3dNodeData, type FdmCadEdge, type ModelRevisionKey, type RevisionId, @@ -32,7 +36,7 @@ import { import { partition } from 'lodash'; import assert from 'assert'; -import { fetchNodesForNodeIds } from './requests'; +import { fetchNodesForNodeIds, inspectNodes } from './requests'; import { type ThreeDModelMappings } from '../../hooks/types'; export class FdmNodeCache { @@ -203,8 +207,9 @@ export class FdmNodeCache { ): Promise> { const revisionIds = modelRevisionIds.map((modelRevisionId) => modelRevisionId.revisionId); const edges = await this.getEdgesForRevisions(revisionIds, this._fdmClient); + const edgesWithViews = await this.getViewsForEdges(edges); const revisionToEdgesMap = await createRevisionToEdgesMap( - edges, + edgesWithViews, modelRevisionIds, this._cdfClient ); @@ -218,12 +223,28 @@ export class FdmNodeCache { modelId: number, revisionId: number, treeIndex: number - ): Promise { + ): Promise { const revisionCache = this.getOrCreateRevisionCache(modelId, revisionId); return await revisionCache.getClosestParentFdmData(treeIndex); } + private async getViewsForEdges( + edges: FdmCadEdge[] + ): Promise> { + const nodeInspectionResults = await inspectNodes( + this._fdmClient, + edges.map((edge) => edge.startNode) + ); + + const dataWithViews = edges.map((edge, ind) => ({ + edge, + view: nodeInspectionResults.items[ind].inspectionResults.involvedViewsAndContainers.views[0] + })); + + return dataWithViews; + } + private async getEdgesForRevisions( revisionIds: number[], fdmClient: FdmSDK @@ -266,24 +287,29 @@ export class FdmNodeCache { } async function createRevisionToEdgesMap( - edges: FdmCadEdge[], + edgesWithViews: Array<{ edge: FdmCadEdge; view: Source }>, modelRevisionIds: ModelRevisionId[], cdfClient: CogniteClient ): Promise> { - const revisionToNodeIdsMap = createRevisionToNodeIdMap(edges); + const revisionToNodeIdsMap = createRevisionToNodeIdMap(edgesWithViews); const modelNodeIdToNodeMap = await createModelNodeIdToNodeMap( revisionToNodeIdsMap, modelRevisionIds, cdfClient ); - return edges.reduce((map, edge) => { - const edgeRevisionId = edge.properties.revisionId; + return edgesWithViews.reduce((map, edgeWithView) => { + const edgeRevisionId = edgeWithView.edge.properties.revisionId; const modelRevisionId = modelRevisionIds.find((p) => p.revisionId === edgeRevisionId); if (modelRevisionId === undefined) return map; - const value = createFdmEdgeWithNode(modelRevisionId, edge, modelNodeIdToNodeMap); + const value = createFdmEdgeWithNode( + modelRevisionId, + edgeWithView.edge, + edgeWithView.view, + modelNodeIdToNodeMap + ); insertEdgeIntoMapList(value, map, modelRevisionId); @@ -294,6 +320,7 @@ async function createRevisionToEdgesMap( function createFdmEdgeWithNode( modelRevisionId: ModelRevisionId, edge: FdmCadEdge, + view: Source, modelNodeIdToNodeMap: Map ): FdmEdgeWithNode { const revisionNodeIdKey = createModelNodeIdKey( @@ -305,7 +332,7 @@ function createFdmEdgeWithNode( const node = modelNodeIdToNodeMap.get(revisionNodeIdKey); assert(node !== undefined); - return { edge, node }; + return { edge, node, view }; } function insertEdgeIntoMapList( @@ -350,14 +377,18 @@ async function createModelNodeIdToNodeMap( return revisionNodeIdToNode; } -function createRevisionToNodeIdMap(edges: FdmCadEdge[]): Map { - return edges.reduce((revisionNodeIdMap, edge) => { - const nodeIdsInRevision = revisionNodeIdMap.get(edge.properties.revisionId); +function createRevisionToNodeIdMap( + edgesWithViews: Array<{ edge: FdmCadEdge; view: Source }> +): Map { + return edgesWithViews.reduce((revisionNodeIdMap, edgeWithView) => { + const { revisionNodeId, revisionId } = edgeWithView.edge.properties; + + const nodeIdsInRevision = revisionNodeIdMap.get(revisionId); if (nodeIdsInRevision !== undefined) { - nodeIdsInRevision.push(edge.properties.revisionNodeId); + nodeIdsInRevision.push(revisionNodeId); } else { - revisionNodeIdMap.set(edge.properties.revisionId, [edge.properties.revisionNodeId]); + revisionNodeIdMap.set(revisionId, [revisionNodeId]); } return revisionNodeIdMap; diff --git a/react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx b/react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx index 3fe04dbd0b9..0ec0e844bcf 100644 --- a/react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx +++ b/react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx @@ -6,7 +6,7 @@ import { type ReactElement, type ReactNode, createContext, useContext, useMemo } import { FdmNodeCache } from './FdmNodeCache'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; import { useFdmSdk, useSDK } from '../RevealContainer/SDKProvider'; -import { type ModelRevisionToEdgeMap, type Fdm3dNodeData } from './types'; +import { type FdmEdgeWithNode, type ModelRevisionToEdgeMap } from './types'; import assert from 'assert'; import { type DmsUniqueIdentifier } from '../../utilities/FdmSDK'; @@ -20,16 +20,22 @@ export type FdmNodeCacheContent = { export const FdmNodeCacheContext = createContext(undefined); -export const useMappedEdgesForRevisions = ( - modelRevisionIds: Array<{ modelId: number; revisionId: number }>, - enabled = true -): UseQueryResult => { +export const useFdmNodeCache = (): FdmNodeCacheContent => { const content = useContext(FdmNodeCacheContext); if (content === undefined) { throw Error('Must use useNodeCache inside a NodeCacheContext'); } + return content; +}; + +export const useMappedEdgesForRevisions = ( + modelRevisionIds: Array<{ modelId: number; revisionId: number }>, + enabled = true +): UseQueryResult => { + const content = useFdmNodeCache(); + return useQuery( [ 'reveal', @@ -45,8 +51,8 @@ export const useFdm3dNodeData = ( modelId: number | undefined, revisionId: number | undefined, treeIndex: number | undefined -): UseQueryResult => { - const content = useContext(FdmNodeCacheContext); +): UseQueryResult => { + const content = useFdmNodeCache(); const enableQuery = content !== undefined && @@ -65,10 +71,6 @@ export const useFdm3dNodeData = ( } ); - if (content === undefined) { - throw Error('Must use useNodeCache inside a NodeCacheContext'); - } - return result; }; @@ -76,14 +78,12 @@ export const useFdmAssetMappings = ( fdmAssetExternalIds: DmsUniqueIdentifier[], models: TypedReveal3DModel[] ): UseQueryResult => { - const nodeCacheContent = useContext(FdmNodeCacheContext); + const nodeCacheContent = useFdmNodeCache(); return useQuery( ['reveal', 'react-components', 'fdm-asset-mappings', fdmAssetExternalIds], async () => { - return ( - (await nodeCacheContent?.cache.getMappingsForFdmIds(fdmAssetExternalIds, models)) ?? [] - ); + return await nodeCacheContent.cache.getMappingsForFdmIds(fdmAssetExternalIds, models); }, { enabled: fdmAssetExternalIds.length > 0 && models.length > 0, diff --git a/react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts b/react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts index f55562002da..d10afa1d7e4 100644 --- a/react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts +++ b/react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts @@ -4,7 +4,7 @@ import { type CogniteClient, type Node3D } from '@cognite/sdk'; import { type FdmSDK } from '../../utilities/FdmSDK'; -import { type TreeIndex, type Fdm3dNodeData, type FdmEdgeWithNode, type FdmCadEdge } from './types'; +import { type TreeIndex, type FdmEdgeWithNode, type FdmCadEdge } from './types'; import { fetchAncestorNodesForTreeIndex, @@ -24,7 +24,6 @@ export class RevisionFdmNodeCache { private readonly _revisionId: number; private readonly _treeIndexToFdmEdges = new Map(); - private readonly _treeIndexToFdmData = new Map(); constructor( cogniteClient: CogniteClient, @@ -39,8 +38,8 @@ export class RevisionFdmNodeCache { this._revisionId = revisionId; } - public async getClosestParentFdmData(searchTreeIndex: number): Promise { - const cachedFdmData = this._treeIndexToFdmData.get(searchTreeIndex); + public async getClosestParentFdmData(searchTreeIndex: number): Promise { + const cachedFdmData = this._treeIndexToFdmEdges.get(searchTreeIndex); if (cachedFdmData !== undefined) { return cachedFdmData; @@ -55,7 +54,7 @@ export class RevisionFdmNodeCache { return await this.findNodeDataFromAncestors(searchTreeIndex); } - private async findNodeDataFromAncestors(treeIndex: TreeIndex): Promise { + private async findNodeDataFromAncestors(treeIndex: TreeIndex): Promise { const { edges, ancestorsWithSameMapping, firstMappedAncestorTreeIndex } = await this.getClosestParentMapping(treeIndex); @@ -63,7 +62,7 @@ export class RevisionFdmNodeCache { return []; } - const cachedFdmData = this._treeIndexToFdmData.get(firstMappedAncestorTreeIndex); + const cachedFdmData = this._treeIndexToFdmEdges.get(firstMappedAncestorTreeIndex); if (cachedFdmData !== undefined) { this.setCacheForNodes(ancestorsWithSameMapping, cachedFdmData); @@ -82,29 +81,28 @@ export class RevisionFdmNodeCache { return await this.getDataWithViewsForFdmEdges(nodeEdges, ancestorsWithSameMapping); } - private setCacheForNodes(nodes: Node3D[], nodeData: Fdm3dNodeData[]): void { + private setCacheForNodes(nodes: Node3D[], nodeData: FdmEdgeWithNode[]): void { nodes.forEach((node) => { - this._treeIndexToFdmData.set(node.treeIndex, nodeData); + this._treeIndexToFdmEdges.set(node.treeIndex, nodeData); }); } private async getDataWithViewsForFdmEdges( - nodeEdges: FdmEdgeWithNode[], + nodeEdges: Array<{ edge: FdmCadEdge; node: Node3D }>, ancestorsWithSameMapping: Node3D[] - ): Promise { + ): Promise { const nodeInspectionResults = await inspectNodes( this._fdmClient, nodeEdges.map((edge) => edge.edge.startNode) ); const dataWithViews = nodeEdges.map((fdmEdgeWithNode, ind) => ({ - fdmId: fdmEdgeWithNode.edge.startNode, - view: nodeInspectionResults.items[ind].inspectionResults.involvedViewsAndContainers.views[0], - cadNode: fdmEdgeWithNode.node + ...fdmEdgeWithNode, + view: nodeInspectionResults.items[ind].inspectionResults.involvedViewsAndContainers.views[0] })); ancestorsWithSameMapping.forEach((ancestor) => - this._treeIndexToFdmData.set(ancestor.treeIndex, dataWithViews) + this._treeIndexToFdmEdges.set(ancestor.treeIndex, dataWithViews) ); return dataWithViews; diff --git a/react-components/src/components/NodeCacheProvider/requests.ts b/react-components/src/components/NodeCacheProvider/requests.ts index 42829f77a3a..c42775e009f 100644 --- a/react-components/src/components/NodeCacheProvider/requests.ts +++ b/react-components/src/components/NodeCacheProvider/requests.ts @@ -14,6 +14,7 @@ import { type InModel3dEdgeProperties, SYSTEM_3D_EDGE_SOURCE } from '../../utilities/globalDataModels'; +import { chunk } from 'lodash'; export async function fetchAncestorNodesForTreeIndex( modelId: number, @@ -83,14 +84,24 @@ export async function inspectNodes( fdmClient: FdmSDK, dataNodes: DmsUniqueIdentifier[] ): Promise { - const inspectionResult = await fdmClient.inspectInstances({ - inspectionOperations: { involvedViewsAndContainers: {} }, - items: dataNodes.map((node) => ({ - instanceType: 'node', - externalId: node.externalId, - space: node.space - })) - }); + const chunkedNodes = chunk(dataNodes, 100); + + const inspectionResult: InspectResultList = { + items: [] + }; + + for (const nodesChunk of chunkedNodes) { + const chunkInspectionResults = await fdmClient.inspectInstances({ + inspectionOperations: { involvedViewsAndContainers: {} }, + items: nodesChunk.map((node) => ({ + instanceType: 'node', + externalId: node.externalId, + space: node.space + })) + }); + + inspectionResult.items.push(...chunkInspectionResults.items); + } return inspectionResult; } diff --git a/react-components/src/components/NodeCacheProvider/types.ts b/react-components/src/components/NodeCacheProvider/types.ts index 73b8be1c8f5..dedd5be190a 100644 --- a/react-components/src/components/NodeCacheProvider/types.ts +++ b/react-components/src/components/NodeCacheProvider/types.ts @@ -5,9 +5,8 @@ import { type Node3D } from '@cognite/sdk'; import { type EdgeItem, type DmsUniqueIdentifier, type Source } from '../../utilities/FdmSDK'; import { type InModel3dEdgeProperties } from '../../utilities/globalDataModels'; -export type Fdm3dNodeData = { fdmId: DmsUniqueIdentifier; view: Source; cadNode: Node3D }; export type FdmCadEdge = EdgeItem; -export type FdmEdgeWithNode = { edge: FdmCadEdge; node: Node3D }; +export type FdmEdgeWithNode = { edge: FdmCadEdge; node: Node3D; view: Source }; export type ModelId = number; export type RevisionId = number; diff --git a/react-components/src/hooks/use3dNodeByExternalId.tsx b/react-components/src/hooks/use3dNodeByExternalId.tsx new file mode 100644 index 00000000000..507cfacd464 --- /dev/null +++ b/react-components/src/hooks/use3dNodeByExternalId.tsx @@ -0,0 +1,47 @@ +/*! + * Copyright 2023 Cognite AS + */ +import { type UseQueryResult, useQuery } from '@tanstack/react-query'; +import { useFdmNodeCache } from '../components/NodeCacheProvider/NodeCacheProvider'; +import { type DmsUniqueIdentifier, useReveal } from '../index'; +import { type Node3D } from '@cognite/sdk'; + +export const use3dNodeByExternalId = ({ + externalId, + space +}: Partial): UseQueryResult => { + const viewer = useReveal(); + const fdmNodeCache = useFdmNodeCache(); + + return useQuery( + ['reveal', 'react-components', '3dNodeByExternalId', externalId, space], + async () => { + if (externalId === undefined || space === undefined) { + await Promise.reject( + new Error(`No externalId and space provided to use3dNodeByExternalId hook`) + ); + return; + } + + const modelsRevisionIds = viewer.models.map((model) => ({ + modelId: model.modelId, + revisionId: model.revisionId + })); + + const modelMappings = ( + await fdmNodeCache.cache.getMappingsForFdmIds([{ externalId, space }], modelsRevisionIds) + ).find((model) => model.mappings.size > 0); + + const node3d = modelMappings?.mappings.get(externalId)?.[0]; + + if (node3d === undefined) { + await Promise.reject( + new Error(`Could not find a connected model to instance ${externalId} in space ${space}`) + ); + return; + } + + return node3d; + } + ); +}; diff --git a/react-components/src/hooks/useCameraNavigation.tsx b/react-components/src/hooks/useCameraNavigation.tsx index a530e9c00e8..7af9b336306 100644 --- a/react-components/src/hooks/useCameraNavigation.tsx +++ b/react-components/src/hooks/useCameraNavigation.tsx @@ -4,8 +4,7 @@ import { type CogniteCadModel } from '@cognite/reveal'; import { useReveal } from '../components/RevealContainer/RevealContext'; -import { useFdmSdk } from '../components/RevealContainer/SDKProvider'; -import { SYSTEM_3D_EDGE_SOURCE, type InModel3dEdgeProperties } from '../utilities/globalDataModels'; +import { useFdmNodeCache } from '../components/NodeCacheProvider/NodeCacheProvider'; export type CameraNavigationActions = { fitCameraToAllModels: () => void; @@ -14,8 +13,8 @@ export type CameraNavigationActions = { }; export const useCameraNavigation = (): CameraNavigationActions => { + const fdmNodeCache = useFdmNodeCache(); const viewer = useReveal(); - const fdmSDK = useFdmSdk(); const fitCameraToAllModels = (): void => { const models = viewer.models; @@ -36,28 +35,25 @@ export const useCameraNavigation = (): CameraNavigationActions => { }; const fitCameraToInstance = async (externalId: string, space: string): Promise => { - const fdmAssetMappingFilter = { - equals: { - property: ['edge', 'startNode'], - value: { externalId, space } - } - }; + const modelsRevisionIds = viewer.models.map((model) => ({ + modelId: model.modelId, + revisionId: model.revisionId + })); - const assetEdges = await fdmSDK.filterInstances( - fdmAssetMappingFilter, - 'edge', - SYSTEM_3D_EDGE_SOURCE - ); + const modelMappings = ( + await fdmNodeCache.cache.getMappingsForFdmIds([{ externalId, space }], modelsRevisionIds) + ).find((model) => model.mappings.size > 0); - if (assetEdges.edges.length === 0) { + const nodeId = modelMappings?.mappings.get(externalId)?.[0]; + + if (modelMappings === undefined || nodeId === undefined) { await Promise.reject( new Error(`Could not find a connected model to instance ${externalId} in space ${space}`) ); return; } - const { revisionId, revisionNodeId } = assetEdges.edges[0].properties; - await fitCameraToModelNode(revisionId, revisionNodeId); + await fitCameraToModelNode(modelMappings.revisionId, nodeId.id); }; return { diff --git a/react-components/src/hooks/useClickedNode.tsx b/react-components/src/hooks/useClickedNode.tsx index 81591acfeef..50a49e46aa3 100644 --- a/react-components/src/hooks/useClickedNode.tsx +++ b/react-components/src/hooks/useClickedNode.tsx @@ -3,9 +3,16 @@ */ import { type CadIntersection, type PointerEventData } from '@cognite/reveal'; -import { useReveal, type NodeDataResult } from '../'; +import { type DmsUniqueIdentifier, type Source, useReveal } from '../'; import { useEffect, useState } from 'react'; import { useFdm3dNodeData } from '../components/NodeCacheProvider/NodeCacheProvider'; +import { type Node3D } from '@cognite/sdk'; + +export type NodeDataResult = { + fdmNode: DmsUniqueIdentifier; + view: Source; + cadNode: Node3D; +}; export type ClickedNodeData = Partial & { intersection: CadIntersection; @@ -66,9 +73,9 @@ export const useClickedNodeData = (): ClickedNodeData | undefined => { setClickedNodeData({ intersection: cadIntersection, - fdmNode: chosenNode.fdmId, + fdmNode: chosenNode.edge.startNode, view: chosenNode.view, - cadNode: chosenNode.cadNode + cadNode: chosenNode.node }); function isWaitingForQueryResult(): boolean { diff --git a/react-components/src/index.ts b/react-components/src/index.ts index 810258ba771..9ba183226f8 100644 --- a/react-components/src/index.ts +++ b/react-components/src/index.ts @@ -19,9 +19,14 @@ export { RevealKeepAlive } from './components/RevealKeepAlive/RevealKeepAlive'; export { useReveal } from './components/RevealContainer/RevealContext'; export { use3DModelName } from './hooks/use3DModelName'; export { useFdmAssetMappings } from './components/NodeCacheProvider/NodeCacheProvider'; -export { useClickedNodeData, type ClickedNodeData } from './hooks/useClickedNode'; +export { + useClickedNodeData, + type ClickedNodeData, + type NodeDataResult +} from './hooks/useClickedNode'; export { useCameraNavigation } from './hooks/useCameraNavigation'; export { useMappedEdgesForRevisions } from './components/NodeCacheProvider/NodeCacheProvider'; +export { use3dNodeByExternalId } from './hooks/use3dNodeByExternalId'; // Higher order components export { withSuppressRevealEvents } from './higher-order-components/withSuppressRevealEvents'; @@ -45,8 +50,7 @@ export { export type { AddImageCollection360Options, AddResourceOptions, - AddReveal3DModelOptions, - NodeDataResult + AddReveal3DModelOptions } from './components/Reveal3DResources/types'; export type { CameraNavigationActions } from './hooks/useCameraNavigation'; export type { Source, DmsUniqueIdentifier } from './utilities/FdmSDK'; diff --git a/react-components/stories/HighlightNode.stories.tsx b/react-components/stories/HighlightNode.stories.tsx index 653be9ed936..0d8e9c8aa91 100644 --- a/react-components/stories/HighlightNode.stories.tsx +++ b/react-components/stories/HighlightNode.stories.tsx @@ -7,10 +7,10 @@ import { RevealContainer, RevealToolbar, Reveal3DResources, - type AddResourceOptions, useClickedNodeData, - type FdmAssetStylingGroup, - useCameraNavigation + useCameraNavigation, + type AddResourceOptions, + type FdmAssetStylingGroup } from '../src'; import { Color } from 'three'; import { type ReactElement, useState, useEffect } from 'react';