From e345cbf4cc8cf1085c691272241f63f31c88ba59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= <70905152+haakonflatval-cognite@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:29:43 +0100 Subject: [PATCH] feat(react-components): color clicked poi instance and bump to 0.73.0 (#4915) * refactor(react-components): move caching into domain object * chore: preliminary lint fix * chore: lint fix some more * chore: remove unwanted changes * chore: make direct reference from render targe to caches * feat(react-components): color clicked instance from POI tool * fix: issue where cad data would sometimes get uncached * feat: add event listener to InstanceStylingController * chore: export InstanceStylingGroup type * chore: return undefined at end of function * feat: set style on assigned instance in PoITool * chore: rename variable * chore: lint fix * chore: remove unnecessary async * chore: correct method name * chore: bump package version * chore: add option to turn off exit button --- react-components/package.json | 2 +- .../base/renderTarget/CdfCaches.ts | 8 + .../renderTarget/InstanceStylingController.ts | 41 +++++ .../base/renderTarget/RevealRenderTarget.ts | 8 + .../pointsOfInterest/PointsOfInterestTool.ts | 46 +++++- .../AssetMappingAndNode3DCache.ts | 26 ++-- .../Image360Details/Image360Details.tsx | 18 ++- .../src/components/Reveal3DResources/index.ts | 1 + ...sePointCloudAnnotationMappingForAssetId.ts | 49 ++++++ react-components/src/hooks/useClickedNode.tsx | 30 ++-- .../query/core-dm/usePointCloudDMVolumes.ts | 4 +- ...ointCloudVolumeMappingForAssetInstances.ts | 93 +++++------ .../src/utilities/getInstancesFromClick.ts | 146 ++++++++++++++++++ 13 files changed, 379 insertions(+), 93 deletions(-) create mode 100644 react-components/src/architecture/base/renderTarget/InstanceStylingController.ts create mode 100644 react-components/src/hooks/pointClouds/usePointCloudAnnotationMappingForAssetId.ts create mode 100644 react-components/src/utilities/getInstancesFromClick.ts diff --git a/react-components/package.json b/react-components/package.json index f8ba3e13f6f..b1549a3eb56 100644 --- a/react-components/package.json +++ b/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@cognite/reveal-react-components", - "version": "0.73.0", + "version": "0.73.1", "exports": { ".": { "import": "./dist/index.js", diff --git a/react-components/src/architecture/base/renderTarget/CdfCaches.ts b/react-components/src/architecture/base/renderTarget/CdfCaches.ts index a141c85e30e..1d12d692405 100644 --- a/react-components/src/architecture/base/renderTarget/CdfCaches.ts +++ b/react-components/src/architecture/base/renderTarget/CdfCaches.ts @@ -16,6 +16,8 @@ export class CdfCaches { private readonly _pointCloudAnnotationCache: PointCloudAnnotationCache; private readonly _image360AnnotationCache: Image360AnnotationCache; + private readonly _cogniteClient: CogniteClient; + constructor( cdfClient: CogniteClient, fdm3dDataProvider: Fdm3dDataProvider, @@ -27,6 +29,8 @@ export class CdfCaches { this._fdmNodeCache = new FdmNodeCache(cdfClient, fdmClient, fdm3dDataProvider); this._pointCloudAnnotationCache = new PointCloudAnnotationCache(cdfClient); this._image360AnnotationCache = new Image360AnnotationCache(cdfClient, viewer); + + this._cogniteClient = cdfClient; } public get assetMappingAndNode3dCache(): AssetMappingAndNode3DCache { @@ -44,4 +48,8 @@ export class CdfCaches { public get image360Cache(): Image360AnnotationCache { return this._image360AnnotationCache; } + + public get cogniteClient(): CogniteClient { + return this._cogniteClient; + } } diff --git a/react-components/src/architecture/base/renderTarget/InstanceStylingController.ts b/react-components/src/architecture/base/renderTarget/InstanceStylingController.ts new file mode 100644 index 00000000000..e1342cbe04c --- /dev/null +++ b/react-components/src/architecture/base/renderTarget/InstanceStylingController.ts @@ -0,0 +1,41 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { remove } from 'lodash'; +import { type InstanceStylingGroup } from '../../../components/Reveal3DResources/types'; + +export class InstanceStylingController { + private readonly _eventHandlers: Array<() => void> = []; + + private readonly _stylingMap = new Map(); + + setStylingGroup(symbol: symbol, group: InstanceStylingGroup | undefined): void { + if (group === undefined) { + this._stylingMap.delete(symbol); + this.fireChangeEvent(); + return; + } + + this._stylingMap.set(symbol, group); + this.fireChangeEvent(); + } + + getStylingGroups(): Iterable { + return this._stylingMap.values(); + } + + addEventListener(eventHandler: () => void): void { + this._eventHandlers.push(eventHandler); + } + + removeEventListener(eventHandler: () => void): boolean { + return remove(this._eventHandlers, eventHandler).length > 0; + } + + private fireChangeEvent(): void { + this._eventHandlers.forEach((f) => { + f(); + }); + } +} diff --git a/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts b/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts index 1876263cd26..ccb90d87642 100644 --- a/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts +++ b/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts @@ -38,6 +38,7 @@ import { Changes } from '../domainObjectsHelpers/Changes'; import { type CogniteClient } from '@cognite/sdk'; import { type BaseTool } from '../commands/BaseTool'; import { ContextMenuController } from './ContextMenuController'; +import { InstanceStylingController } from './InstanceStylingController'; import { type Class } from '../domainObjectsHelpers/Class'; import { type CdfCaches } from './CdfCaches'; @@ -53,6 +54,8 @@ export class RevealRenderTarget { private readonly _rootDomainObject: RootDomainObject; private readonly _contextmenuController: ContextMenuController; private readonly _cdfCaches: CdfCaches; + private readonly _instanceStylingController: InstanceStylingController; + private _ambientLight: AmbientLight | undefined; private _directionalLight: DirectionalLight | undefined; private _clippedBoundingBox: Box3 | undefined; @@ -79,6 +82,7 @@ export class RevealRenderTarget { this._commandsController.addEventListeners(); this._contextmenuController = new ContextMenuController(); this._cdfCaches = cdfCaches; + this._instanceStylingController = new InstanceStylingController(); this._rootDomainObject = new RootDomainObject(this, sdk); this.initializeLights(); @@ -131,6 +135,10 @@ export class RevealRenderTarget { return this._cdfCaches; } + public get instanceStylingController(): InstanceStylingController { + return this._instanceStylingController; + } + public get cursor(): string { return this.domElement.style.cursor; } diff --git a/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestTool.ts b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestTool.ts index 394dd9cff6e..096fb04edbf 100644 --- a/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestTool.ts +++ b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestTool.ts @@ -13,10 +13,18 @@ import { AnchoredDialogUpdater } from '../../base/reactUpdaters/AnchoredDialogUp import { CreatePointsOfInterestWithDescriptionCommand } from './CreatePointsOfInterestWithDescriptionCommand'; import { type RevealRenderTarget } from '../../base/renderTarget/RevealRenderTarget'; import { BaseEditTool } from '../../base/commands/BaseEditTool'; +import { getInstancesFromClick } from '../../../utilities/getInstancesFromClick'; +import { type InstanceReference } from '../../../data-providers'; +import { DefaultNodeAppearance } from '@cognite/reveal'; +import { isAssetInstance } from '../../../data-providers/types'; + +const ASSIGNED_INSTANCE_STYLING_SYMBOL = Symbol('poi3d-assigned-instance-styling'); export class PointsOfInterestTool extends BaseEditTool { private _isCreating: boolean = false; + private _assignedInstance: InstanceReference | undefined; + private _anchoredDialogContent: AnchoredDialogContent | undefined; public override get icon(): IconName { @@ -50,11 +58,12 @@ export class PointsOfInterestTool extends BaseEditTool { const domainObject = this.getPointsOfInterestDomainObject(); domainObject.setSelectedPointOfInterest(undefined); this.setIsCreating(false); + this.setAssignedInstance(undefined); } public override async onClick(event: PointerEvent): Promise { if (this._isCreating) { - await this.initiateCreatPointOfInterest(event); + await this.initiateCreatePointOfInterest(event); this.setIsCreating(false); return; } @@ -105,6 +114,32 @@ export class PointsOfInterestTool extends BaseEditTool { } } + private setAssignedInstance(instance: InstanceReference | undefined): void { + this._assignedInstance = instance; + + if (instance === undefined) { + this.renderTarget.instanceStylingController.setStylingGroup( + ASSIGNED_INSTANCE_STYLING_SYMBOL, + undefined + ); + return; + } + + const modelStyle = DefaultNodeAppearance.Highlighted; + + if (isAssetInstance(instance)) { + this.renderTarget.instanceStylingController.setStylingGroup( + ASSIGNED_INSTANCE_STYLING_SYMBOL, + { assetIds: [instance.assetId], style: { cad: modelStyle, pointcloud: modelStyle } } + ); + } else { + this.renderTarget.instanceStylingController.setStylingGroup( + ASSIGNED_INSTANCE_STYLING_SYMBOL, + { fdmAssetExternalIds: [instance], style: { cad: modelStyle, pointcloud: modelStyle } } + ); + } + } + private setAnchoredDialogContent(dialogContent: AnchoredDialogContent | undefined): void { this._anchoredDialogContent = dialogContent; AnchoredDialogUpdater.update(); @@ -167,12 +202,19 @@ export class PointsOfInterestTool extends BaseEditTool { intersection.domainObject.setSelectedPointOfInterest(intersection.userData); } - private async initiateCreatPointOfInterest(event: PointerEvent): Promise { + private async initiateCreatePointOfInterest(event: PointerEvent): Promise { const intersection = await this.getIntersection(event); if (intersection === undefined || isPointsOfInterestIntersection(intersection)) { this.closeCreateCommandDialog(); return; } + this.openCreateCommandDialog(intersection.point); + + const instances = await getInstancesFromClick(this.renderTarget, event); + + if (instances !== undefined && instances.length !== 0) { + this.setAssignedInstance(instances[0]); + } } } diff --git a/react-components/src/components/CacheProvider/AssetMappingAndNode3DCache.ts b/react-components/src/components/CacheProvider/AssetMappingAndNode3DCache.ts index c909ff75831..5b170878ce7 100644 --- a/react-components/src/components/CacheProvider/AssetMappingAndNode3DCache.ts +++ b/react-components/src/components/CacheProvider/AssetMappingAndNode3DCache.ts @@ -235,20 +235,18 @@ export class AssetMappingAndNode3DCache { }) .autoPagingToArray({ limit: Infinity }); - assetMapping3D.forEach(async (item) => { - const keyAssetId: ModelAssetIdKey = modelRevisionNodesAssetToKey( - modelId, - revisionId, - item.assetId - ); - const keyNodeId: ModelTreeIndexKey = modelRevisionNodesAssetToKey( - modelId, - revisionId, - item.nodeId - ); - await this.assetIdsToAssetMappingCache.setAssetMappingsCacheItem(keyAssetId, item); - await this.nodeIdsToAssetMappingCache.setAssetMappingsCacheItem(keyNodeId, item); - }); + await Promise.all( + assetMapping3D.map(async (item) => { + const keyAssetId: ModelAssetIdKey = modelRevisionNodesAssetToKey( + modelId, + revisionId, + item.assetId + ); + const keyNodeId = modelRevisionNodesAssetToKey(modelId, revisionId, item.nodeId); + await this.assetIdsToAssetMappingCache.setAssetMappingsCacheItem(keyAssetId, item); + await this.nodeIdsToAssetMappingCache.setAssetMappingsCacheItem(keyNodeId, item); + }) + ); currentChunk.forEach(async (id) => { const key = modelRevisionNodesAssetToKey(modelId, revisionId, id); diff --git a/react-components/src/components/Image360Details/Image360Details.tsx b/react-components/src/components/Image360Details/Image360Details.tsx index 145033365b4..996758691b1 100644 --- a/react-components/src/components/Image360Details/Image360Details.tsx +++ b/react-components/src/components/Image360Details/Image360Details.tsx @@ -12,9 +12,13 @@ import { useImage360Collections } from '../../hooks/useImage360Collections'; type Image360DetailsProps = { appLanguage?: string; + enableExitButton?: boolean; }; -export function Image360Details({ appLanguage }: Image360DetailsProps): ReactElement { +export function Image360Details({ + appLanguage, + enableExitButton = true +}: Image360DetailsProps): ReactElement { const viewer = useReveal(); const [enteredEntity, setEnteredEntity] = useState(); const [is360HistoricalPanelExpanded, setIs360HistoricalPanelExpanded] = useState(false); @@ -57,9 +61,15 @@ export function Image360Details({ appLanguage }: Image360DetailsProps): ReactEle fallbackLanguage={appLanguage} /> - - type="tertiary" onClick={exitImage360Image} /> - + {enableExitButton && ( + + + type="tertiary" + onClick={exitImage360Image} + /> + + )} )} diff --git a/react-components/src/components/Reveal3DResources/index.ts b/react-components/src/components/Reveal3DResources/index.ts index 0eb9630058a..b50d29764cd 100644 --- a/react-components/src/components/Reveal3DResources/index.ts +++ b/react-components/src/components/Reveal3DResources/index.ts @@ -9,6 +9,7 @@ export type { AssetStylingGroup, DefaultResourceStyling, Image360AssetStylingGroup, + InstanceStylingGroup, CommonImage360Settings, TaggedAddCadResourceOptions, TaggedAddPointCloudResourceOptions, diff --git a/react-components/src/hooks/pointClouds/usePointCloudAnnotationMappingForAssetId.ts b/react-components/src/hooks/pointClouds/usePointCloudAnnotationMappingForAssetId.ts new file mode 100644 index 00000000000..c9254a73582 --- /dev/null +++ b/react-components/src/hooks/pointClouds/usePointCloudAnnotationMappingForAssetId.ts @@ -0,0 +1,49 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type UseQueryResult, useQuery } from '@tanstack/react-query'; +import { type PointCloudAnnotationMappedAssetData } from '../types'; +import { EMPTY_ARRAY } from '../../utilities/constants'; +import { type AnyIntersection } from '@cognite/reveal'; +import { queryKeys } from '../../utilities/queryKeys'; +import { usePointCloudAnnotationCache } from '../../components/CacheProvider/CacheProvider'; +import { fetchAnnotationsForModel } from './fetchAnnotationsForModel'; + +export const usePointCloudAnnotationMappingForAssetId = ( + intersection: AnyIntersection | undefined +): UseQueryResult => { + const pointCloudAnnotationCache = usePointCloudAnnotationCache(); + + const isPointCloudIntersection = intersection?.type === 'pointcloud'; + const [modelId, revisionId, assetId] = isPointCloudIntersection + ? [ + intersection.model.modelId, + intersection.model.revisionId, + intersection.assetRef?.externalId ?? intersection.assetRef?.id + ] + : [undefined, undefined, undefined]; + + return useQuery({ + queryKey: [ + queryKeys.pointCloudAnnotationForAssetId( + `${modelId}/${revisionId}`, + assetId?.toString() ?? '' + ) + ], + queryFn: async () => { + if (modelId === undefined || revisionId === undefined || assetId === undefined) { + return EMPTY_ARRAY; + } + const result = await fetchAnnotationsForModel( + modelId, + revisionId, + [assetId], + pointCloudAnnotationCache + ); + return result ?? EMPTY_ARRAY; + }, + staleTime: Infinity, + enabled: isPointCloudIntersection && assetId !== undefined + }); +}; diff --git a/react-components/src/hooks/useClickedNode.tsx b/react-components/src/hooks/useClickedNode.tsx index ded58aed967..07b02a65123 100644 --- a/react-components/src/hooks/useClickedNode.tsx +++ b/react-components/src/hooks/useClickedNode.tsx @@ -18,8 +18,8 @@ import { type DmsUniqueIdentifier, type Source } from '../data-providers/FdmSDK' import { useRenderTarget, useReveal } from '../components/RevealCanvas/ViewerContext'; import { isActiveEditTool } from '../architecture/base/commands/BaseEditTool'; import { - type PointCloudVolumeAssetWithViews, - usePointCloudVolumeMappingForIntersection + type PointCloudFdmVolumeMappingWithViews, + usePointCloudFdmVolumeMappingForIntersection } from '../query/core-dm/usePointCloudVolumeMappingForAssetInstances'; import { useAssetMappingForTreeIndex, useFdm3dNodeDataPromises } from './cad'; @@ -40,7 +40,7 @@ export type ClickedNodeData = { fdmResult?: FdmNodeDataResult; assetMappingResult?: AssetMappingDataResult; pointCloudAnnotationMappingResult?: PointCloudAnnotationMappedAssetData[]; - pointCloudVolumeAssetMappingResult?: PointCloudVolumeAssetWithViews[]; + pointCloudFdmVolumeMappingResult?: PointCloudFdmVolumeMappingWithViews[]; intersection: AnyIntersection | Image360AnnotationIntersection; }; @@ -58,7 +58,7 @@ export const useClickedNodeData = (options?: { const [intersection, setIntersection] = useState(undefined); - const [annotationIntersection, setAnnotationIntersection] = useState< + const [image360AnnotationIntersection, setImage360AnnotationIntersection] = useState< Image360AnnotationIntersection | undefined >(undefined); @@ -99,9 +99,9 @@ export const useClickedNodeData = (options?: { } if (annotationIntersection !== null) { - setAnnotationIntersection(annotationIntersection); + setImage360AnnotationIntersection(annotationIntersection); } else { - setAnnotationIntersection(undefined); + setImage360AnnotationIntersection(undefined); } })(); }; @@ -117,20 +117,20 @@ export const useClickedNodeData = (options?: { const { data: assetMappingResult } = useAssetMappingForTreeIndex(intersection); - const { data: pointCloudAssetMappingResult } = + const { data: pointCloudAnnotationMappingResult } = usePointCloudAnnotationMappingForIntersection(intersection); - const { data: pointCloudAssetMappingVolumeResult } = - usePointCloudVolumeMappingForIntersection(intersection); + const { data: pointCloudFdmVolumeMappingResult } = + usePointCloudFdmVolumeMappingForIntersection(intersection); return useCombinedClickedNodeData( mouseButton, position, nodeDataPromises, assetMappingResult, - pointCloudAssetMappingResult, - pointCloudAssetMappingVolumeResult, - annotationIntersection ?? intersection + pointCloudAnnotationMappingResult, + pointCloudFdmVolumeMappingResult, + image360AnnotationIntersection ?? intersection ); }; @@ -140,7 +140,7 @@ const useCombinedClickedNodeData = ( fdmPromises: FdmNodeDataPromises | undefined, assetMappings: NodeAssetMappingResult | undefined, pointCloudAssetMappings: PointCloudAnnotationMappedAssetData[] | undefined, - pointCloudVolumeAssetMappings: PointCloudVolumeAssetWithViews[] | undefined, + pointCloudFdmVolumeMappings: PointCloudFdmVolumeMappingWithViews[] | undefined, intersection: AnyIntersection | Image360AnnotationIntersection | undefined ): ClickedNodeData | undefined => { const [clickedNodeData, setClickedNodeData] = useState(); @@ -166,7 +166,7 @@ const useCombinedClickedNodeData = ( fdmResult: fdmData, assetMappingResult: assetMappingData, pointCloudAnnotationMappingResult: pointCloudAssetMappings, - pointCloudVolumeAssetMappingResult: pointCloudVolumeAssetMappings, + pointCloudFdmVolumeMappingResult: pointCloudFdmVolumeMappings, intersection }); }, [ @@ -174,7 +174,7 @@ const useCombinedClickedNodeData = ( fdmData, assetMappings?.node, pointCloudAssetMappings, - pointCloudVolumeAssetMappings + pointCloudFdmVolumeMappings ]); return clickedNodeData; diff --git a/react-components/src/query/core-dm/usePointCloudDMVolumes.ts b/react-components/src/query/core-dm/usePointCloudDMVolumes.ts index 4eaa1b05945..41699583024 100644 --- a/react-components/src/query/core-dm/usePointCloudDMVolumes.ts +++ b/react-components/src/query/core-dm/usePointCloudDMVolumes.ts @@ -36,7 +36,7 @@ export const usePointCloudDMVolumes = ( queryFn: async () => { return await Promise.all( modelOptions.map(async (model) => { - const pointCloudDMVolumeWithAsset = await getPointCloudDMVolumesForModel( + const pointCloudDMVolumeWithAsset = await fetchPointCloudDMVolumesForModel( model.modelId, model.revisionId, fdmSdk @@ -53,7 +53,7 @@ export const usePointCloudDMVolumes = ( }); }; -const getPointCloudDMVolumesForModel = async ( +const fetchPointCloudDMVolumesForModel = async ( modelId: number, revisionId: number, fdmSdk: FdmSDK diff --git a/react-components/src/query/core-dm/usePointCloudVolumeMappingForAssetInstances.ts b/react-components/src/query/core-dm/usePointCloudVolumeMappingForAssetInstances.ts index 3d81a7248da..d81b1ca104b 100644 --- a/react-components/src/query/core-dm/usePointCloudVolumeMappingForAssetInstances.ts +++ b/react-components/src/query/core-dm/usePointCloudVolumeMappingForAssetInstances.ts @@ -21,14 +21,14 @@ export type PointCloudVolumeMappedAssetData = { asset: DMInstanceRef & AssetProperties; }; -export type PointCloudVolumeAssetWithViews = { +export type PointCloudFdmVolumeMappingWithViews = { views: Source[]; assetInstance: DMInstanceRef; }; export const usePointCloudVolumeMappingForAssetInstances = ( assetInstanceRefs: DmsUniqueIdentifier[] -): UseQueryResult => { +): PointCloudVolumeMappedAssetData[] => { const { data: models } = usePointCloudModelRevisionIdsFromReveal(); const addClassicModelOptionsResults = useModelIdRevisionIdFromModelOptions(models); @@ -36,62 +36,46 @@ export const usePointCloudVolumeMappingForAssetInstances = ( () => addClassicModelOptionsResults.map((result) => result.data).filter(isDefined), [addClassicModelOptionsResults] ); - const { data: pointCloudVolumeResults, isLoading } = usePointCloudDMVolumes(classicModelOptions); + const { data: pointCloudVolumeResults } = usePointCloudDMVolumes(classicModelOptions); - return useQuery({ - queryKey: [ - queryKeys.pointCloudDMVolumeAssetMappings( - classicModelOptions.map((model) => `${model.modelId}/${model.revisionId}`).sort(), - assetInstanceRefs - .map((assetInstance) => `${assetInstance.space}/${assetInstance.externalId}`) - .sort() - ) - ], - queryFn: async () => { - if (classicModelOptions.length === 0 || assetInstanceRefs.length === 0) { - return EMPTY_ARRAY; - } + return useMemo(() => { + if (classicModelOptions.length === 0 || assetInstanceRefs.length === 0) { + return EMPTY_ARRAY; + } - const result: PointCloudVolumeMappedAssetData[] = - pointCloudVolumeResults?.flatMap((pointCloudVolumeDataResult) => - pointCloudVolumeDataResult.pointCloudDMVolumeWithAsset - .filter((pointCloudDMVolumeWithAsset) => - assetInstanceRefs.some( - (assetInstance) => - pointCloudDMVolumeWithAsset.dmAsset !== undefined && - assetInstance.externalId === pointCloudDMVolumeWithAsset.dmAsset.externalId && - assetInstance.space === pointCloudDMVolumeWithAsset.dmAsset.space - ) + const result: PointCloudVolumeMappedAssetData[] = + pointCloudVolumeResults?.flatMap((pointCloudVolumeDataResult) => + pointCloudVolumeDataResult.pointCloudDMVolumeWithAsset + .filter((pointCloudDMVolumeWithAsset) => + assetInstanceRefs.some( + (assetInstance) => + pointCloudDMVolumeWithAsset.dmAsset !== undefined && + assetInstance.externalId === pointCloudDMVolumeWithAsset.dmAsset.externalId && + assetInstance.space === pointCloudDMVolumeWithAsset.dmAsset.space ) - .map((pointCloudDMVolumeWithAsset) => { - if (pointCloudDMVolumeWithAsset.dmAsset === undefined) { - return undefined; - } - return { - volumeInstanceRef: { - externalId: pointCloudDMVolumeWithAsset.externalId, - space: pointCloudDMVolumeWithAsset.space - }, - asset: pointCloudDMVolumeWithAsset.dmAsset - }; - }) - .filter(isDefined) - ) ?? EMPTY_ARRAY; + ) + .map((pointCloudDMVolumeWithAsset) => { + if (pointCloudDMVolumeWithAsset.dmAsset === undefined) { + return undefined; + } + return { + volumeInstanceRef: { + externalId: pointCloudDMVolumeWithAsset.externalId, + space: pointCloudDMVolumeWithAsset.space + }, + asset: pointCloudDMVolumeWithAsset.dmAsset + }; + }) + .filter(isDefined) + ) ?? EMPTY_ARRAY; - return result; - }, - staleTime: Infinity, - enabled: - pointCloudVolumeResults !== undefined && - pointCloudVolumeResults.length > 0 && - assetInstanceRefs.length > 0 && - !isLoading - }); + return result; + }, [classicModelOptions, assetInstanceRefs]); }; -export const usePointCloudVolumeMappingForIntersection = ( +export const usePointCloudFdmVolumeMappingForIntersection = ( intersection: AnyIntersection | undefined -): UseQueryResult => { +): UseQueryResult => { const fdmSdk = useFdmSdk(); const assetInstanceRefs = useMemo(() => { @@ -104,8 +88,7 @@ export const usePointCloudVolumeMappingForIntersection = ( return []; }, [intersection]); - const { data: volumeMappings, isLoading } = - usePointCloudVolumeMappingForAssetInstances(assetInstanceRefs); + const volumeMappings = usePointCloudVolumeMappingForAssetInstances(assetInstanceRefs); return useQuery({ queryKey: [ @@ -119,7 +102,7 @@ export const usePointCloudVolumeMappingForIntersection = ( if (volumeMappings === undefined || volumeMappings.length === 0) { return EMPTY_ARRAY; } - const result: PointCloudVolumeAssetWithViews[] = await Promise.all( + const result: PointCloudFdmVolumeMappingWithViews[] = await Promise.all( volumeMappings.map(async (volumeMapping) => { const assetInstance = { externalId: volumeMapping.asset.externalId, @@ -134,6 +117,6 @@ export const usePointCloudVolumeMappingForIntersection = ( ); return result; }, - enabled: volumeMappings !== undefined && volumeMappings.length > 0 && !isLoading + enabled: volumeMappings !== undefined && volumeMappings.length > 0 }); }; diff --git a/react-components/src/utilities/getInstancesFromClick.ts b/react-components/src/utilities/getInstancesFromClick.ts new file mode 100644 index 00000000000..8eadeb0522d --- /dev/null +++ b/react-components/src/utilities/getInstancesFromClick.ts @@ -0,0 +1,146 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { + type CadIntersection, + type ClassicDataSourceType, + type DataSourceType, + type DMDataSourceType, + type PointCloudIntersection +} from '@cognite/reveal'; +import { type InstanceReference } from '../data-providers'; +import { type CdfCaches } from '../architecture/base/renderTarget/CdfCaches'; +import { fetchAncestorNodesForTreeIndex } from '../components/CacheProvider/requests'; +import { EMPTY_ARRAY } from './constants'; +import { fetchAnnotationsForModel } from '../hooks/pointClouds/fetchAnnotationsForModel'; +import { isDMIdentifier } from '../components'; +import { is360ImageAnnotation } from './is360ImageAnnotation'; +import { type RevealRenderTarget } from '../architecture'; + +export async function getInstancesFromClick( + renderTarget: RevealRenderTarget, + event: PointerEvent +): Promise { + const viewer = renderTarget.viewer; + const caches = renderTarget.cdfCaches; + + const pixelCoordinates = viewer.getPixelCoordinatesFromEvent(event); + const intersection = await viewer.getAnyIntersectionFromPixel(pixelCoordinates); + const image360AnnotationIntersection = await viewer.get360AnnotationIntersectionFromPixel( + event.offsetX, + event.offsetY + ); + + const image360AnnotationData = image360AnnotationIntersection?.annotation.annotation.data; + const has360Asset = + image360AnnotationData !== undefined && is360ImageAnnotation(image360AnnotationData); + + if (has360Asset && image360AnnotationData.assetRef.id !== undefined) { + return [{ assetId: image360AnnotationData.assetRef.id }]; + } + + if (intersection === undefined) { + return undefined; + } + + if (intersection.type === 'cad') { + return await getInstancesFromCadIntersection(intersection, caches); + } else if (intersection.type === 'pointcloud') { + return await getInstancesFromPointCloudIntersection(intersection, caches); + } + + return undefined; +} + +async function getInstancesFromPointCloudIntersection( + intersection: PointCloudIntersection, + caches: CdfCaches +): Promise { + if (isDMIdentifier(intersection.model.modelIdentifier)) { + return getPointCloudFdmInstancesFromIntersection( + intersection as PointCloudIntersection + ); + } else { + return await getPointCloudAnnotationMappingsFromIntersection( + intersection as PointCloudIntersection, + caches + ); + } +} + +async function getPointCloudAnnotationMappingsFromIntersection( + intersection: PointCloudIntersection, + caches: CdfCaches +): Promise { + if (intersection.volumeMetadata?.assetRef?.id !== undefined) { + return [{ assetId: intersection.volumeMetadata.assetRef.id }]; + } + const assetExternalId = intersection.volumeMetadata?.assetRef?.externalId; + + if (assetExternalId === undefined) { + return []; + } + + const annotations = await fetchAnnotationsForModel( + intersection.model.modelIdentifier.modelId, + intersection.model.modelIdentifier.revisionId, + [assetExternalId], + caches.pointCloudAnnotationCache + ); + + return annotations?.map((annotation) => ({ assetId: annotation.asset.id })) ?? EMPTY_ARRAY; +} + +function getPointCloudFdmInstancesFromIntersection( + intersection: PointCloudIntersection +): InstanceReference[] { + return intersection.volumeMetadata?.assetRef === undefined + ? EMPTY_ARRAY + : [intersection.volumeMetadata.assetRef]; +} + +async function getInstancesFromCadIntersection( + intersection: CadIntersection, + caches: CdfCaches +): Promise { + const fdmDataPromise = getCadFdmDataPromise(intersection, caches); + + const assetMappingPromise = getAssetMappingPromise(intersection, caches); + + const [fdmData, assetMapping] = await Promise.all([fdmDataPromise, assetMappingPromise] as const); + return [...fdmData, ...assetMapping]; +} + +async function getCadFdmDataPromise( + intersection: CadIntersection, + caches: CdfCaches +): Promise { + const fdmNodeDataPromises = caches.fdmNodeCache.getClosestParentDataPromises( + intersection.model.modelId, + intersection.model.revisionId, + intersection.treeIndex + ); + + return (await fdmNodeDataPromises.cadAndFdmNodesPromise)?.fdmIds ?? EMPTY_ARRAY; +} + +async function getAssetMappingPromise( + intersection: CadIntersection, + caches: CdfCaches +): Promise { + const ancestors = await fetchAncestorNodesForTreeIndex( + intersection.model.modelId, + intersection.model.revisionId, + intersection.treeIndex, + caches.cogniteClient + ); + + const nodeAssetResult = await caches.assetMappingAndNode3dCache.getAssetMappingsForLowestAncestor( + intersection.model.modelId, + intersection.model.revisionId, + ancestors + ); + + return nodeAssetResult.mappings.map((mapping) => ({ assetId: mapping.assetId })); +}