diff --git a/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx b/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx index 17fe396efb0..05b70935eb4 100644 --- a/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx +++ b/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx @@ -2,7 +2,7 @@ * Copyright 2023 Cognite AS */ import { useRef, type ReactElement, useState, useEffect } from 'react'; -import { type Cognite3DViewer, type PointerEventData } from '@cognite/reveal'; +import { type Cognite3DViewer } from '@cognite/reveal'; import { CadModelContainer, type CadModelStyling } from '../CadModelContainer/CadModelContainer'; import { PointCloudContainer, @@ -18,9 +18,8 @@ import { type Reveal3DResourcesProps, type DefaultResourceStyling } from './types'; -import { queryMappedData } from './queryMappedData'; -import { useFdmSdk, useSDK } from '../RevealContainer/SDKProvider'; import { useCalculateModelsStyling } from '../../hooks/useCalculateModelsStyling'; +import { useClickedNodeData } from '../..'; export const Reveal3DResources = ({ resources, @@ -32,8 +31,6 @@ export const Reveal3DResources = ({ const [reveal3DModels, setReveal3DModels] = useState([]); const viewer = useReveal(); - const fdmSdk = useFdmSdk(); - const client = useSDK(); const numModelsLoaded = useRef(0); useEffect(() => { @@ -49,20 +46,13 @@ export const Reveal3DResources = ({ }, [resources, viewer]); const reveal3DModelsStyling = useCalculateModelsStyling(reveal3DModels, instanceStyling ?? []); + const clickedNodeData = useClickedNodeData(); useEffect(() => { - const callback = (event: PointerEventData): void => { - if (onNodeClick === undefined) return; - const data = queryMappedData(viewer, client, fdmSdk, event); - onNodeClick(data); - }; - - viewer.on('click', callback); - - return () => { - viewer.off('click', callback); - }; - }, [viewer, client, fdmSdk, onNodeClick]); + if (clickedNodeData !== undefined) { + onNodeClick?.(Promise.resolve(clickedNodeData)); + } + }, [clickedNodeData, onNodeClick]); const image360CollectionAddOptions = resources.filter( (resource): resource is AddImageCollection360Options => diff --git a/react-components/src/components/Reveal3DResources/queryMappedData.ts b/react-components/src/components/Reveal3DResources/queryMappedData.ts deleted file mode 100644 index 4ccea77e925..00000000000 --- a/react-components/src/components/Reveal3DResources/queryMappedData.ts +++ /dev/null @@ -1,149 +0,0 @@ -/*! - * Copyright 2023 Cognite AS - */ - -import { type Cognite3DViewer, type PointerEventData, type CogniteCadModel } from '@cognite/reveal'; -import { type CogniteInternalId, type CogniteClient, type Node3D } from '@cognite/sdk'; -import { - type EdgeItem, - type InspectResultList, - type FdmSDK, - type DmsUniqueIdentifier -} from '../../utilities/FdmSDK'; -import { type NodeDataResult } from './types'; -import assert from 'assert'; -import { - INSTANCE_SPACE_3D_DATA, - type InModel3dEdgeProperties, - SYSTEM_3D_EDGE_SOURCE -} from '../../utilities/globalDataModels'; - -export async function queryMappedData( - viewer: Cognite3DViewer, - cdfClient: CogniteClient, - fdmClient: FdmSDK, - clickEvent: PointerEventData -): Promise { - const intersection = await viewer.getIntersectionFromPixel( - clickEvent.offsetX, - clickEvent.offsetY - ); - - if (intersection === null || intersection.type !== 'cad') { - return; - } - - const cadIntersection = intersection; - const model = cadIntersection.model; - - const ancestors = await getAncestorNodesForTreeIndex(cdfClient, model, cadIntersection.treeIndex); - - const mappings = await getMappingEdges( - fdmClient, - model, - ancestors.map((n) => n.id) - ); - - if (mappings.edges.length === 0) { - return; - } - - const selectedEdge = mappings.edges[0]; - const selectedNodeId = selectedEdge.properties.revisionNodeId; - const selectedNode = ancestors.find((n) => n.id === selectedNodeId); - assert(selectedNode !== undefined); - - const dataNode = selectedEdge.startNode; - - const inspectionResult = await inspectNode(fdmClient, dataNode); - - const dataView = - inspectionResult.items[0]?.inspectionResults.involvedViewsAndContainers?.views[0]; - - return { - nodeExternalId: dataNode.externalId, - view: dataView, - cadNode: selectedNode, - intersection: cadIntersection - }; -} - -async function getAncestorNodesForTreeIndex( - client: CogniteClient, - model: CogniteCadModel, - treeIndex: number -): Promise { - const nodeId = await model.mapTreeIndexToNodeId(treeIndex); - - const ancestorNodes = await client.revisions3D.list3DNodeAncestors( - model.modelId, - model.revisionId, - nodeId - ); - - return ancestorNodes.items; -} - -async function getMappingEdges( - fdmClient: FdmSDK, - model: CogniteCadModel, - ancestorIds: CogniteInternalId[] -): Promise<{ edges: Array> }> { - const filter = { - and: [ - { - equals: { - property: ['edge', 'endNode'], - value: { - space: INSTANCE_SPACE_3D_DATA, - externalId: `${model.modelId}` - } - } - }, - { - equals: { - property: [ - SYSTEM_3D_EDGE_SOURCE.space, - `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`, - 'revisionId' - ], - value: model.revisionId - } - }, - { - in: { - property: [ - SYSTEM_3D_EDGE_SOURCE.space, - `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`, - 'revisionNodeId' - ], - values: ancestorIds - } - } - ] - }; - - return await fdmClient.filterAllInstances( - filter, - 'edge', - SYSTEM_3D_EDGE_SOURCE - ); -} - -async function inspectNode( - fdmClient: FdmSDK, - dataNode: DmsUniqueIdentifier -): Promise { - const inspectionResult = await fdmClient.inspectInstances({ - inspectionOperations: { involvedViewsAndContainers: {} }, - items: [ - { - instanceType: 'node', - externalId: dataNode.externalId, - space: dataNode.space - } - ] - }); - - return inspectionResult; -} diff --git a/react-components/src/components/Reveal3DResources/types.ts b/react-components/src/components/Reveal3DResources/types.ts index 3ca1b91216e..c98d13c83f7 100644 --- a/react-components/src/components/Reveal3DResources/types.ts +++ b/react-components/src/components/Reveal3DResources/types.ts @@ -3,11 +3,11 @@ */ import { + type NodeAppearance, type AddModelOptions, - type SupportedModelTypes, - type CadIntersection, - type NodeAppearance + type SupportedModelTypes } from '@cognite/reveal'; + import { type Matrix4 } from 'three'; import { type DmsUniqueIdentifier, type Source } from '../../utilities/FdmSDK'; import { type Node3D } from '@cognite/sdk/dist/src'; @@ -29,7 +29,6 @@ export type NodeDataResult = { nodeExternalId: string; view: Source; cadNode: Node3D; - intersection: CadIntersection; }; export type FdmAssetStylingGroup = { diff --git a/react-components/src/hooks/useClickedNode.tsx b/react-components/src/hooks/useClickedNode.tsx new file mode 100644 index 00000000000..a4d87abee75 --- /dev/null +++ b/react-components/src/hooks/useClickedNode.tsx @@ -0,0 +1,45 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { type CadIntersection, type PointerEventData } from '@cognite/reveal'; +import { useReveal, type NodeDataResult } from '../'; +import { useEffect, useState } from 'react'; +import { useNodeMappedData } from './useNodeMappedData'; + +export type ClickedNodeData = NodeDataResult & { + intersection: CadIntersection; +}; + +export const useClickedNodeData = (): ClickedNodeData | undefined => { + const viewer = useReveal(); + + const [cadIntersection, setCadIntersection] = useState(undefined); + + useEffect(() => { + const callback = (event: PointerEventData): void => { + void (async () => { + const intersection = await viewer.getIntersectionFromPixel(event.offsetX, event.offsetY); + + if (intersection === null || intersection.type !== 'cad') { + return; + } + + setCadIntersection(intersection); + })(); + }; + + viewer.on('click', callback); + + return () => { + viewer.off('click', callback); + }; + }, [viewer]); + + const nodeData = useNodeMappedData(cadIntersection?.treeIndex, cadIntersection?.model); + + if (nodeData === undefined || cadIntersection === undefined) { + return undefined; + } + return { intersection: cadIntersection, ...nodeData }; +}; diff --git a/react-components/src/hooks/useNodeMappedData.tsx b/react-components/src/hooks/useNodeMappedData.tsx new file mode 100644 index 00000000000..d1afa4dafe7 --- /dev/null +++ b/react-components/src/hooks/useNodeMappedData.tsx @@ -0,0 +1,161 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { useQuery } from '@tanstack/react-query'; + +import { type CogniteCadModel } from '@cognite/reveal'; +import { type CogniteClient, type CogniteInternalId, type Node3D } from '@cognite/sdk'; +import { type NodeDataResult } from '../components/Reveal3DResources/types'; +import { useFdmSdk, useSDK } from '../components/RevealContainer/SDKProvider'; + +import assert from 'assert'; +import { + type FdmSDK, + type DmsUniqueIdentifier, + type EdgeItem, + type InspectResultList +} from '../utilities/FdmSDK'; +import { + INSTANCE_SPACE_3D_DATA, + SYSTEM_3D_EDGE_SOURCE, + SYSTEM_SPACE_3D_SCHEMA +} from '../utilities/globalDataModels'; + +export const useNodeMappedData = ( + treeIndex: number | undefined, + model: CogniteCadModel | undefined +): NodeDataResult | undefined => { + const cogniteClient = useSDK(); + const fdmClient = useFdmSdk(); + + const mappedDataHashKey = `${model?.modelId ?? ''}-${model?.revisionId ?? ''}-${treeIndex ?? ''}`; + + const queryResult = useQuery(['cdf', '3d', mappedDataHashKey], async () => { + if (model === undefined || treeIndex === undefined) { + return null; + } + + const ancestors = await fetchAncestorNodesForTreeIndex(model, treeIndex, cogniteClient); + + if (ancestors.length === 0) { + return null; + } + + const mappings = await fetchNodeMappingEdges( + model, + ancestors.map((n) => n.id), + fdmClient + ); + + const selectedEdge = + mappings !== undefined && mappings.edges.length > 0 ? mappings.edges[0] : undefined; + + const selectedNodeId = selectedEdge?.properties.revisionNodeId; + + const dataNode = selectedEdge?.startNode; + + if (dataNode === undefined) { + return null; + } + + const inspectionResult = await inspectNode(dataNode, fdmClient); + + const dataView = + inspectionResult?.items[0]?.inspectionResults.involvedViewsAndContainers?.views[0]; + + if (dataView === undefined) { + return null; + } + + const selectedNode = ancestors.find((n) => n.id === selectedNodeId); + + assert(selectedNode !== undefined); + + return { + nodeExternalId: dataNode.externalId, + view: dataView, + cadNode: selectedNode + }; + }); + + return queryResult.data ?? undefined; +}; + +async function fetchAncestorNodesForTreeIndex( + model: CogniteCadModel, + treeIndex: number, + cogniteClient: CogniteClient +): Promise { + const nodeId = await model.mapTreeIndexToNodeId(treeIndex); + + const ancestorNodes = await cogniteClient.revisions3D.list3DNodeAncestors( + model.modelId, + model.revisionId, + nodeId + ); + + return ancestorNodes.items; +} + +async function fetchNodeMappingEdges( + model: CogniteCadModel, + ancestorIds: CogniteInternalId[], + fdmClient: FdmSDK +): Promise<{ edges: Array>> } | undefined> { + assert(ancestorIds.length !== 0); + + const filter = { + and: [ + { + equals: { + property: ['edge', 'endNode'], + value: { + space: INSTANCE_SPACE_3D_DATA, + externalId: `${model.modelId}` + } + } + }, + { + equals: { + property: [ + SYSTEM_SPACE_3D_SCHEMA, + `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`, + 'revisionId' + ], + value: model.revisionId + } + }, + { + in: { + property: [ + SYSTEM_SPACE_3D_SCHEMA, + `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`, + 'revisionNodeId' + ], + values: ancestorIds + } + } + ] + }; + + return await fdmClient.filterAllInstances(filter, 'edge', SYSTEM_3D_EDGE_SOURCE); +} + +async function inspectNode( + dataNode: DmsUniqueIdentifier, + fdmClient: FdmSDK +): Promise { + const inspectionResult = await fdmClient.inspectInstances({ + inspectionOperations: { involvedViewsAndContainers: {} }, + items: [ + { + instanceType: 'node', + externalId: dataNode.externalId, + space: dataNode.space + } + ] + }); + + return inspectionResult; +} diff --git a/react-components/src/index.ts b/react-components/src/index.ts index 8186f7d92d0..3e5f19e7a89 100644 --- a/react-components/src/index.ts +++ b/react-components/src/index.ts @@ -18,6 +18,7 @@ export { RevealKeepAlive } from './components/RevealKeepAlive/RevealKeepAlive'; export { useReveal } from './components/RevealContainer/RevealContext'; export { use3DModelName } from './hooks/use3DModelName'; export { useFdmAssetMappings } from './hooks/useFdmAssetMappings'; +export { useClickedNodeData } from './hooks/useClickedNode'; export { useCameraNavigation } from './hooks/useCameraNavigation'; // Higher order components diff --git a/react-components/stories/HighlightNode.stories.tsx b/react-components/stories/HighlightNode.stories.tsx index 3e452442f9d..1f409501d73 100644 --- a/react-components/stories/HighlightNode.stories.tsx +++ b/react-components/stories/HighlightNode.stories.tsx @@ -7,14 +7,14 @@ import { RevealContainer, RevealToolbar, Reveal3DResources, - type NodeDataResult, type AddResourceOptions, + useClickedNodeData, type FdmAssetStylingGroup, useCameraNavigation } from '../src'; import { Color } from 'three'; -import { type ReactElement, useState, useCallback, useRef } from 'react'; -import { DefaultNodeAppearance, TreeIndexNodeCollection } from '@cognite/reveal'; +import { type ReactElement, useState, useEffect, useRef } from 'react'; +import { DefaultNodeAppearance } from '@cognite/reveal'; import { createSdkByUrlToken } from './utilities/createSdkByUrlToken'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { RevealResourcesFitCameraOnLoad } from './utilities/with3dResoursesFitCameraOnLoad'; @@ -60,23 +60,17 @@ export const Main: Story = { const StoryContent = ({ resources }: { resources: AddResourceOptions[] }): ReactElement => { const [highlightedId, setHighlightedId] = useState(undefined); const stylingGroupsRef = useRef([]); + + const nodeData = useClickedNodeData(); const cameraNavigation = useCameraNavigation(); - const onClick = useCallback( - async (nodeData: Promise): Promise => { - const nodeDataResult = await nodeData; - setHighlightedId(nodeDataResult?.nodeExternalId); - if (nodeDataResult === undefined) return; + useEffect(() => { + setHighlightedId(nodeData?.nodeExternalId); - await cameraNavigation.fitCameraToInstance(nodeDataResult.nodeExternalId, 'pdms-mapping'); + if (nodeData === undefined) return; - nodeDataResult.intersection.model.assignStyledNodeCollection( - new TreeIndexNodeCollection([nodeDataResult.cadNode.treeIndex]), - DefaultNodeAppearance.Highlighted - ); - }, - [] - ); + void cameraNavigation.fitCameraToInstance(nodeData.nodeExternalId, 'pdms-mapping'); + }, [nodeData?.nodeExternalId]); if (stylingGroupsRef.current.length === 1) { stylingGroupsRef.current.pop(); @@ -94,7 +88,6 @@ const StoryContent = ({ resources }: { resources: AddResourceOptions[] }): React