From 7af7766fcab228014cc0c1a81d2a5bbb5dd08baa Mon Sep 17 00:00:00 2001 From: Daniel Priori Date: Thu, 11 Jul 2024 11:40:46 +0200 Subject: [PATCH 01/26] 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) => ( ))} }> - ); }; diff --git a/react-components/src/components/Architecture/CommandButtons.tsx b/react-components/src/components/Architecture/CommandButtons.tsx index 84e20378de7..32c6909a699 100644 --- a/react-components/src/components/Architecture/CommandButtons.tsx +++ b/react-components/src/components/Architecture/CommandButtons.tsx @@ -1,5 +1,5 @@ /*! - * Copyright 2023 Cognite AS + * Copyright 2024 Cognite AS */ import { useMemo, type ReactElement } from 'react'; @@ -8,12 +8,26 @@ import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; import { OptionButton } from './OptionButton'; import { BaseOptionCommand } from '../../architecture/base/commands/BaseOptionCommand'; import { CommandButton } from './CommandButton'; +import { SettingsButton } from './SettingsButton'; +import { SettingsCommand } from '../../architecture/base/concreteCommands/SettingsCommand'; -export function createButton(command: BaseCommand, isHorizontal = false): ReactElement { - if (command instanceof BaseOptionCommand) { +export function createButton( + command: BaseCommand, + isHorizontal = false, + usedInSettings = false +): ReactElement { + if (command instanceof SettingsCommand) { + return ; + } else if (command instanceof BaseOptionCommand) { return ; } else { - return ; + return ( + + ); } } diff --git a/react-components/src/components/Architecture/LabelWithShortcut.tsx b/react-components/src/components/Architecture/LabelWithShortcut.tsx index 0a7e8c5dde7..ac4faf14f88 100644 --- a/react-components/src/components/Architecture/LabelWithShortcut.tsx +++ b/react-components/src/components/Architecture/LabelWithShortcut.tsx @@ -1,5 +1,5 @@ /*! - * Copyright 2023 Cognite AS + * Copyright 2024 Cognite AS */ import React from 'react'; @@ -8,11 +8,14 @@ import styled from 'styled-components'; import { Shortcut } from '@cognite/cogs.js'; type LabelWithShortcutProps = { - label: string; + label?: string; shortcut?: string; }; export const LabelWithShortcut: React.FC = ({ label, shortcut }) => { + if (label === undefined) { + return <>; + } return ( diff --git a/react-components/src/components/Architecture/OptionButton.tsx b/react-components/src/components/Architecture/OptionButton.tsx index f28b9f2bba4..c77f0590d48 100644 --- a/react-components/src/components/Architecture/OptionButton.tsx +++ b/react-components/src/components/Architecture/OptionButton.tsx @@ -1,9 +1,9 @@ /*! - * Copyright 2023 Cognite AS + * Copyright 2024 Cognite AS */ -import { useCallback, useEffect, useMemo, useState, type ReactElement } from 'react'; -import { Button, Dropdown, Menu, Tooltip as CogsTooltip, type IconType } from '@cognite/cogs.js'; +import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'; +import { Button, Dropdown, Menu, Tooltip as CogsTooltip } from '@cognite/cogs.js'; import { useTranslation } from '../i18n/I18n'; import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; import { useRenderTarget } from '../RevealCanvas/ViewerContext'; @@ -12,17 +12,21 @@ import { getButtonType, getDefaultCommand, getFlexDirection, - getIcon, - getTooltipPlacement + getTooltipPlacement, + getIcon } from './utilities'; import { LabelWithShortcut } from './LabelWithShortcut'; +import { type TranslateDelegate } from '../../architecture/base/utilities/TranslateKey'; +import { useClickOutside } from './useClickOutside'; export const OptionButton = ({ inputCommand, - isHorizontal = false + isHorizontal = false, + usedInSettings = false }: { inputCommand: BaseOptionCommand; isHorizontal: boolean; + usedInSettings?: boolean; }): ReactElement => { const renderTarget = useRenderTarget(); const { t } = useTranslation(); @@ -32,13 +36,21 @@ export const OptionButton = ({ const [isEnabled, setEnabled] = useState(true); const [isVisible, setVisible] = useState(true); const [uniqueId, setUniqueId] = useState(0); - const [icon, setIcon] = useState(undefined); + + const postAction = (): void => { + setOpen(false); + renderTarget.domElement.focus(); + }; + + const menuRef = useRef(null); + useClickOutside(menuRef, () => { + postAction(); + }); const update = useCallback((command: BaseCommand) => { setEnabled(command.isEnabled); setVisible(command.isVisible); setUniqueId(command.uniqueId); - setIcon(getIcon(command)); }, []); useEffect(() => { @@ -56,67 +68,74 @@ export const OptionButton = ({ return <>; } const placement = getTooltipPlacement(isHorizontal); - const tooltip = command.getLabel(t); + const label = usedInSettings ? undefined : command.getLabel(t); const shortcut = command.getShortCutKeys(); const flexDirection = getFlexDirection(isHorizontal); - const options = command.getOrCreateOptions(renderTarget); + const options = command.options; const selectedLabel = command.selectedOption?.getLabel(t); return ( - } - appendTo={document.body} - placement={placement}> - + } + disabled={usedInSettings || label === undefined} appendTo={document.body} - onClickOutside={() => { - setOpen(false); - renderTarget.domElement.focus(); - }} - content={ - + + {options.map((command, _index): ReactElement => { + return createMenuItem(command, t, postAction); + })} + + }> + - - + {selectedLabel} + + + + ); }; + +export function createMenuItem( + command: BaseCommand, + t: TranslateDelegate, + postAction: () => void +): ReactElement { + return ( + { + command.invoke(); + postAction(); + }}> + {command.getLabel(t)} + + ); +} diff --git a/react-components/src/components/Architecture/RevealButtons.tsx b/react-components/src/components/Architecture/RevealButtons.tsx index 431aa6fa1cb..8541f38f9e1 100644 --- a/react-components/src/components/Architecture/RevealButtons.tsx +++ b/react-components/src/components/Architecture/RevealButtons.tsx @@ -1,5 +1,5 @@ /*! - * Copyright 2023 Cognite AS + * Copyright 2024 Cognite AS */ import { type ReactElement } from 'react'; @@ -13,8 +13,15 @@ import { MeasurementTool } from '../../architecture/concrete/measurements/Measur import { KeyboardSpeedCommand } from '../../architecture/base/concreteCommands/KeyboardSpeedCommand'; import { ObservationsTool } from '../../architecture/concrete/observations/ObservationsTool'; import { createButtonFromCommandConstructor } from './CommandButtons'; +import { SettingsCommand } from '../../architecture/base/concreteCommands/SettingsCommand'; +import { SetPointColorTypeCommand } from '../../architecture/base/concreteCommands/SetPointColorTypeCommand'; +import { SetPointShapeCommand } from '../../architecture/base/concreteCommands/SetPointShapeCommand'; +import { SetPointSizeCommand } from '../../architecture/base/concreteCommands/SetPointSizeCommand'; +import { SetQualityCommand } from '../../architecture/base/concreteCommands/SetQualityCommand'; export class RevealButtons { + static Settings = (): ReactElement => createButtonFromCommandConstructor(() => createSettings()); + static FitView = (): ReactElement => createButtonFromCommandConstructor(() => new FitViewCommand()); @@ -46,3 +53,12 @@ export class RevealButtons { static KeyboardSpeed = (): ReactElement => createButtonFromCommandConstructor(() => new KeyboardSpeedCommand()); } + +function createSettings(): SettingsCommand { + const settings = new SettingsCommand(); + settings.add(new SetQualityCommand()); + settings.add(new SetPointSizeCommand()); + settings.add(new SetPointColorTypeCommand()); + settings.add(new SetPointShapeCommand()); + return settings; +} diff --git a/react-components/src/components/Architecture/SettingsButton.tsx b/react-components/src/components/Architecture/SettingsButton.tsx new file mode 100644 index 00000000000..742c8b8efc8 --- /dev/null +++ b/react-components/src/components/Architecture/SettingsButton.tsx @@ -0,0 +1,220 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { useCallback, useEffect, useMemo, useState, type ReactElement } from 'react'; +import { + Button, + Dropdown, + Menu, + Tooltip as CogsTooltip, + type IconType, + Slider +} from '@cognite/cogs.js'; +import { useTranslation } from '../i18n/I18n'; +import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; +import { useRenderTarget } from '../RevealCanvas/ViewerContext'; +import { + getButtonType, + getDefaultCommand, + getFlexDirection, + getTooltipPlacement, + getIcon +} from './utilities'; +import { LabelWithShortcut } from './LabelWithShortcut'; +import { type TranslateDelegate } from '../../architecture/base/utilities/TranslateKey'; +import styled from 'styled-components'; +import { SettingsCommand } from '../../architecture/base/concreteCommands/SettingsCommand'; +import { createButton } from './CommandButtons'; +import { BaseOptionCommand } from '../../architecture/base/commands/BaseOptionCommand'; +import { OptionButton } from './OptionButton'; +import { BaseSliderCommand } from '../../architecture/base/commands/BaseSliderCommand'; + +export const SettingsButton = ({ + inputCommand, + isHorizontal = false +}: { + inputCommand: SettingsCommand; + isHorizontal: boolean; +}): ReactElement => { + const renderTarget = useRenderTarget(); + const { t } = useTranslation(); + const command = useMemo(() => getDefaultCommand(inputCommand, renderTarget), []); + + const [isOpen, setOpen] = useState(false); + const [isEnabled, setEnabled] = useState(true); + const [isVisible, setVisible] = useState(true); + const [uniqueId, setUniqueId] = useState(0); + const [icon, setIcon] = useState(undefined); + + const update = useCallback((command: BaseCommand) => { + setEnabled(command.isEnabled); + setVisible(command.isVisible); + setUniqueId(command.uniqueId); + setIcon(getIcon(command)); + }, []); + + useEffect(() => { + update(command); + command.addEventListener(update); + return () => { + command.removeEventListener(update); + }; + }, [command]); + + if (!(command instanceof SettingsCommand)) { + return <>; + } + if (!isVisible) { + return <>; + } + const placement = getTooltipPlacement(isHorizontal); + const label = command.getLabel(t); + const shortcut = command.getShortCutKeys(); + const flexDirection = getFlexDirection(isHorizontal); + const commands = command.commands; + + return ( + } + disabled={label === undefined} + appendTo={document.body} + placement={placement}> + + {commands.map((command, _index): ReactElement | undefined => { + return createMenuItem(command, t); + })} + + }> + + }}> ); }; diff --git a/react-components/src/components/Architecture/CommandButtons.tsx b/react-components/src/components/Architecture/CommandButtons.tsx index 32c6909a699..b6a94678c5c 100644 --- a/react-components/src/components/Architecture/CommandButtons.tsx +++ b/react-components/src/components/Architecture/CommandButtons.tsx @@ -9,26 +9,21 @@ import { OptionButton } from './OptionButton'; import { BaseOptionCommand } from '../../architecture/base/commands/BaseOptionCommand'; import { CommandButton } from './CommandButton'; import { SettingsButton } from './SettingsButton'; -import { SettingsCommand } from '../../architecture/base/concreteCommands/SettingsCommand'; +import { SettingsCommand } from '../../architecture/base/commands/SettingsCommand'; +import { BaseFilterCommand } from '../../architecture/base/commands/BaseFilterCommand'; +import { FilterButton } from './FilterButton'; -export function createButton( - command: BaseCommand, - isHorizontal = false, - usedInSettings = false -): ReactElement { +export function createButton(command: BaseCommand, isHorizontal = false): ReactElement { + if (command instanceof BaseFilterCommand) { + return ; + } if (command instanceof SettingsCommand) { return ; - } else if (command instanceof BaseOptionCommand) { + } + if (command instanceof BaseOptionCommand) { return ; - } else { - return ( - - ); } + return ; } export function createButtonFromCommandConstructor( diff --git a/react-components/src/components/Architecture/FilterButton.tsx b/react-components/src/components/Architecture/FilterButton.tsx new file mode 100644 index 00000000000..d879d1e32b8 --- /dev/null +++ b/react-components/src/components/Architecture/FilterButton.tsx @@ -0,0 +1,165 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ReactElement, + type MouseEvent +} from 'react'; +import { Button, Dropdown, Menu, Tooltip as CogsTooltip, type IconType } from '@cognite/cogs.js'; +import { useTranslation } from '../i18n/I18n'; +import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; +import { useRenderTarget } from '../RevealCanvas/ViewerContext'; +import { + getButtonType, + getDefaultCommand, + getFlexDirection, + getTooltipPlacement, + getIcon +} from './utilities'; +import { LabelWithShortcut } from './LabelWithShortcut'; +import { useClickOutside } from './useClickOutside'; +import styled from 'styled-components'; +import { BaseFilterCommand } from '../../architecture/base/commands/BaseFilterCommand'; +import { FilterItem } from './FilterItem'; + +export const FilterButton = ({ + inputCommand, + isHorizontal = false, + usedInSettings = false +}: { + inputCommand: BaseFilterCommand; + isHorizontal: boolean; + usedInSettings?: boolean; +}): ReactElement => { + const renderTarget = useRenderTarget(); + const { t } = useTranslation(); + const command = useMemo( + () => getDefaultCommand(inputCommand, renderTarget), + [] + ); + + const [isEnabled, setEnabled] = useState(true); + const [isVisible, setVisible] = useState(true); + const [uniqueId, setUniqueId] = useState(0); + const [icon, setIcon] = useState(undefined); + const [isOpen, setOpen] = useState(false); + const [isAllChecked, setAllChecked] = useState(false); + const [selectedLabel, setSelectedLabel] = useState(''); + + const update = useCallback( + (command: BaseCommand) => { + setEnabled(command.isEnabled); + setVisible(command.isVisible); + setUniqueId(command.uniqueId); + setIcon(getIcon(command)); + if (command instanceof BaseFilterCommand) { + setAllChecked(command.isAllChecked); + setSelectedLabel(command.getSelectedLabel(t)); + } + }, + [command] + ); + + useEffect(() => { + update(command); + command.addEventListener(update); + return () => { + command.removeEventListener(update); + }; + }, [command]); + + const outsideAction = (): boolean => { + if (!isOpen) { + return false; + } + setOpen(false); + renderTarget.domElement.focus(); + return true; + }; + + const menuRef = useRef(null); + useClickOutside(menuRef, outsideAction); + + if (!isVisible) { + return <>; + } + const placement = getTooltipPlacement(isHorizontal); + const label = usedInSettings ? undefined : command.getLabel(t); + const shortcut = command.getShortCutKeys(); + const flexDirection = getFlexDirection(isHorizontal); + + command.initializeChildrenIfNeeded(); + const children = command.children; + if (children === undefined || !command.hasChildren) { + return <>; + } + return ( + } + disabled={usedInSettings || label === undefined} + appendTo={document.body} + placement={placement}> + + + { + command.toggleAllChecked(); + }}> + {command.getAllLabel(t)} + + + {children.map((child, _index): ReactElement => { + return ; + })} + + + + }> + + + + ); +}; + +const StyledMenuItems = styled.div` + max-height: 300px; + overflow-y: auto; + overflow-x: hidden; +`; diff --git a/react-components/src/components/Architecture/FilterItem.tsx b/react-components/src/components/Architecture/FilterItem.tsx new file mode 100644 index 00000000000..194b2aeea3d --- /dev/null +++ b/react-components/src/components/Architecture/FilterItem.tsx @@ -0,0 +1,68 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { useCallback, useEffect, useState, type ReactElement } from 'react'; +import { Menu } from '@cognite/cogs.js'; +import { useTranslation } from '../i18n/I18n'; +import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; +import styled from 'styled-components'; +import { type Color } from 'three'; +import { type BaseFilterItemCommand } from '../../architecture/base/commands/BaseFilterCommand'; + +export const FilterItem = ({ command }: { command: BaseFilterItemCommand }): ReactElement => { + const { t } = useTranslation(); + + const [isChecked, setChecked] = useState(false); + const [isEnabled, setEnabled] = useState(true); + const [isVisible, setVisible] = useState(true); + const [uniqueId, setUniqueId] = useState(0); + + const update = useCallback((command: BaseCommand) => { + setChecked(command.isChecked); + setEnabled(command.isEnabled); + setVisible(command.isVisible); + setUniqueId(command.uniqueId); + }, []); + + useEffect(() => { + update(command); + command.addEventListener(update); + return () => { + command.removeEventListener(update); + }; + }, [command]); + + if (!isVisible) { + return <>; + } + return ( + { + command.invoke(); + }}> + + {command.color !== undefined && } + + + + ); +}; + +const ColorBox = styled.div<{ backgroundColor: Color }>` + width: 16px; + height: 16px; + border: 1px solid black; + display: inline-block; + background-color: ${(props) => props.backgroundColor.getStyle()}; +`; + +const CenteredContainer = styled.div` + display: flex; + row-gap: 3px; + gap: 3px; + align-items: center; +`; diff --git a/react-components/src/components/Architecture/OptionButton.tsx b/react-components/src/components/Architecture/OptionButton.tsx index c77f0590d48..4dbe664d4cb 100644 --- a/react-components/src/components/Architecture/OptionButton.tsx +++ b/react-components/src/components/Architecture/OptionButton.tsx @@ -2,12 +2,20 @@ * Copyright 2024 Cognite AS */ -import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ReactElement, + type MouseEvent +} from 'react'; import { Button, Dropdown, Menu, Tooltip as CogsTooltip } from '@cognite/cogs.js'; import { useTranslation } from '../i18n/I18n'; import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; import { useRenderTarget } from '../RevealCanvas/ViewerContext'; -import { BaseOptionCommand } from '../../architecture/base/commands/BaseOptionCommand'; +import { type BaseOptionCommand } from '../../architecture/base/commands/BaseOptionCommand'; import { getButtonType, getDefaultCommand, @@ -30,23 +38,16 @@ export const OptionButton = ({ }): ReactElement => { const renderTarget = useRenderTarget(); const { t } = useTranslation(); - const command = useMemo(() => getDefaultCommand(inputCommand, renderTarget), []); + const command = useMemo( + () => getDefaultCommand(inputCommand, renderTarget), + [] + ); const [isOpen, setOpen] = useState(false); const [isEnabled, setEnabled] = useState(true); const [isVisible, setVisible] = useState(true); const [uniqueId, setUniqueId] = useState(0); - const postAction = (): void => { - setOpen(false); - renderTarget.domElement.focus(); - }; - - const menuRef = useRef(null); - useClickOutside(menuRef, () => { - postAction(); - }); - const update = useCallback((command: BaseCommand) => { setEnabled(command.isEnabled); setVisible(command.isVisible); @@ -61,61 +62,78 @@ export const OptionButton = ({ }; }, [command]); - if (!(command instanceof BaseOptionCommand)) { - return <>; - } - if (!isVisible) { + const outsideAction = (): boolean => { + if (!isOpen) { + return false; + } + postAction(); + return true; + }; + + const postAction = (): void => { + setOpen(false); + renderTarget.domElement.focus(); + }; + + const menuRef = useRef(null); + useClickOutside(menuRef, outsideAction); + + if (!isVisible || command.children === undefined) { return <>; } const placement = getTooltipPlacement(isHorizontal); const label = usedInSettings ? undefined : command.getLabel(t); const shortcut = command.getShortCutKeys(); const flexDirection = getFlexDirection(isHorizontal); - const options = command.options; - const selectedLabel = command.selectedOption?.getLabel(t); + const children = command.children; + const selectedLabel = command.selectedChild?.getLabel(t); return ( -
- } - disabled={usedInSettings || label === undefined} + } + disabled={usedInSettings || label === undefined} + appendTo={document.body} + placement={placement}> + - - {options.map((command, _index): ReactElement => { - return createMenuItem(command, t, postAction); + {children.map((child, _index): ReactElement => { + return createMenuItem(child, t, postAction); })} - }> - - - -
+ + }> + + + ); }; diff --git a/react-components/src/components/Architecture/RevealButtons.tsx b/react-components/src/components/Architecture/RevealButtons.tsx index 8541f38f9e1..3bd16695867 100644 --- a/react-components/src/components/Architecture/RevealButtons.tsx +++ b/react-components/src/components/Architecture/RevealButtons.tsx @@ -13,11 +13,12 @@ import { MeasurementTool } from '../../architecture/concrete/measurements/Measur import { KeyboardSpeedCommand } from '../../architecture/base/concreteCommands/KeyboardSpeedCommand'; import { ObservationsTool } from '../../architecture/concrete/observations/ObservationsTool'; import { createButtonFromCommandConstructor } from './CommandButtons'; -import { SettingsCommand } from '../../architecture/base/concreteCommands/SettingsCommand'; +import { SettingsCommand } from '../../architecture/base/commands/SettingsCommand'; import { SetPointColorTypeCommand } from '../../architecture/base/concreteCommands/SetPointColorTypeCommand'; import { SetPointShapeCommand } from '../../architecture/base/concreteCommands/SetPointShapeCommand'; import { SetPointSizeCommand } from '../../architecture/base/concreteCommands/SetPointSizeCommand'; import { SetQualityCommand } from '../../architecture/base/concreteCommands/SetQualityCommand'; +import { PointCloudFilterCommand } from '../../architecture/base/concreteCommands/PointCloudFilterCommand'; export class RevealButtons { static Settings = (): ReactElement => createButtonFromCommandConstructor(() => createSettings()); @@ -60,5 +61,6 @@ function createSettings(): SettingsCommand { settings.add(new SetPointSizeCommand()); settings.add(new SetPointColorTypeCommand()); settings.add(new SetPointShapeCommand()); + settings.add(new PointCloudFilterCommand()); return settings; } diff --git a/react-components/src/components/Architecture/SettingsButton.tsx b/react-components/src/components/Architecture/SettingsButton.tsx index 742c8b8efc8..c8efbd8ac5c 100644 --- a/react-components/src/components/Architecture/SettingsButton.tsx +++ b/react-components/src/components/Architecture/SettingsButton.tsx @@ -24,11 +24,12 @@ import { import { LabelWithShortcut } from './LabelWithShortcut'; import { type TranslateDelegate } from '../../architecture/base/utilities/TranslateKey'; import styled from 'styled-components'; -import { SettingsCommand } from '../../architecture/base/concreteCommands/SettingsCommand'; -import { createButton } from './CommandButtons'; +import { type SettingsCommand } from '../../architecture/base/commands/SettingsCommand'; import { BaseOptionCommand } from '../../architecture/base/commands/BaseOptionCommand'; import { OptionButton } from './OptionButton'; import { BaseSliderCommand } from '../../architecture/base/commands/BaseSliderCommand'; +import { BaseFilterCommand } from '../../architecture/base/commands/BaseFilterCommand'; +import { FilterButton } from './FilterButton'; export const SettingsButton = ({ inputCommand, @@ -39,7 +40,10 @@ export const SettingsButton = ({ }): ReactElement => { const renderTarget = useRenderTarget(); const { t } = useTranslation(); - const command = useMemo(() => getDefaultCommand(inputCommand, renderTarget), []); + const command = useMemo( + () => getDefaultCommand(inputCommand, renderTarget), + [] + ); const [isOpen, setOpen] = useState(false); const [isEnabled, setEnabled] = useState(true); @@ -62,17 +66,14 @@ export const SettingsButton = ({ }; }, [command]); - if (!(command instanceof SettingsCommand)) { - return <>; - } - if (!isVisible) { + if (!isVisible || !command.hasChildren) { return <>; } const placement = getTooltipPlacement(isHorizontal); const label = command.getLabel(t); const shortcut = command.getShortCutKeys(); const flexDirection = getFlexDirection(isHorizontal); - const commands = command.commands; + const children = command.children; return ( - {commands.map((command, _index): ReactElement | undefined => { - return createMenuItem(command, t); + {children.map((child, _index): ReactElement | undefined => { + return createMenuItem(child, t); })} - + }> +
Date: Fri, 9 Aug 2024 14:49:07 +0530 Subject: [PATCH 26/26] bumped react-components to v0.55.3 (#4696) --- react-components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-components/package.json b/react-components/package.json index bd7fa1a6e44..6073887a51f 100644 --- a/react-components/package.json +++ b/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@cognite/reveal-react-components", - "version": "0.55.2", + "version": "0.55.3", "exports": { ".": { "import": "./dist/index.js",