From 7af7766fcab228014cc0c1a81d2a5bbb5dd08baa Mon Sep 17 00:00:00 2001 From: Daniel Priori Date: Thu, 11 Jul 2024 11:40:46 +0200 Subject: [PATCH] fix(react-components): improve asset mapping cache mechanism and refactoring to stabilize rule threshold styling and switching (#4652) * refactoring to use caching and use treeindex instead of numeric range for styling group creation - wip * use the asset mappings per model cache to generate the cache per asset and implement an extra check per item when applying stylings - wip * add cache for node 3d when loading reveal 3d resources to speed up the rule base styling * some wip refactoring on cache functions * minor refactoring * refactoring and adding cache for assets for no mappings to skip requesting again * cleanup * separate caches for asset and node ids * initial test for triggering callback to switch on off rule base styling loading * add loading spinner and some ui changes * split into different useEffects hooks to let render the spinner sooner * dont use spinner on the reset button * cleanup * lint and remove unused function * missed call to show up outputs panel * changes from cr * changes from cr - splitting out selector component into smaller pieces * move extract asset id from mapped to the general hooks folder * move _amountOfAssetIdsChunks to a private readonly parameter * use the correct map key for asset ids * refactoring from cr and using the resource context provider to link styling loading to rule base * refactoring and splitting asset mapping caches into smaller classes * lint * turn splitChunkInCacheNode3D to private * more refactoring - cr * lint * removing not used rule based callback * populate the both caches * move hooks into the hooks sub folder * update import for Reveal3DResourcesInfoContext * minor refactoring to force spinner more stable - still not fully but a bit better --- .../AssetMappingAndNode3DCache.ts | 363 ++++++++++++++++++ ...=> AssetMappingAndNode3DCacheProvider.tsx} | 89 ++++- .../CacheProvider/AssetMappingCache.ts | 203 ---------- .../AssetMappingPerAssetIdCache.ts | 37 ++ .../AssetMappingPerModelCache.ts | 52 +++ .../AssetMappingPerNodeIdCache.ts | 37 ++ .../CacheProvider/Node3DPerNodeIdCache.ts | 81 ++++ .../src/components/CacheProvider/types.ts | 5 + .../src/components/CacheProvider/utils.ts | 7 +- .../CadModelContainer/CadModelContainer.tsx | 2 +- .../Image360CollectionContainer.tsx | 2 +- .../PointCloudContainer.tsx | 2 +- .../Reveal3DResources/Reveal3DResources.tsx | 30 +- .../Reveal3DResourcesCountContext.tsx | 49 --- .../Reveal3DResourcesInfoContext.tsx | 74 ++++ .../RevealContext/RevealContext.tsx | 12 +- .../RevealKeepAlive/RevealKeepAlive.tsx | 4 +- .../RevealKeepAlive/RevealKeepAliveContext.ts | 4 +- .../AssetContextualizedButton.tsx | 2 +- .../RevealToolbar/RuleBasedOutputsButton.tsx | 90 +++-- .../RuleBasedOutputsSelector.tsx | 139 ++++--- .../components/RuleBasedSelectionItem.tsx | 21 +- .../useConvertAssetMetadatasToLowerCase.tsx | 18 + .../useExtractTimeseriesIdsFromRuleSet.tsx | 17 + .../useExtractUniqueAssetIdsFromMapped.tsx | 23 ++ .../hooks/useFetchRuleInstances.tsx | 2 +- .../src/components/RuleBasedOutputs/utils.ts | 110 +++--- react-components/src/hooks/use3dModels.ts | 2 +- .../src/hooks/useCalculateModelsStyling.tsx | 78 ++-- react-components/src/hooks/useClickedNode.tsx | 4 +- .../useCreateAssetMappingsMapPerModel.tsx | 33 ++ .../src/hooks/useImage360Collections.ts | 2 +- .../useSearchMappedEquipmentAssetMappings.tsx | 1 - .../convertAssetMetadataToLowerCase.ts | 15 + .../utilities/RevealStoryContainer.tsx | 4 +- .../components/RevealContainer.test.tsx | 4 +- 36 files changed, 1138 insertions(+), 480 deletions(-) create mode 100644 react-components/src/components/CacheProvider/AssetMappingAndNode3DCache.ts rename react-components/src/components/CacheProvider/{AssetMappingCacheProvider.tsx => AssetMappingAndNode3DCacheProvider.tsx} (57%) delete mode 100644 react-components/src/components/CacheProvider/AssetMappingCache.ts create mode 100644 react-components/src/components/CacheProvider/AssetMappingPerAssetIdCache.ts create mode 100644 react-components/src/components/CacheProvider/AssetMappingPerModelCache.ts create mode 100644 react-components/src/components/CacheProvider/AssetMappingPerNodeIdCache.ts create mode 100644 react-components/src/components/CacheProvider/Node3DPerNodeIdCache.ts delete mode 100644 react-components/src/components/Reveal3DResources/Reveal3DResourcesCountContext.tsx create mode 100644 react-components/src/components/Reveal3DResources/Reveal3DResourcesInfoContext.tsx create mode 100644 react-components/src/components/RuleBasedOutputs/hooks/useConvertAssetMetadatasToLowerCase.tsx create mode 100644 react-components/src/components/RuleBasedOutputs/hooks/useExtractTimeseriesIdsFromRuleSet.tsx create mode 100644 react-components/src/components/RuleBasedOutputs/hooks/useExtractUniqueAssetIdsFromMapped.tsx create mode 100644 react-components/src/hooks/useCreateAssetMappingsMapPerModel.tsx create mode 100644 react-components/src/utilities/convertAssetMetadataToLowerCase.ts diff --git a/react-components/src/components/CacheProvider/AssetMappingAndNode3DCache.ts b/react-components/src/components/CacheProvider/AssetMappingAndNode3DCache.ts new file mode 100644 index 00000000000..2b6cb7aac18 --- /dev/null +++ b/react-components/src/components/CacheProvider/AssetMappingAndNode3DCache.ts @@ -0,0 +1,363 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { + type CogniteClient, + type AssetMapping3D, + type Node3D, + type CogniteInternalId +} from '@cognite/sdk'; +import { + type ModelNodeIdKey, + type AssetId, + type ModelId, + type RevisionId, + type ChunkInCacheTypes, + type ModelAssetIdKey +} from './types'; +import { chunk, maxBy } from 'lodash'; +import assert from 'assert'; +import { isValidAssetMapping, modelRevisionNodesAssetsToKey, modelRevisionToKey } from './utils'; +import { type ModelWithAssetMappings } from './AssetMappingAndNode3DCacheProvider'; +import { AssetMappingPerAssetIdCache } from './AssetMappingPerAssetIdCache'; +import { AssetMappingPerNodeIdCache } from './AssetMappingPerNodeIdCache'; +import { Node3DPerNodeIdCache } from './Node3DPerNodeIdCache'; +import { AssetMappingPerModelCache } from './AssetMappingPerModelCache'; + +export type NodeAssetMappingResult = { node?: Node3D; mappings: AssetMapping[] }; + +export type AssetMapping = Required; +export class AssetMappingAndNode3DCache { + private readonly _sdk: CogniteClient; + + private readonly modelToAssetMappingsCache: AssetMappingPerModelCache; + + private readonly assetIdsToAssetMappingCache: AssetMappingPerAssetIdCache; + + private readonly nodeIdsToAssetMappingCache: AssetMappingPerNodeIdCache; + + private readonly nodeIdsToNode3DCache: Node3DPerNodeIdCache; + + private readonly _amountOfAssetIdsChunks = 1; + + constructor(sdk: CogniteClient) { + this._sdk = sdk; + this.assetIdsToAssetMappingCache = new AssetMappingPerAssetIdCache(); + this.nodeIdsToAssetMappingCache = new AssetMappingPerNodeIdCache(); + this.modelToAssetMappingsCache = new AssetMappingPerModelCache(this._sdk); + this.nodeIdsToNode3DCache = new Node3DPerNodeIdCache(this._sdk); + } + + public async getAssetMappingsForLowestAncestor( + modelId: ModelId, + revisionId: RevisionId, + ancestors: Node3D[] + ): Promise { + if (ancestors.length === 0) { + return { mappings: [] }; + } + + const searchTreeIndices = new Set(ancestors.map((ancestor) => ancestor.treeIndex)); + const allNodeMappings = await this.getAssetMappingsForNodes(modelId, revisionId, ancestors); + + const relevantMappings = allNodeMappings.filter((mapping) => + searchTreeIndices.has(mapping.treeIndex) + ); + + if (relevantMappings.length === 0) { + return { mappings: [] }; + } + + const maxRelevantMappingTreeIndex = maxBy( + relevantMappings, + (mapping) => mapping.treeIndex + )?.treeIndex; + + assert(maxRelevantMappingTreeIndex !== undefined); + + const mappingsOfNearestAncestor = relevantMappings.filter( + (mapping) => mapping.treeIndex === maxRelevantMappingTreeIndex + ); + + const nearestMappedAncestor = ancestors.find( + (node) => node.treeIndex === maxRelevantMappingTreeIndex + ); + assert(nearestMappedAncestor !== undefined); + + return { node: nearestMappedAncestor, mappings: mappingsOfNearestAncestor }; + } + + public async getNodesForAssetIds( + modelId: ModelId, + revisionId: RevisionId, + assetIds: CogniteInternalId[] + ): Promise> { + const relevantAssetIds = new Set(assetIds); + + const assetIdsList = Array.from(relevantAssetIds); + const chunkSize = Math.round(assetIdsList.length / this._amountOfAssetIdsChunks); + const listChunks = chunk(assetIdsList, chunkSize); + + const allAssetMappingsReturned = listChunks.map(async (itemChunk) => { + const assetMappings = await this.getAssetMappingsForAssetIds(modelId, revisionId, itemChunk); + return assetMappings; + }); + + const allAssetMappings = await Promise.all(allAssetMappingsReturned); + const assetMappings = allAssetMappings.flat(); + + const relevantAssetMappings = assetMappings.filter((mapping) => + relevantAssetIds.has(mapping.assetId) + ); + + const nodes = await this.nodeIdsToNode3DCache.getNodesForNodeIds( + modelId, + revisionId, + relevantAssetMappings.map((assetMapping) => assetMapping.nodeId) + ); + + return nodes.reduce((acc, node, index) => { + const key = relevantAssetMappings[index].assetId; + const nodesForAsset = acc.get(key); + + if (nodesForAsset !== undefined) { + nodesForAsset.push(node); + } else { + acc.set(key, [node]); + } + + return acc; + }, new Map()); + } + + public async generateNode3DCachePerItem( + modelId: ModelId, + revisionId: RevisionId, + nodeIds: number[] | undefined + ): Promise { + await this.nodeIdsToNode3DCache.generateNode3DCachePerItem(modelId, revisionId, nodeIds); + } + + public async generateAssetMappingsCachePerItemFromModelCache( + modelId: ModelId, + revisionId: RevisionId, + assetMappingsPerModel: ModelWithAssetMappings[] | undefined + ): Promise { + if (assetMappingsPerModel === undefined) { + return; + } + assetMappingsPerModel.forEach(async (modelMapping) => { + modelMapping.assetMappings.forEach(async (item) => { + const key = modelRevisionNodesAssetsToKey(modelId, revisionId, [item.assetId]); + await this.assetIdsToAssetMappingCache.setAssetMappingsCacheItem(key, item); + }); + }); + } + + public async getAssetMappingsForModel( + modelId: ModelId, + revisionId: RevisionId + ): Promise { + const key = modelRevisionToKey(modelId, revisionId); + const cachedResult = await this.modelToAssetMappingsCache.getModelToAssetMappingCacheItems(key); + + if (cachedResult !== undefined) { + return cachedResult; + } + + return await this.modelToAssetMappingsCache.fetchAndCacheMappingsForModel(modelId, revisionId); + } + + private async splitChunkInCacheAssetMappings( + currentChunk: number[], + modelId: ModelId, + revisionId: RevisionId, + type: string + ): Promise> { + const chunkInCache: Array> = []; + const chunkNotCached: number[] = []; + + await Promise.all( + currentChunk.map(async (id) => { + const key = modelRevisionNodesAssetsToKey(modelId, revisionId, [id]); + const cachedResult = await this.getItemCacheResult(type, key); + if (cachedResult !== undefined) { + chunkInCache.push(...cachedResult); + } else { + chunkNotCached.push(id); + } + }) + ); + + return { chunkInCache, chunkNotInCache: chunkNotCached }; + } + + private async getItemCacheResult( + type: string, + key: ModelNodeIdKey | ModelAssetIdKey + ): Promise { + return type === 'nodeIds' + ? await this.nodeIdsToAssetMappingCache.getNodeIdsToAssetMappingCacheItem(key) + : await this.assetIdsToAssetMappingCache.getAssetIdsToAssetMappingCacheItem(key); + } + + private setItemCacheResult( + type: string, + key: ModelNodeIdKey | ModelAssetIdKey, + item: AssetMapping[] | undefined + ): void { + const value = Promise.resolve(item ?? []); + type === 'nodeIds' + ? this.nodeIdsToAssetMappingCache.setNodeIdsToAssetMappingCacheItem(key, value) + : this.assetIdsToAssetMappingCache.setAssetIdsToAssetMappingCacheItem(key, value); + } + + private async fetchAssetMappingsRequest( + currentChunk: number[], + filterType: string, + modelId: ModelId, + revisionId: RevisionId + ): Promise { + let assetMapping3D: AssetMapping3D[] = []; + + if (currentChunk.length === 0) { + return []; + } + const filter = + filterType === 'nodeIds' ? { nodeIds: currentChunk } : { assetIds: currentChunk }; + + assetMapping3D = await this._sdk.assetMappings3D + .filter(modelId, revisionId, { + limit: 1000, + filter + }) + .autoPagingToArray({ limit: Infinity }); + + assetMapping3D.forEach(async (item) => { + const keyAssetId: ModelAssetIdKey = modelRevisionNodesAssetsToKey(modelId, revisionId, [ + item.assetId + ]); + const keyNodeId: ModelNodeIdKey = modelRevisionNodesAssetsToKey(modelId, revisionId, [ + item.nodeId + ]); + await this.assetIdsToAssetMappingCache.setAssetMappingsCacheItem(keyAssetId, item); + await this.nodeIdsToAssetMappingCache.setAssetMappingsCacheItem(keyNodeId, item); + }); + + currentChunk.forEach(async (id) => { + const key = modelRevisionNodesAssetsToKey(modelId, revisionId, [id]); + const cachedResult = await this.getItemCacheResult(filterType, key); + + if (cachedResult === undefined) { + this.setItemCacheResult(filterType, key, []); + } + }); + + return assetMapping3D.filter(isValidAssetMapping); + } + + private async fetchMappingsInQueue( + index: number, + idChunks: number[][], + filterType: string, + modelId: ModelId, + revisionId: RevisionId, + assetMappingsList: Array> + ): Promise { + const assetMappings = await this.fetchAssetMappingsRequest( + idChunks[index], + filterType, + modelId, + revisionId + ); + + assetMappingsList = assetMappingsList.concat(assetMappings); + if (index >= idChunks.length - 1) { + return assetMappingsList; + } + + const nextIndex = index + 1; + return await this.fetchMappingsInQueue( + nextIndex, + idChunks, + filterType, + modelId, + revisionId, + assetMappingsList + ); + } + + private async fetchAndCacheMappingsForIds( + modelId: ModelId, + revisionId: RevisionId, + ids: number[], + filterType: string + ): Promise { + if (ids.length === 0) { + return []; + } + const idChunks = chunk(ids, 100); + const initialIndex = 0; + const assetMappings = await this.fetchMappingsInQueue( + initialIndex, + idChunks, + filterType, + modelId, + revisionId, + [] + ); + return assetMappings; + } + + private async getAssetMappingsForNodes( + modelId: ModelId, + revisionId: RevisionId, + nodes: Node3D[] + ): Promise { + const nodeIds = nodes.map((node) => node.id); + + const { chunkNotInCache, chunkInCache } = await this.splitChunkInCacheAssetMappings( + nodeIds, + modelId, + revisionId, + 'nodeIds' + ); + + const notCachedNodeIds: number[] = chunkNotInCache; + + const assetMappings = await this.fetchAndCacheMappingsForIds( + modelId, + revisionId, + notCachedNodeIds, + 'nodeIds' + ); + + const allAssetMappings = chunkInCache.concat(assetMappings); + return allAssetMappings; + } + + private async getAssetMappingsForAssetIds( + modelId: ModelId, + revisionId: RevisionId, + assetIds: number[] + ): Promise { + const { chunkNotInCache, chunkInCache } = await this.splitChunkInCacheAssetMappings( + assetIds, + modelId, + revisionId, + 'assetIds' + ); + + const notCachedAssetIds: number[] = chunkNotInCache; + + const assetMappings = await this.fetchAndCacheMappingsForIds( + modelId, + revisionId, + notCachedAssetIds, + 'assetIds' + ); + const allAssetMappings = chunkInCache.concat(assetMappings); + return allAssetMappings; + } +} diff --git a/react-components/src/components/CacheProvider/AssetMappingCacheProvider.tsx b/react-components/src/components/CacheProvider/AssetMappingAndNode3DCacheProvider.tsx similarity index 57% rename from react-components/src/components/CacheProvider/AssetMappingCacheProvider.tsx rename to react-components/src/components/CacheProvider/AssetMappingAndNode3DCacheProvider.tsx index 364d5621296..768a1a27bf2 100644 --- a/react-components/src/components/CacheProvider/AssetMappingCacheProvider.tsx +++ b/react-components/src/components/CacheProvider/AssetMappingAndNode3DCacheProvider.tsx @@ -6,9 +6,9 @@ import { type ReactElement, type ReactNode, createContext, useContext, useMemo } import { type CadModelOptions } from '../Reveal3DResources/types'; import { type AssetMapping, - AssetMappingCache, + AssetMappingAndNode3DCache, type NodeAssetMappingResult -} from './AssetMappingCache'; +} from './AssetMappingAndNode3DCache'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; import { type CogniteInternalId } from '@cognite/sdk'; import { useSDK } from '../RevealCanvas/SDKProvider'; @@ -17,8 +17,8 @@ import { type ModelRevisionId, type ModelRevisionAssetNodesResult } from './type import { fetchAncestorNodesForTreeIndex } from './requests'; import { type AnyIntersection } from '@cognite/reveal'; -export type AssetMappingCacheContent = { - cache: AssetMappingCache; +export type AssetMappingAndNode3DCacheContent = { + cache: AssetMappingAndNode3DCache; }; export type ModelWithAssetMappings = { @@ -26,22 +26,71 @@ export type ModelWithAssetMappings = { assetMappings: AssetMapping[]; }; -const AssetMappingCacheContext = createContext(undefined); +const AssetMappingAndNode3DCacheContext = createContext< + AssetMappingAndNode3DCacheContent | undefined +>(undefined); -const useAssetMappingCache = (): AssetMappingCache => { - const content = useContext(AssetMappingCacheContext); +const useAssetMappingAndNode3DCache = (): AssetMappingAndNode3DCache => { + const content = useContext(AssetMappingAndNode3DCacheContext); if (content === undefined) { - throw Error('Must use useAssetMappingCache inside a AssetMappingCacheContext'); + throw Error('Must use useAssetMappingAndNode3DCache inside a AssetMappingCacheContext'); } return content.cache; }; +export const useGenerateNode3DCache = ( + cadModelOptions: CadModelOptions[], + assetMappings: ModelWithAssetMappings[] | undefined +): void => { + const assetMappingAndNode3DCache = useAssetMappingAndNode3DCache(); + + useMemo(() => { + cadModelOptions.forEach(async ({ modelId, revisionId }) => { + const assetMapping = assetMappings?.filter( + (item) => item.model.modelId === modelId && item.model.revisionId === revisionId + ); + const nodeIdsFromAssetMappings = assetMapping?.flatMap((item) => + item.assetMappings.map((mapping) => mapping.nodeId) + ); + + if (nodeIdsFromAssetMappings === undefined || nodeIdsFromAssetMappings.length === 0) return; + + await assetMappingAndNode3DCache.generateNode3DCachePerItem( + modelId, + revisionId, + nodeIdsFromAssetMappings + ); + }); + }, [cadModelOptions, assetMappings]); +}; + +export const useGenerateAssetMappingCachePerItemFromModelCache = ( + cadModelOptions: CadModelOptions[], + assetMappings: ModelWithAssetMappings[] | undefined +): void => { + const assetMappingAndNode3DCache = useAssetMappingAndNode3DCache(); + useMemo(() => { + cadModelOptions.forEach(async ({ modelId, revisionId }) => { + const assetMapping = assetMappings?.filter( + (item) => item.model.modelId === modelId && item.model.revisionId === revisionId + ); + if (assetMapping !== undefined && assetMapping.length > 0) { + await assetMappingAndNode3DCache.generateAssetMappingsCachePerItemFromModelCache( + modelId, + revisionId, + assetMapping + ); + } + }); + }, [cadModelOptions, assetMappings]); +}; + export const useAssetMappedNodesForRevisions = ( cadModels: CadModelOptions[] ): UseQueryResult => { - const assetMappingCache = useAssetMappingCache(); + const assetMappingAndNode3DCache = useAssetMappingAndNode3DCache(); return useQuery({ queryKey: [ @@ -53,7 +102,7 @@ export const useAssetMappedNodesForRevisions = ( queryFn: async () => { const fetchPromises = cadModels.map( async (model) => - await assetMappingCache + await assetMappingAndNode3DCache .getAssetMappingsForModel(model.modelId, model.revisionId) .then((assetMappings) => ({ model, assetMappings })) ); @@ -68,7 +117,7 @@ export const useNodesForAssets = ( models: ModelRevisionId[], assetIds: CogniteInternalId[] ): UseQueryResult => { - const assetMappingCache = useAssetMappingCache(); + const assetMappingAndNode3DCache = useAssetMappingAndNode3DCache(); return useQuery({ queryKey: [ @@ -80,7 +129,7 @@ export const useNodesForAssets = ( ], queryFn: async () => { const modelAndNodeMapPromises = models.map(async (model) => { - const nodeMap = await assetMappingCache.getNodesForAssetIds( + const nodeMap = await assetMappingAndNode3DCache.getNodesForAssetIds( model.modelId, model.revisionId, assetIds @@ -98,7 +147,7 @@ export const useNodesForAssets = ( export const useAssetMappingForTreeIndex = ( intersection: AnyIntersection | undefined ): UseQueryResult => { - const assetMappingCache = useAssetMappingCache(); + const assetMappingAndNode3DCache = useAssetMappingAndNode3DCache(); const cdfClient = useSDK(); const isCadModel = intersection?.type === 'cad'; @@ -130,7 +179,7 @@ export const useAssetMappingForTreeIndex = ( cdfClient ); - return await assetMappingCache.getAssetMappingsForLowestAncestor( + return await assetMappingAndNode3DCache.getAssetMappingsForLowestAncestor( modelId, revisionId, ancestors @@ -140,13 +189,17 @@ export const useAssetMappingForTreeIndex = ( }); }; -export function AssetMappingCacheProvider({ children }: { children?: ReactNode }): ReactElement { +export function AssetMappingAndNode3DCacheProvider({ + children +}: { + children?: ReactNode; +}): ReactElement { const cdfClient = useSDK(); const revealKeepAliveData = useRevealKeepAlive(); const fdmCache = useMemo(() => { const cache = - revealKeepAliveData?.assetMappingCache.current ?? new AssetMappingCache(cdfClient); + revealKeepAliveData?.assetMappingCache.current ?? new AssetMappingAndNode3DCache(cdfClient); const isRevealKeepAliveContextProvided = revealKeepAliveData !== undefined; if (isRevealKeepAliveContextProvided) { @@ -157,8 +210,8 @@ export function AssetMappingCacheProvider({ children }: { children?: ReactNode } }, [cdfClient]); return ( - + {children} - + ); } diff --git a/react-components/src/components/CacheProvider/AssetMappingCache.ts b/react-components/src/components/CacheProvider/AssetMappingCache.ts deleted file mode 100644 index 49fdad14422..00000000000 --- a/react-components/src/components/CacheProvider/AssetMappingCache.ts +++ /dev/null @@ -1,203 +0,0 @@ -/*! - * Copyright 2023 Cognite AS - */ - -import { - type CogniteClient, - type AssetMapping3D, - type Node3D, - type CogniteInternalId -} from '@cognite/sdk'; -import { - type ModelNodeIdKey, - type AssetId, - type ModelId, - type ModelRevisionKey, - type RevisionId -} from './types'; -import { chunk, maxBy } from 'lodash'; -import assert from 'assert'; -import { fetchNodesForNodeIds } from './requests'; -import { modelRevisionNodesAssetsToKey, modelRevisionToKey } from './utils'; - -export type NodeAssetMappingResult = { node?: Node3D; mappings: AssetMapping[] }; - -export type AssetMapping = Required; - -export class AssetMappingCache { - private readonly _sdk: CogniteClient; - - private readonly _modelToAssetMappings = new Map>(); - private readonly _nodeAssetIdsToAssetMappings = new Map< - ModelNodeIdKey, - Promise - >(); - - constructor(sdk: CogniteClient) { - this._sdk = sdk; - } - - public async getAssetMappingsForLowestAncestor( - modelId: ModelId, - revisionId: RevisionId, - ancestors: Node3D[] - ): Promise { - if (ancestors.length === 0) { - return { mappings: [] }; - } - - const searchTreeIndices = new Set(ancestors.map((ancestor) => ancestor.treeIndex)); - const allNodeMappings = await this.getAssetMappingsForNodes(modelId, revisionId, ancestors); - - const relevantMappings = allNodeMappings.filter((mapping) => - searchTreeIndices.has(mapping.treeIndex) - ); - - if (relevantMappings.length === 0) { - return { mappings: [] }; - } - - const maxRelevantMappingTreeIndex = maxBy( - relevantMappings, - (mapping) => mapping.treeIndex - )?.treeIndex; - - assert(maxRelevantMappingTreeIndex !== undefined); - - const mappingsOfNearestAncestor = relevantMappings.filter( - (mapping) => mapping.treeIndex === maxRelevantMappingTreeIndex - ); - - const nearestMappedAncestor = ancestors.find( - (node) => node.treeIndex === maxRelevantMappingTreeIndex - ); - assert(nearestMappedAncestor !== undefined); - - return { node: nearestMappedAncestor, mappings: mappingsOfNearestAncestor }; - } - - public async getNodesForAssetIds( - modelId: ModelId, - revisionId: RevisionId, - assetIds: CogniteInternalId[] - ): Promise> { - const assetMappings = await this.getAssetMappingsForAssetIds(modelId, revisionId, assetIds); - const relevantAssetIds = new Set(assetIds); - - const relevantAssetMappings = assetMappings.filter((mapping) => - relevantAssetIds.has(mapping.assetId) - ); - - const nodes = await fetchNodesForNodeIds( - modelId, - revisionId, - relevantAssetMappings.map((assetMapping) => assetMapping.nodeId), - this._sdk - ); - - return nodes.reduce((acc, node, index) => { - const key = relevantAssetMappings[index].assetId; - const nodesForAsset = acc.get(key); - - if (nodesForAsset !== undefined) { - nodesForAsset.push(node); - } else { - acc.set(key, [node]); - } - - return acc; - }, new Map()); - } - - public async getAssetMappingsForModel( - modelId: ModelId, - revisionId: RevisionId - ): Promise { - const key = modelRevisionToKey(modelId, revisionId); - const cachedResult = this._modelToAssetMappings.get(key); - - if (cachedResult !== undefined) { - return await cachedResult; - } - - return await this.fetchAndCacheMappingsForModel(modelId, revisionId); - } - - private async fetchAndCacheMappingsForIds( - modelId: ModelId, - revisionId: RevisionId, - ids: number[], - filterType: string - ): Promise { - const key: ModelNodeIdKey = modelRevisionNodesAssetsToKey(modelId, revisionId, ids); - const idChunks = chunk(ids, 100); - const assetMappingsPromises = idChunks.map(async (idChunk) => { - const filter = filterType === 'nodeIds' ? { nodeIds: idChunk } : { assetIds: idChunk }; - return await this._sdk.assetMappings3D - .filter(modelId, revisionId, { - limit: 1000, - filter - }) - .autoPagingToArray({ limit: Infinity }); - }); - const assetMappingsArrays = await Promise.all(assetMappingsPromises); - const assetMappings = assetMappingsArrays.flat(); - this._nodeAssetIdsToAssetMappings.set(key, Promise.resolve(assetMappings)); - return assetMappings; - } - - private async getAssetMappingsForNodes( - modelId: ModelId, - revisionId: RevisionId, - nodes: Node3D[] - ): Promise { - const nodeIds = nodes.map((node) => node.id); - const key: ModelNodeIdKey = modelRevisionNodesAssetsToKey(modelId, revisionId, nodeIds); - const cachedResult = this._nodeAssetIdsToAssetMappings.get(key); - - if (cachedResult !== undefined) { - return await cachedResult; - } - return await this.fetchAndCacheMappingsForIds(modelId, revisionId, nodeIds, 'nodeIds'); - } - - private async getAssetMappingsForAssetIds( - modelId: ModelId, - revisionId: RevisionId, - assetIds: number[] - ): Promise { - const key: ModelNodeIdKey = modelRevisionNodesAssetsToKey(modelId, revisionId, assetIds); - const cachedResult = this._nodeAssetIdsToAssetMappings.get(key); - - if (cachedResult !== undefined) { - return await cachedResult; - } - return await this.fetchAndCacheMappingsForIds(modelId, revisionId, assetIds, 'assetIds'); - } - - private async fetchAndCacheMappingsForModel( - modelId: ModelId, - revisionId: RevisionId - ): Promise { - const key = modelRevisionToKey(modelId, revisionId); - const assetMappings = this.fetchAssetMappingsForModel(modelId, revisionId); - - this._modelToAssetMappings.set(key, assetMappings); - return await assetMappings; - } - - private async fetchAssetMappingsForModel( - modelId: ModelId, - revisionId: RevisionId - ): Promise { - const assetMapping3D = await this._sdk.assetMappings3D - .list(modelId, revisionId, { limit: 1000 }) - .autoPagingToArray({ limit: Infinity }); - - return assetMapping3D.filter(isValidAssetMapping); - } -} - -function isValidAssetMapping(assetMapping: AssetMapping3D): assetMapping is AssetMapping { - return assetMapping.treeIndex !== undefined && assetMapping.subtreeSize !== undefined; -} diff --git a/react-components/src/components/CacheProvider/AssetMappingPerAssetIdCache.ts b/react-components/src/components/CacheProvider/AssetMappingPerAssetIdCache.ts new file mode 100644 index 00000000000..83c9512d253 --- /dev/null +++ b/react-components/src/components/CacheProvider/AssetMappingPerAssetIdCache.ts @@ -0,0 +1,37 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type AssetMapping3D } from '@cognite/sdk/dist/src'; +import { type ModelAssetIdKey } from './types'; +import { type AssetMapping } from './AssetMappingAndNode3DCache'; + +export class AssetMappingPerAssetIdCache { + private readonly _assetIdsToAssetMappings = new Map>(); + + public setAssetIdsToAssetMappingCacheItem( + key: ModelAssetIdKey, + item: Promise>> + ): void { + this._assetIdsToAssetMappings.set(key, Promise.resolve(item)); + } + + public async getAssetIdsToAssetMappingCacheItem( + key: ModelAssetIdKey + ): Promise { + return await this._assetIdsToAssetMappings.get(key); + } + + public async setAssetMappingsCacheItem(key: ModelAssetIdKey, item: AssetMapping): Promise { + const currentAssetMappings = this.getAssetIdsToAssetMappingCacheItem(key); + this.setAssetIdsToAssetMappingCacheItem( + key, + currentAssetMappings.then((value) => { + if (value === undefined) { + return [item]; + } + value.push(item); + return value; + }) + ); + } +} diff --git a/react-components/src/components/CacheProvider/AssetMappingPerModelCache.ts b/react-components/src/components/CacheProvider/AssetMappingPerModelCache.ts new file mode 100644 index 00000000000..2e6b0565920 --- /dev/null +++ b/react-components/src/components/CacheProvider/AssetMappingPerModelCache.ts @@ -0,0 +1,52 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type CogniteClient, type AssetMapping3D } from '@cognite/sdk/dist/src'; +import { type ModelId, type RevisionId, type ModelRevisionKey } from './types'; +import { type AssetMapping } from './AssetMappingAndNode3DCache'; +import { isValidAssetMapping, modelRevisionToKey } from './utils'; + +export class AssetMappingPerModelCache { + private readonly _sdk: CogniteClient; + + private readonly _modelToAssetMappings = new Map>(); + + constructor(sdk: CogniteClient) { + this._sdk = sdk; + } + + public setModelToAssetMappingCacheItems( + key: ModelRevisionKey, + assetMappings: Promise>> + ): void { + this._modelToAssetMappings.set(key, assetMappings); + } + + public async getModelToAssetMappingCacheItems( + key: ModelRevisionKey + ): Promise { + return await this._modelToAssetMappings.get(key); + } + + public async fetchAndCacheMappingsForModel( + modelId: ModelId, + revisionId: RevisionId + ): Promise { + const key = modelRevisionToKey(modelId, revisionId); + const assetMappings = this.fetchAssetMappingsForModel(modelId, revisionId); + + this.setModelToAssetMappingCacheItems(key, assetMappings); + return await assetMappings; + } + + private async fetchAssetMappingsForModel( + modelId: ModelId, + revisionId: RevisionId + ): Promise { + const assetMapping3D = await this._sdk.assetMappings3D + .list(modelId, revisionId, { limit: 1000 }) + .autoPagingToArray({ limit: Infinity }); + + return assetMapping3D.filter(isValidAssetMapping); + } +} diff --git a/react-components/src/components/CacheProvider/AssetMappingPerNodeIdCache.ts b/react-components/src/components/CacheProvider/AssetMappingPerNodeIdCache.ts new file mode 100644 index 00000000000..67299645b11 --- /dev/null +++ b/react-components/src/components/CacheProvider/AssetMappingPerNodeIdCache.ts @@ -0,0 +1,37 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type AssetMapping3D } from '@cognite/sdk/dist/src'; +import { type ModelNodeIdKey } from './types'; +import { type AssetMapping } from './AssetMappingAndNode3DCache'; + +export class AssetMappingPerNodeIdCache { + private readonly _nodeIdsToAssetMappings = new Map>(); + + public setNodeIdsToAssetMappingCacheItem( + key: ModelNodeIdKey, + item: Promise>> + ): void { + this._nodeIdsToAssetMappings.set(key, Promise.resolve(item)); + } + + public async getNodeIdsToAssetMappingCacheItem( + key: ModelNodeIdKey + ): Promise { + return await this._nodeIdsToAssetMappings.get(key); + } + + public async setAssetMappingsCacheItem(key: ModelNodeIdKey, item: AssetMapping): Promise { + const currentAssetMappings = this.getNodeIdsToAssetMappingCacheItem(key); + this.setNodeIdsToAssetMappingCacheItem( + key, + currentAssetMappings.then((value) => { + if (value === undefined) { + return [item]; + } + value.push(item); + return value; + }) + ); + } +} diff --git a/react-components/src/components/CacheProvider/Node3DPerNodeIdCache.ts b/react-components/src/components/CacheProvider/Node3DPerNodeIdCache.ts new file mode 100644 index 00000000000..62d8cc53f30 --- /dev/null +++ b/react-components/src/components/CacheProvider/Node3DPerNodeIdCache.ts @@ -0,0 +1,81 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type Node3D, type CogniteClient } from '@cognite/sdk'; +import { + type ChunkInCacheTypes, + type ModelId, + type RevisionId, + type ModelNodeIdKey +} from './types'; +import { modelRevisionNodesAssetsToKey } from './utils'; +import { fetchNodesForNodeIds } from './requests'; + +export class Node3DPerNodeIdCache { + private readonly _sdk: CogniteClient; + + private readonly _nodeIdsToNode3D = new Map>(); + + constructor(sdk: CogniteClient) { + this._sdk = sdk; + } + + private async splitChunkInCacheNode3D( + currentChunk: number[], + modelId: ModelId, + revisionId: RevisionId + ): Promise> { + const chunkInCache: Node3D[] = []; + const chunkNotCached: number[] = []; + + await Promise.all( + currentChunk.map(async (id) => { + const key = modelRevisionNodesAssetsToKey(modelId, revisionId, [id]); + const cachedResult = await this.getNodeIdToNode3DCacheItem(key); + if (cachedResult !== undefined) { + chunkInCache.push(cachedResult); + } else { + chunkNotCached.push(id); + } + }) + ); + + return { chunkInCache, chunkNotInCache: chunkNotCached }; + } + + public async generateNode3DCachePerItem( + modelId: ModelId, + revisionId: RevisionId, + nodeIds: number[] | undefined + ): Promise { + const node3Ds = await this.getNodesForNodeIds(modelId, revisionId, nodeIds ?? []); + node3Ds.forEach((node) => { + const key = modelRevisionNodesAssetsToKey(modelId, revisionId, [node.id]); + this.setNodeIdToNode3DCacheItem(key, Promise.resolve(node)); + }); + } + + public async getNodesForNodeIds( + modelId: ModelId, + revisionId: RevisionId, + nodeIds: number[] + ): Promise { + const { chunkNotInCache, chunkInCache } = await this.splitChunkInCacheNode3D( + nodeIds, + modelId, + revisionId + ); + + const nodes = await fetchNodesForNodeIds(modelId, revisionId, chunkNotInCache, this._sdk); + const allNodes = chunkInCache.concat(nodes); + return allNodes; + } + + public async getNodeIdToNode3DCacheItem(key: ModelNodeIdKey): Promise { + return await this._nodeIdsToNode3D.get(key); + } + + public setNodeIdToNode3DCacheItem(key: ModelNodeIdKey, item: Promise): void { + this._nodeIdsToNode3D.set(key, Promise.resolve(item)); + } +} diff --git a/react-components/src/components/CacheProvider/types.ts b/react-components/src/components/CacheProvider/types.ts index c8cdece601c..e1ac16a90c4 100644 --- a/react-components/src/components/CacheProvider/types.ts +++ b/react-components/src/components/CacheProvider/types.ts @@ -64,3 +64,8 @@ export type Image360AnnotationAssetInfo = { }; export type AnnotationId = number; + +export type ChunkInCacheTypes = { + chunkInCache: ObjectType[]; + chunkNotInCache: number[]; +}; diff --git a/react-components/src/components/CacheProvider/utils.ts b/react-components/src/components/CacheProvider/utils.ts index 7befbd370bb..4b12ee628e7 100644 --- a/react-components/src/components/CacheProvider/utils.ts +++ b/react-components/src/components/CacheProvider/utils.ts @@ -6,7 +6,8 @@ import { type AnnotationsCogniteAnnotationTypesImagesAssetLink, type AnnotationModel, type AnnotationsBoundingVolume, - type CogniteInternalId + type CogniteInternalId, + type AssetMapping3D } from '@cognite/sdk'; import { type ModelRevisionId, @@ -50,3 +51,7 @@ export function getAssetIdOrExternalIdFromImage360Annotation( const annotationData = annotation.data as AnnotationsCogniteAnnotationTypesImagesAssetLink; return annotationData.assetRef?.id ?? annotationData.assetRef?.externalId; } + +export function isValidAssetMapping(assetMapping: AssetMapping3D): assetMapping is AssetMapping3D { + return assetMapping.treeIndex !== undefined && assetMapping.subtreeSize !== undefined; +} diff --git a/react-components/src/components/CadModelContainer/CadModelContainer.tsx b/react-components/src/components/CadModelContainer/CadModelContainer.tsx index fca8f049347..c192f69c964 100644 --- a/react-components/src/components/CadModelContainer/CadModelContainer.tsx +++ b/react-components/src/components/CadModelContainer/CadModelContainer.tsx @@ -7,7 +7,7 @@ import { useReveal } from '../RevealCanvas/ViewerContext'; import { Matrix4 } from 'three'; import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; import { type CadModelStyling, useApplyCadModelStyling } from './useApplyCadModelStyling'; -import { useReveal3DResourcesCount } from '../Reveal3DResources/Reveal3DResourcesCountContext'; +import { useReveal3DResourcesCount } from '../Reveal3DResources/Reveal3DResourcesInfoContext'; import { isEqual } from 'lodash'; import { modelExists } from '../../utilities/modelExists'; import { getViewerResourceCount } from '../../utilities/getViewerResourceCount'; diff --git a/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx b/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx index 3543f975581..ab102b4da63 100644 --- a/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx +++ b/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx @@ -15,7 +15,7 @@ import { DEFAULT_IMAGE360_ICON_COUNT_LIMIT, DEFAULT_IMAGE360_ICON_CULLING_RADIUS } from './constants'; -import { useReveal3DResourcesCount } from '../Reveal3DResources/Reveal3DResourcesCountContext'; +import { useReveal3DResourcesCount } from '../Reveal3DResources/Reveal3DResourcesInfoContext'; import { getViewerResourceCount } from '../../utilities/getViewerResourceCount'; type Image360CollectionContainerProps = { diff --git a/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx b/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx index d563aa4707b..70e5ba5e871 100644 --- a/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx +++ b/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx @@ -7,7 +7,7 @@ import { useEffect, type ReactElement, useState, useRef } from 'react'; import { Matrix4 } from 'three'; import { useReveal } from '../RevealCanvas/ViewerContext'; import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; -import { useReveal3DResourcesCount } from '../Reveal3DResources/Reveal3DResourcesCountContext'; +import { useReveal3DResourcesCount } from '../Reveal3DResources/Reveal3DResourcesInfoContext'; import { cloneDeep, isEqual } from 'lodash'; import { useApplyPointCloudStyling, diff --git a/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx b/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx index 64101a93d34..d44470447a1 100644 --- a/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx +++ b/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx @@ -31,6 +31,12 @@ import { import { type ImageCollectionModelStyling } from '../Image360CollectionContainer/useApply360AnnotationStyling'; import { is360ImageAddOptions } from './typeGuards'; import { useRemoveNonReferencedModels } from './useRemoveNonReferencedModels'; +import { + useAssetMappedNodesForRevisions, + useGenerateAssetMappingCachePerItemFromModelCache, + useGenerateNode3DCache +} from '../CacheProvider/AssetMappingAndNode3DCacheProvider'; +import { useReveal3DResourcesStylingLoadingSetter } from './Reveal3DResourcesInfoContext'; export const Reveal3DResources = ({ resources, @@ -41,6 +47,7 @@ export const Reveal3DResources = ({ image360Settings }: Reveal3DResourcesProps): ReactElement => { const viewer = useReveal(); + const [reveal3DModels, setReveal3DModels] = useState([]); const numModelsLoaded = useRef(0); @@ -62,6 +69,11 @@ export const Reveal3DResources = ({ [reveal3DModels] ); + const { data: assetMappings } = useAssetMappedNodesForRevisions(cadModelOptions); + + useGenerateAssetMappingCachePerItemFromModelCache(cadModelOptions, assetMappings); + useGenerateNode3DCache(cadModelOptions, assetMappings); + const pointCloudModelOptions = useMemo( () => reveal3DModels.filter( @@ -70,15 +82,29 @@ export const Reveal3DResources = ({ [reveal3DModels] ); - const styledCadModelOptions = useCalculateCadStyling( + const { + styledModels: styledCadModelOptions, + isModelMappingsFetched, + isModelMappingsLoading + } = useCalculateCadStyling( cadModelOptions, instanceStyling?.filter(isCadAssetMappingStylingGroup) ?? EMPTY_ARRAY, defaultResourceStyling ); + const setModel3DStylingLoading = useReveal3DResourcesStylingLoadingSetter(); + setModel3DStylingLoading(!(isModelMappingsFetched || !isModelMappingsLoading)); + + useEffect(() => { + setModel3DStylingLoading(!(isModelMappingsFetched || !isModelMappingsLoading)); + }, [isModelMappingsFetched, isModelMappingsLoading]); + + const instaceStylingWithAssetMappings = + instanceStyling?.filter(isAssetMappingStylingGroup) ?? EMPTY_ARRAY; + const styledPointCloudModelOptions = useCalculatePointCloudStyling( pointCloudModelOptions, - instanceStyling?.filter(isAssetMappingStylingGroup) ?? EMPTY_ARRAY, + instaceStylingWithAssetMappings, defaultResourceStyling ); diff --git a/react-components/src/components/Reveal3DResources/Reveal3DResourcesCountContext.tsx b/react-components/src/components/Reveal3DResources/Reveal3DResourcesCountContext.tsx deleted file mode 100644 index 9c491173847..00000000000 --- a/react-components/src/components/Reveal3DResources/Reveal3DResourcesCountContext.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/*! - * Copyright 2023 Cognite AS - */ - -import { - type ReactElement, - createContext, - useContext, - useState, - type ReactNode, - useMemo -} from 'react'; - -type Reveal3DResourcesCountContent = { - reveal3DResourcesCount: number; - setRevealResourcesCount: (newCount: number) => void; -}; - -const Reveal3DResourcesCountContext = createContext(null); - -export const useReveal3DResourcesCount = (): Reveal3DResourcesCountContent => { - const element = useContext(Reveal3DResourcesCountContext); - if (element === null) { - throw new Error( - 'useReveal3DResourcesCount must be used within a Reveal3DResourcesCountContextProvider' - ); - } - return element; -}; - -export const Reveal3DResourcesCountContextProvider = ({ - children -}: { - children: ReactNode; -}): ReactElement => { - const [reveal3DResourcesCount, setRevealResourcesCount] = useState(0); - const memoedState = useMemo( - () => ({ - reveal3DResourcesCount, - setRevealResourcesCount - }), - [reveal3DResourcesCount, setRevealResourcesCount] - ); - return ( - - {children} - - ); -}; diff --git a/react-components/src/components/Reveal3DResources/Reveal3DResourcesInfoContext.tsx b/react-components/src/components/Reveal3DResources/Reveal3DResourcesInfoContext.tsx new file mode 100644 index 00000000000..a8d3101e779 --- /dev/null +++ b/react-components/src/components/Reveal3DResources/Reveal3DResourcesInfoContext.tsx @@ -0,0 +1,74 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { + type ReactElement, + createContext, + useContext, + useState, + type ReactNode, + useMemo +} from 'react'; + +type Reveal3DResourcesInfoContent = { + reveal3DResourcesCount: number; + setRevealResourcesCount: (newCount: number) => void; + model3DStylingLoading: boolean; + setModel3DStylingLoading: (loading: boolean) => void; +}; + +const Reveal3DResourcesInfoContext = createContext(null); + +const getInfoElementOfContext = (): Reveal3DResourcesInfoContent => { + const element = useContext(Reveal3DResourcesInfoContext); + if (element === null) { + throw new Error( + 'ResourcesInfoContent must be used within a Reveal3DResourcesInfoContextProvider' + ); + } + return element; +}; + +export const useReveal3DResourcesCount = (): Reveal3DResourcesInfoContent => { + const element = getInfoElementOfContext(); + return element; +}; + +export const useReveal3DResourcesStylingLoading = (): boolean => { + const element = getInfoElementOfContext(); + return element.model3DStylingLoading; +}; + +export const useReveal3DResourcesStylingLoadingSetter = (): ((value: boolean) => void) => { + const element = getInfoElementOfContext(); + return element.setModel3DStylingLoading; +}; + +export const Reveal3DResourcesInfoContextProvider = ({ + children +}: { + children: ReactNode; +}): ReactElement => { + const [reveal3DResourcesCount, setRevealResourcesCount] = useState(0); + const [model3DStylingLoading, setModel3DStylingLoading] = useState(false); + const memoedState = useMemo( + () => ({ + reveal3DResourcesCount, + setRevealResourcesCount, + model3DStylingLoading, + setModel3DStylingLoading + }), + [ + reveal3DResourcesCount, + setRevealResourcesCount, + model3DStylingLoading, + setModel3DStylingLoading + ] + ); + return ( + + {children} + + ); +}; diff --git a/react-components/src/components/RevealContext/RevealContext.tsx b/react-components/src/components/RevealContext/RevealContext.tsx index d36be8d6b99..fbc6361497c 100644 --- a/react-components/src/components/RevealContext/RevealContext.tsx +++ b/react-components/src/components/RevealContext/RevealContext.tsx @@ -8,9 +8,9 @@ import { type Color } from 'three'; import { I18nContextProvider } from '../i18n/I18n'; import { ViewerContext } from '../RevealCanvas/ViewerContext'; import { NodeCacheProvider } from '../CacheProvider/NodeCacheProvider'; -import { AssetMappingCacheProvider } from '../CacheProvider/AssetMappingCacheProvider'; +import { AssetMappingAndNode3DCacheProvider } from '../CacheProvider/AssetMappingAndNode3DCacheProvider'; import { PointCloudAnnotationCacheProvider } from '../CacheProvider/PointCloudAnnotationCacheProvider'; -import { Reveal3DResourcesCountContextProvider } from '../Reveal3DResources/Reveal3DResourcesCountContext'; +import { Reveal3DResourcesInfoContextProvider } from '../Reveal3DResources/Reveal3DResourcesInfoContext'; import { SDKProvider } from '../RevealCanvas/SDKProvider'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; @@ -52,15 +52,15 @@ export const RevealContext = (props: RevealContextProps): ReactElement => { - + - + {props.children} - + - + diff --git a/react-components/src/components/RevealKeepAlive/RevealKeepAlive.tsx b/react-components/src/components/RevealKeepAlive/RevealKeepAlive.tsx index a012b2bf0c9..b0f943d44a1 100644 --- a/react-components/src/components/RevealKeepAlive/RevealKeepAlive.tsx +++ b/react-components/src/components/RevealKeepAlive/RevealKeepAlive.tsx @@ -5,7 +5,7 @@ import { type ReactNode, type ReactElement, useRef, useEffect } from 'react'; import { RevealKeepAliveContext } from './RevealKeepAliveContext'; import { type FdmNodeCache } from '../CacheProvider/FdmNodeCache'; -import { type AssetMappingCache } from '../CacheProvider/AssetMappingCache'; +import { type AssetMappingAndNode3DCache } from '../CacheProvider/AssetMappingAndNode3DCache'; import { type PointCloudAnnotationCache } from '../CacheProvider/PointCloudAnnotationCache'; import { type Image360AnnotationCache } from '../CacheProvider/Image360AnnotationCache'; import { type SceneIdentifiers } from '../SceneContainer/sceneTypes'; @@ -16,7 +16,7 @@ export function RevealKeepAlive({ children }: { children?: ReactNode }): ReactEl const isRevealContainerMountedRef = useRef(false); const sceneLoadedRef = useRef(); const fdmNodeCache = useRef(); - const assetMappingCache = useRef(); + const assetMappingCache = useRef(); const pointCloudAnnotationCache = useRef(); const image360AnnotationCache = useRef(); diff --git a/react-components/src/components/RevealKeepAlive/RevealKeepAliveContext.ts b/react-components/src/components/RevealKeepAlive/RevealKeepAliveContext.ts index 529ddb0978f..e57d4e9a896 100644 --- a/react-components/src/components/RevealKeepAlive/RevealKeepAliveContext.ts +++ b/react-components/src/components/RevealKeepAlive/RevealKeepAliveContext.ts @@ -3,7 +3,7 @@ */ import { type MutableRefObject, createContext, useContext } from 'react'; import { type FdmNodeCache } from '../CacheProvider/FdmNodeCache'; -import { type AssetMappingCache } from '../CacheProvider/AssetMappingCache'; +import { type AssetMappingAndNode3DCache } from '../CacheProvider/AssetMappingAndNode3DCache'; import { type PointCloudAnnotationCache } from '../CacheProvider/PointCloudAnnotationCache'; import { type Image360AnnotationCache } from '../CacheProvider/Image360AnnotationCache'; import { type SceneIdentifiers } from '../SceneContainer/sceneTypes'; @@ -14,7 +14,7 @@ export type RevealKeepAliveData = { isRevealContainerMountedRef: MutableRefObject; sceneLoadedRef: MutableRefObject; fdmNodeCache: MutableRefObject; - assetMappingCache: MutableRefObject; + assetMappingCache: MutableRefObject; pointCloudAnnotationCache: MutableRefObject; image360AnnotationCache: MutableRefObject; }; diff --git a/react-components/src/components/RevealToolbar/AssetContextualizedButton.tsx b/react-components/src/components/RevealToolbar/AssetContextualizedButton.tsx index 6307632d726..2790b0a094a 100644 --- a/react-components/src/components/RevealToolbar/AssetContextualizedButton.tsx +++ b/react-components/src/components/RevealToolbar/AssetContextualizedButton.tsx @@ -6,7 +6,7 @@ import { useCallback, useState, type ReactElement } from 'react'; import { Button, Tooltip as CogsTooltip } from '@cognite/cogs.js'; import { useTranslation } from '../i18n/I18n'; import { use3dModels } from '../../hooks/use3dModels'; -import { useAssetMappedNodesForRevisions } from '../CacheProvider/AssetMappingCacheProvider'; +import { useAssetMappedNodesForRevisions } from '../CacheProvider/AssetMappingAndNode3DCacheProvider'; import { type CadModelOptions } from '../Reveal3DResources/types'; type AssetContextualizedButtonProps = { diff --git a/react-components/src/components/RevealToolbar/RuleBasedOutputsButton.tsx b/react-components/src/components/RevealToolbar/RuleBasedOutputsButton.tsx index 97f39feb38d..78a750d21d7 100644 --- a/react-components/src/components/RevealToolbar/RuleBasedOutputsButton.tsx +++ b/react-components/src/components/RevealToolbar/RuleBasedOutputsButton.tsx @@ -16,8 +16,13 @@ import { useFetchRuleInstances } from '../RuleBasedOutputs/hooks/useFetchRuleIns import { use3dModels } from '../../hooks/use3dModels'; import { type AssetStylingGroup } from '../..'; import { type CadModelOptions } from '../Reveal3DResources/types'; -import { useAssetMappedNodesForRevisions } from '../CacheProvider/AssetMappingCacheProvider'; +import { useAssetMappedNodesForRevisions } from '../CacheProvider/AssetMappingAndNode3DCacheProvider'; import { RuleBasedSelectionItem } from '../RuleBasedOutputs/components/RuleBasedSelectionItem'; +import { generateEmptyRuleForSelection, getRuleBasedById } from '../RuleBasedOutputs/utils'; +import { + useReveal3DResourcesStylingLoading, + useReveal3DResourcesStylingLoadingSetter +} from '../Reveal3DResources/Reveal3DResourcesInfoContext'; type RuleBasedOutputsButtonProps = { onRuleSetStylingChanged?: (stylings: AssetStylingGroup[] | undefined) => void; @@ -27,43 +32,62 @@ export const RuleBasedOutputsButton = ({ onRuleSetStylingChanged, onRuleSetSelectedChanged }: RuleBasedOutputsButtonProps): ReactElement => { - const [currentRuleSetEnabled, setCurrentRuleSetEnabled] = useState(); - const [emptyRuleSelected, setEmptyRuleSelected] = useState(); - const [ruleInstances, setRuleInstances] = useState(); const { t } = useTranslation(); const models = use3dModels(); const cadModels = models.filter((model) => model.type === 'cad') as CadModelOptions[]; - const { isLoading } = useAssetMappedNodesForRevisions(cadModels); - const ruleInstancesResult = useFetchRuleInstances(); + const [currentRuleSetEnabled, setCurrentRuleSetEnabled] = useState(); + const [emptyRuleSelected, setEmptyRuleSelected] = useState(); + const [currentStylingGroups, setCurrentStylingGroups] = useState< + AssetStylingGroupAndStyleIndex[] | undefined + >(); + const [ruleInstances, setRuleInstances] = useState(); - useEffect(() => { - if (ruleInstancesResult.data === undefined) return; + const [isRuleLoading, setIsRuleLoading] = useState(false); + + const { isLoading: isAssetMappingsLoading } = useAssetMappedNodesForRevisions(cadModels); + + const [newRuleSetEnabled, setNewRuleSetEnabled] = useState(); + const isRuleLoadingFromContext = useReveal3DResourcesStylingLoading(); + const setModel3DStylingLoading = useReveal3DResourcesStylingLoadingSetter(); + + const { data: ruleInstancesResult } = useFetchRuleInstances(); - setRuleInstances(ruleInstancesResult.data); + useEffect(() => { + setRuleInstances(ruleInstancesResult); }, [ruleInstancesResult]); + useEffect(() => { + setCurrentRuleSetEnabled(newRuleSetEnabled); + if (onRuleSetSelectedChanged !== undefined) onRuleSetSelectedChanged(newRuleSetEnabled); + + const hasNewRuleSetEnabled = newRuleSetEnabled !== undefined; + + setIsRuleLoading(hasNewRuleSetEnabled); + setModel3DStylingLoading(hasNewRuleSetEnabled); + }, [newRuleSetEnabled]); + + useEffect(() => { + const hasRuleLoading = + currentStylingGroups !== undefined && + currentStylingGroups.length > 0 && + isRuleLoadingFromContext; + setIsRuleLoading(hasRuleLoading); + setModel3DStylingLoading(hasRuleLoading); + }, [isRuleLoadingFromContext, currentStylingGroups]); + const onChange = useCallback( (data: string | undefined): void => { + const emptySelection = generateEmptyRuleForSelection( + t('RULESET_NO_SELECTION', 'No RuleSet selected') + ); + ruleInstances?.forEach((item) => { if (item === undefined) return; item.isEnabled = false; }); - const emptySelection: EmptyRuleForSelection = { - rule: { - properties: { - id: undefined, - name: t('RULESET_NO_SELECTION', 'No RuleSet selected'), - isNoSelection: true - } - }, - isEnabled: false - }; - - const selectedRule = ruleInstances?.find((item) => { - return item?.rule?.properties.id === data; - }); + const selectedRule = getRuleBasedById(data, ruleInstances); if (selectedRule !== undefined) { selectedRule.isEnabled = true; @@ -72,10 +96,10 @@ export const RuleBasedOutputsButton = ({ if (onRuleSetStylingChanged !== undefined) onRuleSetStylingChanged(undefined); } - if (onRuleSetSelectedChanged !== undefined) onRuleSetSelectedChanged(selectedRule); - setEmptyRuleSelected(emptySelection); - setCurrentRuleSetEnabled(selectedRule); + setNewRuleSetEnabled(selectedRule); + setIsRuleLoading(true); + setModel3DStylingLoading(true); }, [ruleInstances, onRuleSetStylingChanged, onRuleSetSelectedChanged] ); @@ -83,6 +107,7 @@ export const RuleBasedOutputsButton = ({ const ruleSetStylingChanged = ( stylingGroups: AssetStylingGroupAndStyleIndex[] | undefined ): void => { + setCurrentStylingGroups(stylingGroups); const assetStylingGroups = stylingGroups?.map((group) => group.assetStylingGroup); if (onRuleSetStylingChanged !== undefined) onRuleSetStylingChanged(assetStylingGroups); }; @@ -99,7 +124,7 @@ export const RuleBasedOutputsButton = ({ appendTo={document.body}> {ruleInstances?.map((item) => ( ))} }> -