diff --git a/README.md b/README.md index c878379c8ec..c0556afc139 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ that can be explored in the browser. ### Building on Macbook M1 -Building Reveal on Macbook M1 migth require some special care. +Building Reveal on Macbook M1 might require some special care. If you experience issues during the `yarn`-stage in `viewer/`, e.g. ``` diff --git a/examples/src/pages/Viewer.tsx b/examples/src/pages/Viewer.tsx index 8ce60c90795..fd59fa94505 100644 --- a/examples/src/pages/Viewer.tsx +++ b/examples/src/pages/Viewer.tsx @@ -6,7 +6,6 @@ import Stats from 'stats.js'; import { useEffect, useRef } from 'react'; import { CanvasWrapper } from '../components/styled'; import * as THREE from 'three'; -import { CogniteClient } from '@cognite/sdk'; import dat from 'dat.gui'; import { Cognite3DViewer, @@ -29,7 +28,6 @@ import { CameraUI } from '../utils/CameraUI'; import { PointCloudUi } from '../utils/PointCloudUi'; import { ModelUi } from '../utils/ModelUi'; import { NodeTransformUI } from '../utils/NodeTransformUI'; -import { createSDKFromEnvironment, createSDKFromToken } from '../utils/example-helpers'; import { PointCloudClassificationFilterUI } from '../utils/PointCloudClassificationFilterUI'; import { PointCloudObjectStylingUI } from '../utils/PointCloudObjectStylingUI'; import { CustomCameraManager } from '../utils/CustomCameraManager'; @@ -38,6 +36,7 @@ import { Image360UI } from '../utils/Image360UI'; import { Image360StylingUI } from '../utils/Image360StylingUI'; import { LoadGltfUi } from '../utils/LoadGltfUi'; import { createFunnyButton } from '../utils/PageVariationUtils'; +import { getCogniteClient } from '../utils/example-helpers'; window.THREE = THREE; (window as any).reveal = reveal; @@ -51,7 +50,7 @@ export function Viewer() { // Check in order to avoid double initialization of everything, especially dat.gui. // See https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects for why its called twice. if (!canvasWrapperRef.current) { - return () => {}; + return; } const gui = new dat.GUI({ width: Math.min(500, 0.8 * window.innerWidth) }); @@ -76,18 +75,7 @@ export function Viewer() { window.history.pushState({}, '', url.toString()); } - let client: CogniteClient; - if (project && overrideToken) { - client = createSDKFromToken('reveal.example.example', project, overrideToken); - } else if (project && environment) { - client = await createSDKFromEnvironment('reveal.example.example', project, environment); - } else { - client = new CogniteClient({ - appId: 'reveal.example.example', - project: 'dummy', - getToken: async () => 'dummy' - }); - } + let client = await getCogniteClient({ project, environment, overrideToken }); const edlEnabled = (urlParams.get('edl') ?? 'true') === 'true'; const progress = (itemsLoaded: number, itemsRequested: number, itemsCulled: number) => { @@ -502,5 +490,6 @@ export function Viewer() { viewer?.dispose(); }; }, []); + return ; } diff --git a/examples/src/utils/example-helpers.ts b/examples/src/utils/example-helpers.ts index 89a15795b0b..c232c8bed1f 100644 --- a/examples/src/utils/example-helpers.ts +++ b/examples/src/utils/example-helpers.ts @@ -203,3 +203,27 @@ export async function createSDKFromEnvironment( await client.authenticate(); return client; } + +export const getCogniteClient = async ({ + project, + environment, + overrideToken +}: { + project: string | null; + environment: string | null; + overrideToken: string | null; +}): Promise => { + if (project !== null && overrideToken !== null) { + return createSDKFromToken('reveal.example.example', project, overrideToken); + } + + if (project !== null && environment !== null) { + return await createSDKFromEnvironment('reveal.example.example', project, environment); + } + + return new CogniteClient({ + appId: 'reveal.example.example', + project: 'dummy', + getToken: async () => 'dummy' + }); +}; diff --git a/react-components/package.json b/react-components/package.json index 9c3e826b7c9..4ecb75faf42 100644 --- a/react-components/package.json +++ b/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@cognite/reveal-react-components", - "version": "0.10.0", + "version": "0.12.0", "exports": "./dist/index.js", "types": "./dist/index.d.ts", "type": "module", @@ -35,12 +35,12 @@ "@cognite/cogs.js": "^9.17.0", "@cognite/reveal": "4.4.0", "@cognite/sdk": "^8.2.0", - "@storybook/addon-essentials": "7.2.3", - "@storybook/addon-interactions": "7.2.3", - "@storybook/addon-links": "7.2.3", - "@storybook/blocks": "7.2.3", - "@storybook/react": "7.2.3", - "@storybook/react-webpack5": "7.2.3", + "@storybook/addon-essentials": "7.3.1", + "@storybook/addon-interactions": "7.3.1", + "@storybook/addon-links": "7.3.1", + "@storybook/blocks": "7.3.1", + "@storybook/react": "7.3.1", + "@storybook/react-webpack5": "7.3.1", "@storybook/testing-library": "0.2.0", "@tanstack/react-query-devtools": "^4.29.19", "@types/lodash": "^4.14.190", @@ -50,7 +50,7 @@ "@types/three": "0.155.0", "@typescript-eslint/eslint-plugin": "^5.50.0", "eslint": "^8.0.1", - "eslint-config-prettier": "^8.8.0", + "eslint-config-prettier": "^9.0.0", "eslint-config-standard-with-typescript": "latest", "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.25.2", @@ -64,7 +64,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-router-dom": "^6.15.0", - "storybook": "7.2.3", + "storybook": "7.3.1", "style-loader": "^3.3.3", "styled-components": "5.3.11", "three": "0.155.0", @@ -82,4 +82,4 @@ "@tanstack/react-query": "^4.29.19", "lodash": "^4.17.21" } -} +} \ No newline at end of file diff --git a/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx b/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx index 21177fdf543..b327d685d18 100644 --- a/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx +++ b/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx @@ -38,7 +38,11 @@ export function Image360CollectionContainer({ return collection; } - return await viewer.add360ImageSet('events', { site_id: siteId }); + return await viewer.add360ImageSet( + 'events', + { site_id: siteId }, + { preMultipliedRotation: false } + ); } } diff --git a/react-components/src/components/Image360Details/Image360Details.tsx b/react-components/src/components/Image360Details/Image360Details.tsx new file mode 100644 index 00000000000..e26bd7edd33 --- /dev/null +++ b/react-components/src/components/Image360Details/Image360Details.tsx @@ -0,0 +1,89 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { useState, type ReactElement, useCallback, useEffect } from 'react'; +import styled from 'styled-components'; +import { Image360HistoricalDetails } from '../Image360HistoricalDetails/Image360HistoricalDetails'; +import { useReveal } from '../..'; +import { type Image360 } from '@cognite/reveal'; +import { Button } from '@cognite/cogs.js'; + +export function Image360Details(): ReactElement { + const viewer = useReveal(); + const [enteredEntity, setEnteredEntity] = useState(); + const [is360HistoricalPanelExpanded, setIs360HistoricalPanelExpanded] = useState(false); + const handleExpand = useCallback((isExpanded: boolean) => { + setIs360HistoricalPanelExpanded(isExpanded); + }, []); + + const clearEnteredImage360 = (): void => { + setEnteredEntity(undefined); + }; + + const exitImage360Image = (): void => { + viewer.exit360Image(); + }; + + const collections = viewer.get360ImageCollections(); + + useEffect(() => { + collections.forEach((collection) => { + collection.on('image360Entered', setEnteredEntity); + collection.on('image360Exited', clearEnteredImage360); + }); + return () => { + collections.forEach((collection) => { + collection.off('image360Entered', setEnteredEntity); + collection.off('image360Exited', clearEnteredImage360); + }); + }; + }, [viewer, collections]); + + return ( + <> + {enteredEntity !== undefined && ( + <> + + + + + + + + )} + + ); +} + +const StyledExitButton = styled(Button)` + border-radius: 8px; +`; + +const ExitButtonContainer = styled.div` + position: absolute; + right: 20px; + top: 20px; + background-color: #ffffff; + height: 36px; + width: 36px; + border-radius: 8px; + outline: none; +`; + +const Image360HistoricalPanel = styled.div<{ isExpanded: boolean }>` + position: absolute; + bottom: ${({ isExpanded }) => (isExpanded ? '0px' : '40px')}; + display: flex; + flex-direction: column; + height: fit-content; + width: fit-content; + max-width: 100%; + min-width: fill-available; + transition: transform 0.25s ease-in-out; + transform: ${({ isExpanded }) => (isExpanded ? 'translateY(0)' : 'translateY(100%)')}; +`; diff --git a/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts b/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts new file mode 100644 index 00000000000..079911708af --- /dev/null +++ b/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts @@ -0,0 +1,283 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { type Node3D, type CogniteClient } from '@cognite/sdk'; +import { type EdgeItem, type FdmSDK } from '../../utilities/FdmSDK'; +import { RevisionFdmNodeCache } from './RevisionFdmNodeCache'; +import { + type FdmEdgeWithNode, + type Fdm3dNodeData, + type FdmCadEdge, + type RevisionKey, + type RevisionTreeIndex, + type FdmKey, + type FdmId, + type RevisionId, + type NodeId, + type ModelNodeIdKey, + type ModelId +} from './types'; +import { + type InModel3dEdgeProperties, + SYSTEM_3D_EDGE_SOURCE, + SYSTEM_SPACE_3D_SCHEMA +} from '../../utilities/globalDataModels'; + +import { partition } from 'lodash'; + +import assert from 'assert'; +import { fetchNodesForNodeIds } from './requests'; + +export type ModelRevisionKey = `${number}-${number}`; +export type ModelRevisionToEdgeMap = Map; + +export class FdmNodeCache { + private readonly _revisionNodeCaches = new Map(); + + private readonly _cdfClient: CogniteClient; + private readonly _fdmClient: FdmSDK; + + private readonly _completeRevisions = new Set(); + + public constructor(cdfClient: CogniteClient, fdmClient: FdmSDK) { + this._cdfClient = cdfClient; + this._fdmClient = fdmClient; + } + + public async getAllMappingExternalIds( + modelRevisionIds: Array<{ modelId: number; revisionId: number }> + ): Promise { + const [cachedRevisionIds, nonCachedRevisionIds] = partition(modelRevisionIds, (ids) => { + const key = createRevisionKey(ids.modelId, ids.revisionId); + return this._completeRevisions.has(key); + }); + + const cachedEdges = cachedRevisionIds.map((id) => this.getCachedEdgesForRevision(id)); + + const revisionToEdgesMap = await this.getRevisionToEdgesMap(nonCachedRevisionIds); + + this.writeRevisionDataToCache(revisionToEdgesMap); + + cachedEdges.forEach(([revisionKey, edges]) => { + revisionToEdgesMap.set(revisionKey, edges); + }); + + return revisionToEdgesMap; + } + + private getCachedEdgesForRevision(id: { + modelId: number; + revisionId: number; + }): [RevisionKey, FdmEdgeWithNode[]] { + const revisionCache = this.getOrCreateRevisionCache(id.modelId, id.revisionId); + const revisionKey = createRevisionKey(id.modelId, id.revisionId); + const cachedRevisionEdges = revisionCache.getAllEdges(); + return [revisionKey, cachedRevisionEdges]; + } + + private writeRevisionDataToCache(modelMap: Map): void { + for (const [revisionKey, data] of modelMap.entries()) { + const [modelId, revisionId] = revisionKeyToIds(revisionKey); + const revisionCache = this.getOrCreateRevisionCache(modelId, revisionId); + + data.forEach((edgeAndNode) => { + revisionCache.insertTreeIndexMappings(edgeAndNode.node.treeIndex, edgeAndNode); + }); + + this._completeRevisions.add(revisionKey); + } + } + + private async getRevisionToEdgesMap( + modelRevisionIds: Array<{ modelId: number; revisionId: number }> + ): Promise> { + const revisionIds = modelRevisionIds.map((modelRevisionId) => modelRevisionId.revisionId); + const edges = await this.getEdgesForRevisions(revisionIds, this._fdmClient); + return await groupToModelRevision(edges, modelRevisionIds, this._cdfClient); + } + + public async getClosestParentExternalId( + modelId: number, + revisionId: number, + treeIndex: number + ): Promise { + const revisionCache = this.getOrCreateRevisionCache(modelId, revisionId); + + return await revisionCache.getClosestParentFdmData(treeIndex); + } + + private async getEdgesForRevisions( + revisionIds: number[], + fdmClient: FdmSDK + ): Promise>> { + const versionedPropertiesKey = `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`; + const filter = { + in: { + property: [SYSTEM_SPACE_3D_SCHEMA, versionedPropertiesKey, 'revisionId'], + values: revisionIds + } + }; + const mappings = await fdmClient.filterAllInstances( + filter, + 'edge', + SYSTEM_3D_EDGE_SOURCE + ); + return mappings.edges; + } + + private getOrCreateRevisionCache(modelId: number, revisionId: number): RevisionFdmNodeCache { + const revisionKey = createRevisionKey(modelId, revisionId); + + const revisionCache = this._revisionNodeCaches.get(revisionKey); + + if (revisionCache !== undefined) { + return revisionCache; + } + + const newRevisionCache = new RevisionFdmNodeCache( + this._cdfClient, + this._fdmClient, + modelId, + revisionId + ); + + this._revisionNodeCaches.set(revisionKey, newRevisionCache); + + return newRevisionCache; + } +} + +function createRevisionKey(modelId: number, revisionId: number): RevisionKey { + return `${modelId}-${revisionId}`; +} + +function revisionKeyToIds(revisionKey: RevisionKey): [number, number] { + const components = revisionKey.split('-'); + return [Number(components[0]), Number(components[1])]; +} + +export function createRevisionTreeIndex( + modelId: number, + revisionId: number, + treeIndex: number +): RevisionTreeIndex { + return `${modelId}-${revisionId}-${treeIndex}`; +} + +export function createFdmKey(spaceId: string, externalId: string): FdmKey { + return `${spaceId}-${externalId}`; +} + +export function fdmKeyToId(fdmKey: FdmKey): FdmId { + const parts = fdmKey.split('-'); + + return { space: parts[0], externalId: parts[1] }; +} + +export function insertIntoSetMap(key: T, value: U, globalMap: Map): void { + const prevVal = globalMap.get(key); + + if (prevVal === undefined) { + globalMap.set(key, [value]); + return; + } + + prevVal.push(value); +} + +async function groupToModelRevision( + edges: FdmCadEdge[], + modelRevisionIds: Array<{ modelId: number; revisionId: number }>, + cdfClient: CogniteClient +): Promise> { + const revisionToNodeIdsMap = createRevisionToNodeIdMap(edges); + const modelNodeIdToNodeMap = await createModelNodeIdToNodeMap( + revisionToNodeIdsMap, + modelRevisionIds, + cdfClient + ); + + return edges.reduce((map, edge) => { + const edgeRevisionId = edge.properties.revisionId; + const modelRevisionId = modelRevisionIds.find((p) => p.revisionId === edgeRevisionId); + + if (modelRevisionId === undefined) return map; + + const value = createFdmEdgeWithNode(modelRevisionId, edge, modelNodeIdToNodeMap); + + insertEdgeIntoMapList(value, map, modelRevisionId); + + return map; + }, new Map()); +} + +function createFdmEdgeWithNode( + modelRevisionId: { modelId: number; revisionId: number }, + edge: FdmCadEdge, + modelNodeIdToNodeMap: Map +): FdmEdgeWithNode { + const revisionNodeIdKey = + `${modelRevisionId.modelId}-${modelRevisionId.revisionId}-${edge.properties.revisionNodeId}` as const; + + const node = modelNodeIdToNodeMap.get(revisionNodeIdKey); + assert(node !== undefined); + + return { edge, node }; +} + +function insertEdgeIntoMapList( + value: FdmEdgeWithNode, + map: Map, + modelRevisionId: { modelId: number; revisionId: number } +): void { + const modelRevisionIdKey: ModelRevisionKey = createRevisionKey( + modelRevisionId.modelId, + modelRevisionId.revisionId + ); + + const edgesForModel = map.get(modelRevisionIdKey); + + if (edgesForModel === undefined) { + map.set(modelRevisionIdKey, [value]); + } else { + edgesForModel.push(value); + } +} + +async function createModelNodeIdToNodeMap( + revisionToNodeIdsMap: Map, + modelRevisionIds: Array<{ modelId: ModelId; revisionId: RevisionId }>, + cdfClient: CogniteClient +): Promise> { + const revisionNodeIdToNode = new Map(); + + const nodePromises = [...revisionToNodeIdsMap.entries()].map(async ([revisionId, nodeIds]) => { + const modelId = modelRevisionIds.find((p) => p.revisionId === revisionId)?.modelId; + assert(modelId !== undefined); + + const nodes = await fetchNodesForNodeIds(modelId, revisionId, nodeIds, cdfClient); + nodeIds.forEach((e, ind) => { + const modelNodeIdKey = `${modelId}-${revisionId}-${e}` as const; + revisionNodeIdToNode.set(modelNodeIdKey, nodes[ind]); + }); + }); + + await Promise.all(nodePromises); + + return revisionNodeIdToNode; +} + +function createRevisionToNodeIdMap(edges: FdmCadEdge[]): Map { + return edges.reduce((revisionNodeIdMap, edge) => { + const nodeIdsInRevision = revisionNodeIdMap.get(edge.properties.revisionId); + + if (nodeIdsInRevision !== undefined) { + nodeIdsInRevision.push(edge.properties.revisionNodeId); + } else { + revisionNodeIdMap.set(edge.properties.revisionId, [edge.properties.revisionNodeId]); + } + + return revisionNodeIdMap; + }, new Map()); +} diff --git a/react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx b/react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx new file mode 100644 index 00000000000..4b7cc2e015c --- /dev/null +++ b/react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx @@ -0,0 +1,82 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { type ReactElement, type ReactNode, createContext, useContext, useMemo } from 'react'; +import { FdmNodeCache, type ModelRevisionToEdgeMap } from './FdmNodeCache'; +import { type UseQueryResult, useQuery } from '@tanstack/react-query'; +import { useFdmSdk, useSDK } from '../RevealContainer/SDKProvider'; +import { type Fdm3dNodeData } from './types'; + +import assert from 'assert'; + +export type FdmNodeCacheContent = { + cache: FdmNodeCache; +}; + +export const FdmNodeCacheContext = createContext(undefined); + +export const useMappedEdgesForRevisions = ( + modelRevisionIds: Array<{ modelId: number; revisionId: number }>, + enabled: boolean +): UseQueryResult => { + const content = useContext(FdmNodeCacheContext); + + if (content === undefined) { + throw Error('Must use useNodeCache inside a NodeCacheContext'); + } + + return useQuery( + [ + 'reveal', + 'react-components', + ...modelRevisionIds.map((modelRevisionId) => modelRevisionId.revisionId.toString()).sort() + ], + async () => await content.cache.getAllMappingExternalIds(modelRevisionIds), + { staleTime: Infinity, enabled: enabled && modelRevisionIds.length > 0 } + ); +}; + +export const useFdm3dNodeData = ( + modelId: number | undefined, + revisionId: number | undefined, + treeIndex: number | undefined +): UseQueryResult => { + const content = useContext(FdmNodeCacheContext); + + const enableQuery = + content !== undefined && + modelId !== undefined && + revisionId !== undefined && + treeIndex !== undefined; + + const result = useQuery( + ['reveal', 'react-components', 'tree-index-to-external-id', modelId, revisionId, treeIndex], + async () => { + assert(enableQuery); + return await content.cache.getClosestParentExternalId(modelId, revisionId, treeIndex); + }, + { + enabled: enableQuery + } + ); + + if (content === undefined) { + throw Error('Must use useNodeCache inside a NodeCacheContext'); + } + + return result; +}; + +export function NodeCacheProvider({ children }: { children?: ReactNode }): ReactElement { + const fdmClient = useFdmSdk(); + const cdfClient = useSDK(); + + const fdmCache = useMemo(() => new FdmNodeCache(cdfClient, fdmClient), []); + + return ( + + {children} + + ); +} diff --git a/react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts b/react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts new file mode 100644 index 00000000000..02b75e531e1 --- /dev/null +++ b/react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts @@ -0,0 +1,235 @@ +/*! + * Copyright 2023 Cognite AS + */ + +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 { + fetchAncestorNodesForTreeIndex, + getMappingEdgesForNodeIds, + inspectNodes +} from './requests'; + +import { max } from 'lodash'; + +import assert from 'assert'; + +export class RevisionFdmNodeCache { + private readonly _cogniteClient: CogniteClient; + private readonly _fdmClient: FdmSDK; + + private readonly _modelId: number; + private readonly _revisionId: number; + + private readonly _treeIndexToFdmEdges = new Map(); + private readonly _treeIndexToFdmData = new Map(); + + constructor( + cogniteClient: CogniteClient, + fdmClient: FdmSDK, + modelId: number, + revisionId: number + ) { + this._cogniteClient = cogniteClient; + this._fdmClient = fdmClient; + + this._modelId = modelId; + this._revisionId = revisionId; + } + + public async getClosestParentFdmData(searchTreeIndex: number): Promise { + const cachedFdmData = this._treeIndexToFdmData.get(searchTreeIndex); + + if (cachedFdmData !== undefined) { + return cachedFdmData; + } + + const cachedFdmEdges = this._treeIndexToFdmEdges.get(searchTreeIndex); + + if (cachedFdmEdges !== undefined) { + return await this.getDataWithViewsForFdmEdges(cachedFdmEdges, []); + } + + return await this.findNodeDataFromAncestors(searchTreeIndex); + } + + private async findNodeDataFromAncestors(treeIndex: TreeIndex): Promise { + const { edges, ancestorsWithSameMapping, firstMappedAncestorTreeIndex } = + await this.getClosestParentMapping(treeIndex); + + if (edges.length === 0) { + return []; + } + + const cachedFdmData = this._treeIndexToFdmData.get(firstMappedAncestorTreeIndex); + + if (cachedFdmData !== undefined) { + this.setCacheForNodes(ancestorsWithSameMapping, cachedFdmData); + + return cachedFdmData; + } + + const firstMappedAncestor = ancestorsWithSameMapping.find( + (ancestor) => ancestor.treeIndex === firstMappedAncestorTreeIndex + ); + + assert(firstMappedAncestor !== undefined); + + const nodeEdges = edges.map((edge) => ({ edge, node: firstMappedAncestor })); + + return await this.getDataWithViewsForFdmEdges(nodeEdges, ancestorsWithSameMapping); + } + + private setCacheForNodes(nodes: Node3D[], nodeData: Fdm3dNodeData[]): void { + nodes.forEach((node) => { + this._treeIndexToFdmData.set(node.treeIndex, nodeData); + }); + } + + private async getDataWithViewsForFdmEdges( + nodeEdges: FdmEdgeWithNode[], + ancestorsWithSameMapping: Node3D[] + ): 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 + })); + + ancestorsWithSameMapping.forEach((ancestor) => + this._treeIndexToFdmData.set(ancestor.treeIndex, dataWithViews) + ); + + return dataWithViews; + } + + private async getClosestParentMapping(treeIndex: number): Promise<{ + edges: FdmCadEdge[]; + ancestorsWithSameMapping: Node3D[]; + firstMappedAncestorTreeIndex: number; + }> { + const ancestors: Node3D[] = await fetchAncestorNodesForTreeIndex( + this._modelId, + this._revisionId, + treeIndex, + this._cogniteClient + ); + + const ancestorMappings = await this.getMappingEdgesForAncestors(ancestors); + + if (ancestorMappings.length === 0) { + return { edges: [], ancestorsWithSameMapping: [], firstMappedAncestorTreeIndex: 0 }; + } + + const edgesWithCorrespondingTreeIndex = this.combineEdgesWithTreeIndex( + ancestorMappings, + ancestors + ); + + const firstMappedAncestorTreeIndex = findLargestTreeIndex(edgesWithCorrespondingTreeIndex); + return getAncestorDataForTreeIndex( + firstMappedAncestorTreeIndex, + edgesWithCorrespondingTreeIndex, + ancestors + ); + } + + private combineEdgesWithTreeIndex( + mappingEdges: FdmCadEdge[], + nodes: Node3D[] + ): Array<{ edge: FdmCadEdge; treeIndex: TreeIndex }> { + return mappingEdges.map((edge) => { + const ancestorConnectedToEdge = nodes.find( + (ancestor) => ancestor.id === edge.properties.revisionNodeId + ); + + assert(ancestorConnectedToEdge !== undefined); + + return { + edge, + treeIndex: ancestorConnectedToEdge.treeIndex + }; + }); + } + + private async getMappingEdgesForAncestors(ancestors: Node3D[]): Promise { + const cachedFirstMappedAncestor = ancestors + .filter((ancestor) => this._treeIndexToFdmEdges.has(ancestor.treeIndex)) + .sort((nodeA, nodeB) => nodeB.treeIndex - nodeA.treeIndex)[0]; + + if (cachedFirstMappedAncestor !== undefined) { + const edgesAndNodes = this._treeIndexToFdmEdges.get(cachedFirstMappedAncestor.treeIndex); + + assert(edgesAndNodes !== undefined); + + return edgesAndNodes.map((edge) => edge.edge); + } + + const ancestorMappings = await getMappingEdgesForNodeIds( + this._modelId, + this._revisionId, + this._fdmClient, + ancestors.map((a) => a.id) + ); + + return ancestorMappings.edges; + } + + public insertTreeIndexMappings(treeIndex: TreeIndex, edge: FdmEdgeWithNode): void { + const edgeArray = this._treeIndexToFdmEdges.get(treeIndex); + if (edgeArray === undefined) { + this._treeIndexToFdmEdges.set(treeIndex, [edge]); + } else { + edgeArray.push(edge); + } + } + + public getAllEdges(): FdmEdgeWithNode[] { + return [...this._treeIndexToFdmEdges.values()].flat(); + } + + getIds(): { modelId: number; revisionId: number } { + return { + modelId: this._modelId, + revisionId: this._revisionId + }; + } +} + +function findLargestTreeIndex( + edgesWithTreeIndex: Array<{ edge: FdmCadEdge; treeIndex: TreeIndex }> +): TreeIndex { + const maxTreeIndex = max(edgesWithTreeIndex.map((e) => e.treeIndex)); + assert(maxTreeIndex !== undefined); + return maxTreeIndex; +} + +function getAncestorDataForTreeIndex( + treeIndex: TreeIndex, + edgesWithTreeIndex: Array<{ edge: FdmCadEdge; treeIndex: TreeIndex }>, + ancestors: Node3D[] +): { + edges: FdmCadEdge[]; + ancestorsWithSameMapping: Node3D[]; + firstMappedAncestorTreeIndex: number; +} { + const edgesForFirstMappedAncestor = edgesWithTreeIndex.filter( + (edgeAndTreeIndex) => edgeAndTreeIndex.treeIndex === treeIndex + ); + const ancestorsBetweenSearchNodeAndFirstMappedAncestor = ancestors.filter( + (ancestor) => ancestor.treeIndex >= treeIndex + ); + + return { + edges: edgesForFirstMappedAncestor.map((result) => result.edge), + ancestorsWithSameMapping: ancestorsBetweenSearchNodeAndFirstMappedAncestor, + firstMappedAncestorTreeIndex: treeIndex + }; +} diff --git a/react-components/src/components/NodeCacheProvider/requests.ts b/react-components/src/components/NodeCacheProvider/requests.ts new file mode 100644 index 00000000000..42829f77a3a --- /dev/null +++ b/react-components/src/components/NodeCacheProvider/requests.ts @@ -0,0 +1,128 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { type CogniteClient, type CogniteInternalId, type Node3D } from '@cognite/sdk'; +import { + type DmsUniqueIdentifier, + type FdmSDK, + type InspectResultList +} from '../../utilities/FdmSDK'; +import { type FdmCadEdge } from './types'; +import { + INSTANCE_SPACE_3D_DATA, + type InModel3dEdgeProperties, + SYSTEM_3D_EDGE_SOURCE +} from '../../utilities/globalDataModels'; + +export async function fetchAncestorNodesForTreeIndex( + modelId: number, + revisionId: number, + treeIndex: number, + cogniteClient: CogniteClient +): Promise { + const nodeId = await treeIndexesToNodeIds(modelId, revisionId, [treeIndex], cogniteClient); + + const ancestorNodes = await cogniteClient.revisions3D.list3DNodeAncestors( + modelId, + revisionId, + nodeId[0] + ); + + return ancestorNodes.items; +} + +export async function getMappingEdgesForNodeIds( + modelId: number, + revisionId: number, + fdmClient: FdmSDK, + ancestorIds: CogniteInternalId[] +): Promise<{ edges: FdmCadEdge[] }> { + const filter = { + and: [ + { + equals: { + property: ['edge', 'endNode'], + value: { + space: INSTANCE_SPACE_3D_DATA, + externalId: `${modelId}` + } + } + }, + { + equals: { + property: [ + SYSTEM_3D_EDGE_SOURCE.space, + `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`, + 'revisionId' + ], + value: 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 + ); +} + +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 + })) + }); + + return inspectionResult; +} + +export async function treeIndexesToNodeIds( + modelId: number, + revisionId: number, + treeIndexes: number[], + cogniteClient: CogniteClient +): Promise { + const outputsUrl = `${cogniteClient.getBaseUrl()}/api/v1/projects/${ + cogniteClient.project + }/3d/models/${modelId}/revisions/${revisionId}/nodes/internalids/bytreeindices`; + const response = await cogniteClient.post<{ items: number[] }>(outputsUrl, { + data: { items: treeIndexes } + }); + if (response.status === 200) { + return response.data.items; + } else { + throw Error(`treeIndex-nodeId translation failed for treeIndexes ${treeIndexes.join(',')}`); + } +} + +export async function fetchNodesForNodeIds( + modelId: number, + revisionId: number, + nodeIds: number[], + cogniteClient: CogniteClient +): Promise { + return await cogniteClient.revisions3D.retrieve3DNodes( + modelId, + revisionId, + nodeIds.map((id) => ({ id })) + ); +} diff --git a/react-components/src/components/NodeCacheProvider/types.ts b/react-components/src/components/NodeCacheProvider/types.ts new file mode 100644 index 00000000000..8d4ad971242 --- /dev/null +++ b/react-components/src/components/NodeCacheProvider/types.ts @@ -0,0 +1,21 @@ +/*! + * Copyright 2023 Cognite AS + */ +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 ModelId = number; +export type RevisionId = number; +export type TreeIndex = number; +export type NodeId = number; +export type FdmId = DmsUniqueIdentifier; + +export type RevisionKey = `${ModelId}-${RevisionId}`; +export type FdmKey = `${string}-${string}`; +export type RevisionTreeIndex = `${ModelId}-${RevisionId}-${TreeIndex}`; +export type ModelNodeIdKey = `${ModelId}-${RevisionId}-${NodeId}`; diff --git a/react-components/src/components/Reveal3DResources/types.ts b/react-components/src/components/Reveal3DResources/types.ts index c98d13c83f7..16356a4258f 100644 --- a/react-components/src/components/Reveal3DResources/types.ts +++ b/react-components/src/components/Reveal3DResources/types.ts @@ -26,7 +26,7 @@ export type AddReveal3DModelOptions = AddModelOptions & { transform?: Matrix4 } export type TypedReveal3DModel = AddReveal3DModelOptions & { type: SupportedModelTypes }; export type NodeDataResult = { - nodeExternalId: string; + fdmNode: DmsUniqueIdentifier; view: Source; cadNode: Node3D; }; diff --git a/react-components/src/components/RevealContainer/RevealContainer.tsx b/react-components/src/components/RevealContainer/RevealContainer.tsx index 825214d4308..7fed7ca89d4 100644 --- a/react-components/src/components/RevealContainer/RevealContainer.tsx +++ b/react-components/src/components/RevealContainer/RevealContainer.tsx @@ -10,6 +10,7 @@ import { type Color } from 'three'; import { SDKProvider } from './SDKProvider'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; +import { NodeCacheProvider } from '../NodeCacheProvider/NodeCacheProvider'; import { RevealContainerElementContext } from './RevealContainerElementContext'; type RevealContainerProps = { @@ -77,7 +78,9 @@ export function RevealContainer({ <> - {createPortal(children, viewerDomElement.current)} + + {createPortal(children, viewerDomElement.current)} + diff --git a/react-components/src/components/RevealToolbar/HelpButton.tsx b/react-components/src/components/RevealToolbar/HelpButton.tsx index 3c116478648..fe80a5a09f6 100644 --- a/react-components/src/components/RevealToolbar/HelpButton.tsx +++ b/react-components/src/components/RevealToolbar/HelpButton.tsx @@ -2,7 +2,7 @@ * Copyright 2023 Cognite AS */ -import { useState, type ReactElement } from 'react'; +import { type ReactElement } from 'react'; import { Button, Dropdown } from '@cognite/cogs.js'; import styled from 'styled-components'; @@ -11,12 +11,6 @@ import { TouchNavigation } from './Help/TouchNavigation'; import { KeyboardNavigation } from './Help/KeyboardNavigation'; export const HelpButton = (): ReactElement => { - const [, setHelpEnabled] = useState(false); - - const showHelp = (): void => { - setHelpEnabled((prevState) => !prevState); - }; - return ( { } placement="right"> -