init
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import type { DMInstanceRef } from '@cognite/reveal';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
useInstancesWithBoundingBoxes,
|
||||
type InstancesWithBoxesAndOriginalInstance,
|
||||
} from './useInstancesWithBoundingBoxes';
|
||||
import { useFindRelated3dInstances } from './useFindRelated3dInstances';
|
||||
|
||||
export const useInstancesWithBounds = (
|
||||
inputInstances: DMInstanceRef[],
|
||||
originalInstance: DMInstanceRef
|
||||
): InstancesWithBoxesAndOriginalInstance | undefined => {
|
||||
const instancesWithBounds = useInstancesWithBoundingBoxes(inputInstances);
|
||||
|
||||
return useMemo<InstancesWithBoxesAndOriginalInstance | undefined>(() => {
|
||||
if (inputInstances.length === 0 || instancesWithBounds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return { instancesWithBoxes: [...instancesWithBounds], originalInstance };
|
||||
}, [instancesWithBounds, inputInstances.length, originalInstance]);
|
||||
};
|
||||
|
||||
export function use3dDataForSelectedInstance(
|
||||
instance: DMInstanceRef
|
||||
): InstancesWithBoxesAndOriginalInstance | undefined {
|
||||
const threeDRelatedSelection = useFindRelated3dInstances(instance);
|
||||
const selectedInstancesWithBoundsAndCorrespondingInstance =
|
||||
useInstancesWithBounds(threeDRelatedSelection, instance);
|
||||
return selectedInstancesWithBoundsAndCorrespondingInstance;
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { DMInstanceRef } from '@cognite/reveal';
|
||||
import type { Node3D, CogniteClient } from '@cognite/sdk';
|
||||
import { useRevealContext } from './useRevealContext';
|
||||
import type { ThreeDModelFdmMappings, CadModelOptions } from '../types';
|
||||
import {
|
||||
ASSET_VIEW,
|
||||
COGNITE_3D_OBJECT_VIEW,
|
||||
COGNITE_CAD_NODE_VIEW,
|
||||
} from '../utils/views';
|
||||
import { unwrapProperties } from '../utils/data-mapper';
|
||||
import type { CDFNode } from '../utils/cdf-types';
|
||||
|
||||
interface DmsUniqueIdentifier {
|
||||
space: string;
|
||||
externalId: string;
|
||||
}
|
||||
|
||||
interface CogniteAssetProperties {
|
||||
space: string;
|
||||
externalId: string;
|
||||
object3D: DmsUniqueIdentifier;
|
||||
}
|
||||
|
||||
interface CogniteCADNodeProperties {
|
||||
space: string;
|
||||
externalId: string;
|
||||
object3D: DmsUniqueIdentifier;
|
||||
model3D: DmsUniqueIdentifier;
|
||||
revisions: DmsUniqueIdentifier[];
|
||||
treeIndexes: number[];
|
||||
subTreeSizes: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches FDM-to-CAD mappings using Core DM connections.
|
||||
* This queries the data model for CAD nodes connected to assets via object3D references.
|
||||
*/
|
||||
export function useFdmAssetMappings(
|
||||
instances: DMInstanceRef[],
|
||||
models: CadModelOptions[]
|
||||
) {
|
||||
const { sdk } = useRevealContext();
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
'fdm-cad-connections',
|
||||
instances.map((i) => `${i.space}:${i.externalId}`).join(','),
|
||||
models.map((m) => `${m.modelId}:${m.revisionId}`).join(','),
|
||||
],
|
||||
queryFn: async (): Promise<ThreeDModelFdmMappings[]> => {
|
||||
if (instances.length === 0 || models.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Query DMS for CAD connections
|
||||
// This traverses: Assets → object3D → CAD nodes
|
||||
const queryResult = await sdk.instances.query({
|
||||
with: {
|
||||
// Start from the input instances (assets)
|
||||
assets: {
|
||||
nodes: {
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
in: {
|
||||
property: ['node', 'space'],
|
||||
values: [
|
||||
...new Set(instances.map((inst) => inst.space)),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
in: {
|
||||
property: ['node', 'externalId'],
|
||||
values: instances.map((inst) => inst.externalId),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Navigate to object3D (Cognite3DObject)
|
||||
object_3ds: {
|
||||
nodes: {
|
||||
from: 'assets',
|
||||
through: {
|
||||
view: { type: 'view', ...ASSET_VIEW },
|
||||
identifier: 'object3D',
|
||||
},
|
||||
direction: 'outwards',
|
||||
filter: {
|
||||
hasData: [{ type: 'view', ...COGNITE_3D_OBJECT_VIEW }],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Navigate back to CAD nodes that reference this object3D
|
||||
cad_nodes: {
|
||||
nodes: {
|
||||
from: 'object_3ds',
|
||||
through: {
|
||||
view: { type: 'view', ...COGNITE_CAD_NODE_VIEW },
|
||||
identifier: 'object3D',
|
||||
},
|
||||
direction: 'inwards',
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
assets: {
|
||||
sources: [
|
||||
{
|
||||
source: { type: 'view', ...ASSET_VIEW },
|
||||
properties: ['object3D'],
|
||||
},
|
||||
],
|
||||
},
|
||||
cad_nodes: {
|
||||
sources: [
|
||||
{
|
||||
source: { type: 'view', ...COGNITE_CAD_NODE_VIEW },
|
||||
properties: [
|
||||
'object3D',
|
||||
'model3D',
|
||||
'revisions',
|
||||
'treeIndexes',
|
||||
'subTreeSizes',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Step 2: Build mappings per model/revision
|
||||
const mappingsByModel = new Map<string, Map<string, Node3D[]>>();
|
||||
|
||||
const cadNodes = queryResult.items.cad_nodes || [];
|
||||
|
||||
// Group CAD nodes by which instances reference them
|
||||
const object3DToAssets = new Map<string, DMInstanceRef[]>();
|
||||
for (const asset of queryResult.items.assets || []) {
|
||||
const props = unwrapProperties<CogniteAssetProperties>(
|
||||
asset as CDFNode,
|
||||
ASSET_VIEW
|
||||
);
|
||||
if (props?.object3D) {
|
||||
const key = `${props.object3D.space}/${props.object3D.externalId}`;
|
||||
const existing = object3DToAssets.get(key) || [];
|
||||
existing.push({ space: asset.space, externalId: asset.externalId });
|
||||
object3DToAssets.set(key, existing);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all (modelId, revisionId, treeIndex) tuples for batch fetching
|
||||
interface NodeRequest {
|
||||
modelId: number;
|
||||
revisionId: number;
|
||||
treeIndex: number;
|
||||
assetInstances: DMInstanceRef[];
|
||||
}
|
||||
|
||||
const nodeRequests: NodeRequest[] = [];
|
||||
|
||||
// Process CAD nodes to build Node3D mappings
|
||||
for (const cadNode of cadNodes) {
|
||||
const props = unwrapProperties<CogniteCADNodeProperties>(
|
||||
cadNode as CDFNode,
|
||||
COGNITE_CAD_NODE_VIEW
|
||||
);
|
||||
if (!props) continue;
|
||||
|
||||
const { model3D, revisions, treeIndexes, object3D } = props;
|
||||
if (!model3D || !revisions || !treeIndexes) continue;
|
||||
|
||||
// Find which assets reference this CAD node
|
||||
const object3DKey = `${object3D.space}/${object3D.externalId}`;
|
||||
const relatedAssets = object3DToAssets.get(object3DKey);
|
||||
if (!relatedAssets) continue;
|
||||
|
||||
// Extract modelId and match with requested models
|
||||
const modelId = extractModelId(model3D.externalId);
|
||||
|
||||
// For each revision/treeIndex pair
|
||||
for (let i = 0; i < revisions.length; i++) {
|
||||
const revision = revisions[i];
|
||||
const treeIndex = treeIndexes[i];
|
||||
const revisionId = extractRevisionId(revision.externalId);
|
||||
|
||||
// Check if this model/revision is in our requested list
|
||||
const matchingModel = models.find(
|
||||
(m) => m.modelId === modelId && m.revisionId === revisionId
|
||||
);
|
||||
if (!matchingModel) continue;
|
||||
|
||||
nodeRequests.push({
|
||||
modelId,
|
||||
revisionId,
|
||||
treeIndex,
|
||||
assetInstances: relatedAssets,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Batch fetch nodes by revision
|
||||
const nodesByRevision = new Map<string, NodeRequest[]>();
|
||||
for (const req of nodeRequests) {
|
||||
const key = `${req.modelId}/${req.revisionId}`;
|
||||
const existing = nodesByRevision.get(key) || [];
|
||||
existing.push(req);
|
||||
nodesByRevision.set(key, existing);
|
||||
}
|
||||
|
||||
// Fetch all nodes in parallel per revision
|
||||
const revisionFetchPromises = Array.from(nodesByRevision.entries()).map(
|
||||
async ([revisionKey, requests]) => {
|
||||
const [modelId, revisionId] = revisionKey.split('/').map(Number);
|
||||
const treeIndexes = requests.map((r) => r.treeIndex);
|
||||
|
||||
const nodes = await fetchNodesByTreeIndex(
|
||||
sdk,
|
||||
modelId,
|
||||
revisionId,
|
||||
treeIndexes
|
||||
);
|
||||
|
||||
return { revisionKey, nodes, requests };
|
||||
}
|
||||
);
|
||||
|
||||
const allRevisionData = await Promise.all(revisionFetchPromises);
|
||||
|
||||
for (const { revisionKey, nodes, requests } of allRevisionData) {
|
||||
const treeIndexToNode = new Map(
|
||||
nodes.map((node) => [node.treeIndex, node])
|
||||
);
|
||||
|
||||
const modelMappings =
|
||||
mappingsByModel.get(revisionKey) ?? new Map<string, Node3D[]>();
|
||||
mappingsByModel.set(revisionKey, modelMappings);
|
||||
|
||||
for (const req of requests) {
|
||||
const node3D = treeIndexToNode.get(req.treeIndex);
|
||||
if (!node3D) continue;
|
||||
|
||||
for (const instance of req.assetInstances) {
|
||||
const instanceKey = `${instance.space}:${instance.externalId}`;
|
||||
const arr = modelMappings.get(instanceKey) ?? [];
|
||||
arr.push(node3D);
|
||||
modelMappings.set(instanceKey, arr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to result format
|
||||
const results: ThreeDModelFdmMappings[] = [];
|
||||
for (const model of models) {
|
||||
const modelKey = `${model.modelId}/${model.revisionId}`;
|
||||
results.push({
|
||||
modelId: model.modelId,
|
||||
revisionId: model.revisionId,
|
||||
mappings: mappingsByModel.get(modelKey) ?? new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error fetching FDM CAD connections:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
enabled: !!sdk && instances.length > 0 && models.length > 0,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to extract numeric modelId from externalId like "model_123_space"
|
||||
function extractModelId(externalId: string): number {
|
||||
const match = externalId.match(/model_(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : -1;
|
||||
}
|
||||
|
||||
// Helper to extract numeric revisionId from externalId like "model_123_revision_456_space"
|
||||
function extractRevisionId(externalId: string): number {
|
||||
const match = externalId.match(/revision_(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch 3D nodes by their tree indices using the optimized internal IDs endpoint.
|
||||
* This is much more efficient than fetching all nodes and filtering.
|
||||
*/
|
||||
async function fetchNodesByTreeIndex(
|
||||
sdk: CogniteClient,
|
||||
modelId: number,
|
||||
revisionId: number,
|
||||
treeIndexes: number[]
|
||||
): Promise<Node3D[]> {
|
||||
if (treeIndexes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Deduplicate tree indices
|
||||
const uniqueTreeIndexes = Array.from(new Set(treeIndexes));
|
||||
|
||||
// Step 1: Convert tree indices to internal node IDs
|
||||
const nodeIdResponse = await sdk.post<{ items: number[] }>(
|
||||
`/api/v1/projects/${sdk.project}/3d/models/${modelId}/revisions/${revisionId}/nodes/internalids/bytreeindices`,
|
||||
{
|
||||
data: {
|
||||
items: uniqueTreeIndexes,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const nodeIds = nodeIdResponse.data.items;
|
||||
|
||||
if (nodeIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Step 2: Retrieve full node details by internal IDs
|
||||
const nodes = await sdk.revisions3D.retrieve3DNodes(
|
||||
modelId,
|
||||
revisionId,
|
||||
nodeIds.map((id) => ({ id }))
|
||||
);
|
||||
|
||||
return nodes;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
use3dRelatedDirectConnections,
|
||||
use3dRelatedEdgeConnections,
|
||||
} from './useRelatedInstances';
|
||||
import type { DMInstanceRef } from '@cognite/reveal';
|
||||
|
||||
export const useFindRelated3dInstances = (
|
||||
instance: DMInstanceRef
|
||||
): DMInstanceRef[] => {
|
||||
const edgeRelationData = use3dRelatedEdgeConnections(instance);
|
||||
const directRelationData = use3dRelatedDirectConnections(instance);
|
||||
|
||||
return useMemo<DMInstanceRef[]>(() => {
|
||||
const edgeDirectRelationData = [
|
||||
...(edgeRelationData.data ?? []),
|
||||
...(directRelationData.data ?? []),
|
||||
];
|
||||
return [instance, ...edgeDirectRelationData];
|
||||
}, [instance, edgeRelationData.data, directRelationData.data]);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { DefaultCameraManager, type DMInstanceRef } from '@cognite/reveal';
|
||||
import { useReveal } from './useReveal';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Box3, Vector3 } from 'three';
|
||||
import { use3dDataForSelectedInstance } from './use3dDataForSelectedInstance';
|
||||
import type { InstanceWithBoundingBox } from './useInstancesWithBoundingBoxes';
|
||||
|
||||
/**
|
||||
* Calculate an angled camera position for a bounding box
|
||||
* @param box - The bounding box to frame
|
||||
* @returns Camera position and target vectors
|
||||
*/
|
||||
function calculateAngledCameraPosition(box: Box3): {
|
||||
position: Vector3;
|
||||
target: Vector3;
|
||||
} {
|
||||
// Get bounding box center and size
|
||||
const center = new Vector3();
|
||||
box.getCenter(center);
|
||||
|
||||
const size = new Vector3();
|
||||
box.getSize(size);
|
||||
|
||||
// Calculate the maximum dimension to determine camera distance
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const fov = 60; // Field of view in degrees
|
||||
const cameraDistance = (maxDim / (2 * Math.tan((fov * Math.PI) / 360))) * 1.5;
|
||||
|
||||
// Position camera at 45-degree angle (above and to the side)
|
||||
// Using spherical coordinates: 45° elevation, 45° azimuth
|
||||
const angle = Math.PI / 4; // 45 degrees
|
||||
const cameraPosition = new Vector3(
|
||||
center.x + cameraDistance * Math.cos(angle) * Math.cos(angle),
|
||||
center.y + cameraDistance * Math.sin(angle),
|
||||
center.z + cameraDistance * Math.cos(angle) * Math.sin(angle)
|
||||
);
|
||||
|
||||
return {
|
||||
position: cameraPosition,
|
||||
target: center,
|
||||
};
|
||||
}
|
||||
|
||||
const useFocusCameraWithInstanceBox = (
|
||||
instancesWithBoundingBox: InstanceWithBoundingBox[]
|
||||
) => {
|
||||
const viewer = useReveal();
|
||||
|
||||
useEffect(() => {
|
||||
if (viewer.cameraManager instanceof DefaultCameraManager) {
|
||||
viewer.cameraManager.setCameraControlsOptions({
|
||||
mouseWheelAction: 'zoomToCursor',
|
||||
});
|
||||
}
|
||||
}, [viewer.cameraManager]);
|
||||
|
||||
return useCallback(() => {
|
||||
if (instancesWithBoundingBox.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const box = instancesWithBoundingBox.reduce(
|
||||
(unionBox, instance) => unionBox.union(instance.boundingBox),
|
||||
new Box3()
|
||||
);
|
||||
|
||||
if (!box.isEmpty()) {
|
||||
const cameraState = calculateAngledCameraPosition(box);
|
||||
viewer.cameraManager.setCameraState(cameraState);
|
||||
}
|
||||
}, [instancesWithBoundingBox, viewer]);
|
||||
};
|
||||
|
||||
export const useFocusCamera = (instance: DMInstanceRef) => {
|
||||
const selectedInstanceData = use3dDataForSelectedInstance(instance);
|
||||
return useFocusCameraWithInstanceBox(
|
||||
selectedInstanceData?.instancesWithBoxes ?? []
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRenderTarget } from './useRenderTarget';
|
||||
import type { FdmAssetStylingGroup, InstanceStylingGroup } from '../types';
|
||||
import { DefaultNodeAppearance, type DMInstanceRef } from '@cognite/reveal';
|
||||
import { use3dDataForSelectedInstance } from './use3dDataForSelectedInstance';
|
||||
import type { InstanceWithBoundingBox } from './useInstancesWithBoundingBoxes';
|
||||
|
||||
const useCentralizedInstanceStyling = (): InstanceStylingGroup[] => {
|
||||
const [instanceStylingGroups, setInstanceStylingGroups] = useState<
|
||||
InstanceStylingGroup[]
|
||||
>([]);
|
||||
const instanceStylingController = useRenderTarget().instanceStylingController;
|
||||
|
||||
useEffect(() => {
|
||||
const onStylingChange = () => {
|
||||
setInstanceStylingGroups([
|
||||
...instanceStylingController.getStylingGroups(),
|
||||
]);
|
||||
};
|
||||
|
||||
instanceStylingController.addEventListener(onStylingChange);
|
||||
return () => {
|
||||
instanceStylingController.removeEventListener(onStylingChange);
|
||||
};
|
||||
}, [instanceStylingController]);
|
||||
|
||||
return instanceStylingGroups;
|
||||
};
|
||||
|
||||
const getInstanceStyling = (
|
||||
instances: InstanceWithBoundingBox[]
|
||||
): FdmAssetStylingGroup[] =>
|
||||
instances.length === 0
|
||||
? []
|
||||
: [
|
||||
{
|
||||
fdmAssetExternalIds: instances.map(({ instance }) => instance),
|
||||
style: {
|
||||
cad: DefaultNodeAppearance.Highlighted,
|
||||
pointcloud: DefaultNodeAppearance.Highlighted,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const useInstanceStyling = (instance: DMInstanceRef) => {
|
||||
const selectedInstancesAndOriginalInstance =
|
||||
use3dDataForSelectedInstance(instance);
|
||||
|
||||
const centralizedInstanceStyling = useCentralizedInstanceStyling();
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
...centralizedInstanceStyling,
|
||||
...getInstanceStyling(
|
||||
selectedInstancesAndOriginalInstance?.instancesWithBoxes ?? []
|
||||
),
|
||||
],
|
||||
[
|
||||
selectedInstancesAndOriginalInstance?.instancesWithBoxes,
|
||||
centralizedInstanceStyling,
|
||||
]
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { DMInstanceRef } from '@cognite/reveal';
|
||||
import { use3dModels } from './useModels';
|
||||
import { useFdmAssetMappings } from './useFdmMappings';
|
||||
import type { CadModelOptions, ThreeDModelFdmMappings, CogniteModel } from '../types';
|
||||
import type { Node3D } from '@cognite/sdk';
|
||||
import { useMemo } from 'react';
|
||||
import { Box3 } from 'three';
|
||||
|
||||
export type InstanceWithBoundingBox = {
|
||||
instance: DMInstanceRef;
|
||||
boundingBox: Box3;
|
||||
};
|
||||
|
||||
export type InstancesWithBoxesAndOriginalInstance = {
|
||||
instancesWithBoxes: InstanceWithBoundingBox[];
|
||||
originalInstance: DMInstanceRef;
|
||||
};
|
||||
|
||||
export type NodesWithModelInfo = {
|
||||
nodes: Node3D[];
|
||||
instance: DMInstanceRef;
|
||||
modelId: number;
|
||||
revisionId: number;
|
||||
};
|
||||
|
||||
const combineNodeBoundingBoxes = (nodes: Node3D[]): Box3 =>
|
||||
nodes.reduce(
|
||||
(currentBox, nextNode) =>
|
||||
currentBox.union(
|
||||
nextNode.boundingBox !== undefined
|
||||
? new Box3().setFromArray([
|
||||
...nextNode.boundingBox.min,
|
||||
...nextNode.boundingBox.max,
|
||||
])
|
||||
: new Box3()
|
||||
),
|
||||
new Box3()
|
||||
);
|
||||
|
||||
const getFdmDataWithBoundingBoxes = (
|
||||
modelsWithRelevantNodes: NodesWithModelInfo[],
|
||||
models: CogniteModel[]
|
||||
): InstanceWithBoundingBox[] => {
|
||||
const cdfCoordinateBoundingBoxes = modelsWithRelevantNodes.map(
|
||||
(nodesWithModel) => combineNodeBoundingBoxes(nodesWithModel.nodes)
|
||||
);
|
||||
|
||||
const selectedNodeCadModels = modelsWithRelevantNodes.map((nodeModelData) =>
|
||||
models.find(
|
||||
({ modelId, revisionId }) =>
|
||||
modelId === nodeModelData.modelId &&
|
||||
revisionId === nodeModelData.revisionId
|
||||
)
|
||||
);
|
||||
|
||||
if (
|
||||
selectedNodeCadModels.length === 0 ||
|
||||
cdfCoordinateBoundingBoxes.length === 0
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const viewerCoordinateBoundingBoxes = selectedNodeCadModels
|
||||
.map((model, ind) =>
|
||||
model?.mapBoxFromCdfToModelCoordinates(cdfCoordinateBoundingBoxes[ind])
|
||||
)
|
||||
.filter((val) => val !== undefined);
|
||||
|
||||
return viewerCoordinateBoundingBoxes.map((boundingBox, ind) => ({
|
||||
instance: modelsWithRelevantNodes[ind].instance!,
|
||||
boundingBox,
|
||||
}));
|
||||
};
|
||||
|
||||
export function getNodesFromModelsFdmMappings(
|
||||
instances: DMInstanceRef[],
|
||||
mappings?: ThreeDModelFdmMappings[]
|
||||
): NodesWithModelInfo[] {
|
||||
const nodesWithModelIds = mappings?.flatMap((modelMappings) =>
|
||||
instances.reduce((infoArray, instance) => {
|
||||
const nodes = modelMappings.mappings.get(`${instance.space}:${instance.externalId}`);
|
||||
if (nodes === undefined) {
|
||||
return infoArray;
|
||||
}
|
||||
infoArray.push({
|
||||
instance,
|
||||
modelId: modelMappings.modelId,
|
||||
revisionId: modelMappings.revisionId,
|
||||
nodes,
|
||||
});
|
||||
return infoArray;
|
||||
}, new Array<NodesWithModelInfo>())
|
||||
);
|
||||
return nodesWithModelIds ?? [];
|
||||
}
|
||||
|
||||
const getBoundingBoxInstancesFromFdmAndModelMappings = (
|
||||
instances: DMInstanceRef[],
|
||||
modelMappings: ThreeDModelFdmMappings[] | undefined,
|
||||
models: CogniteModel[]
|
||||
): InstanceWithBoundingBox[] => {
|
||||
const modelsWithRelevantNodes = getNodesFromModelsFdmMappings(
|
||||
instances,
|
||||
modelMappings
|
||||
);
|
||||
if (modelsWithRelevantNodes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return getFdmDataWithBoundingBoxes(modelsWithRelevantNodes, models);
|
||||
};
|
||||
|
||||
export const useInstancesWithBoundingBoxes = (
|
||||
inputInstances: DMInstanceRef[]
|
||||
) => {
|
||||
const models = use3dModels();
|
||||
const { data: modelNodeMappings } = useFdmAssetMappings(
|
||||
inputInstances,
|
||||
models as CadModelOptions[]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
getBoundingBoxInstancesFromFdmAndModelMappings(
|
||||
inputInstances,
|
||||
modelNodeMappings,
|
||||
models
|
||||
),
|
||||
[modelNodeMappings, inputInstances, models]
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { DMInstanceRef } from '@cognite/reveal';
|
||||
import { useRevealContext } from './useRevealContext';
|
||||
import type { TaggedAddResourceOptions } from '../types';
|
||||
import { useReveal } from './useReveal';
|
||||
import {
|
||||
COGNITE_VISUALIZABLE_VIEW,
|
||||
COGNITE_CAD_NODE_VIEW,
|
||||
} from '../utils/views';
|
||||
import { unwrapProperties } from '../utils/data-mapper';
|
||||
import type { CDFNode } from '../utils/cdf-types';
|
||||
|
||||
interface CogniteCADNodeProperties {
|
||||
space: string;
|
||||
externalId: string;
|
||||
model3D: { externalId: string; space: string };
|
||||
revisions: Array<{ externalId: string; space: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts numeric ID from CDF external ID format (e.g., "cog_3d_model_12345" -> 12345)
|
||||
*/
|
||||
function extractNumericId(externalId: string): number | undefined {
|
||||
const lastUnderscoreIndex = externalId.lastIndexOf('_');
|
||||
if (lastUnderscoreIndex === -1) return undefined;
|
||||
|
||||
const numericPart = externalId.substring(lastUnderscoreIndex + 1);
|
||||
const id = parseInt(numericPart, 10);
|
||||
return isNaN(id) ? undefined : id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches 3D CAD models associated with an FDM instance via the Cognite Core Data Model.
|
||||
* Traverses: Asset -> object3D (CogniteVisualizable) -> CogniteCADNode -> models/revisions
|
||||
*/
|
||||
export function useModelsForInstanceQuery(instance: DMInstanceRef) {
|
||||
const { sdk } = useRevealContext();
|
||||
|
||||
const result = useQuery({
|
||||
queryKey: ['3d-models-for-instance', instance.space, instance.externalId],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await sdk.instances.query({
|
||||
with: {
|
||||
asset: {
|
||||
nodes: {
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
equals: {
|
||||
property: ['node', 'externalId'],
|
||||
value: instance.externalId,
|
||||
},
|
||||
},
|
||||
{
|
||||
equals: {
|
||||
property: ['node', 'space'],
|
||||
value: instance.space,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
object_3ds: {
|
||||
nodes: {
|
||||
from: 'asset',
|
||||
through: {
|
||||
view: { type: 'view', ...COGNITE_VISUALIZABLE_VIEW },
|
||||
identifier: 'object3D',
|
||||
},
|
||||
direction: 'outwards',
|
||||
},
|
||||
},
|
||||
cad_nodes: {
|
||||
nodes: {
|
||||
from: 'object_3ds',
|
||||
through: {
|
||||
view: { type: 'view', ...COGNITE_CAD_NODE_VIEW },
|
||||
identifier: 'object3D',
|
||||
},
|
||||
direction: 'inwards',
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
cad_nodes: {
|
||||
sources: [
|
||||
{
|
||||
source: { type: 'view', ...COGNITE_CAD_NODE_VIEW },
|
||||
properties: ['model3D', 'revisions'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const models: TaggedAddResourceOptions[] = [];
|
||||
const seenModels = new Set<string>();
|
||||
|
||||
// Extract model/revision info from CAD nodes
|
||||
const cadNodes = response.items?.cad_nodes || [];
|
||||
|
||||
for (const node of cadNodes) {
|
||||
const props = unwrapProperties<CogniteCADNodeProperties>(
|
||||
node as CDFNode,
|
||||
COGNITE_CAD_NODE_VIEW
|
||||
);
|
||||
if (!props?.model3D || !Array.isArray(props.revisions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const modelId = extractNumericId(props.model3D.externalId);
|
||||
if (!modelId) continue;
|
||||
|
||||
// Process each revision
|
||||
for (const revision of props.revisions) {
|
||||
const revisionId = extractNumericId(revision.externalId);
|
||||
if (!revisionId) continue;
|
||||
|
||||
const modelKey = `${modelId}:${revisionId}`;
|
||||
if (seenModels.has(modelKey)) continue;
|
||||
|
||||
seenModels.add(modelKey);
|
||||
models.push({ type: 'cad', addOptions: { modelId, revisionId } });
|
||||
}
|
||||
}
|
||||
|
||||
return models;
|
||||
} catch (error) {
|
||||
console.error('[useModelsForInstanceQuery] Error:', error);
|
||||
return [] as TaggedAddResourceOptions[];
|
||||
}
|
||||
},
|
||||
enabled: !!sdk,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all loaded 3D models in the viewer.
|
||||
* Models are only present after Reveal3DResources has loaded them.
|
||||
*
|
||||
* Note: This returns the viewer.models array directly without polling.
|
||||
* Components should be structured so that model loading triggers re-renders naturally.
|
||||
*/
|
||||
export function use3dModels() {
|
||||
const viewer = useReveal();
|
||||
return viewer.models || [];
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { DMInstanceRef } from '@cognite/reveal';
|
||||
import type { Node3D, CogniteClient } from '@cognite/sdk';
|
||||
import { useRevealContext } from './useRevealContext';
|
||||
import type { ThreeDModelFdmMappings } from '../types';
|
||||
import {
|
||||
ASSET_VIEW,
|
||||
COGNITE_3D_OBJECT_VIEW,
|
||||
COGNITE_CAD_NODE_VIEW,
|
||||
} from '../utils/views';
|
||||
import { unwrapProperties } from '../utils/data-mapper';
|
||||
import type { CDFNode } from '../utils/cdf-types';
|
||||
import { executeParallel, chunk } from '../utils/executeParallel';
|
||||
|
||||
const TREE_INDEX_CHUNK_SIZE = 1000;
|
||||
const NODE_ID_CHUNK_SIZE = 100;
|
||||
|
||||
interface ModelRef {
|
||||
modelId: number;
|
||||
revisionId: number;
|
||||
}
|
||||
|
||||
interface DmsUniqueIdentifier {
|
||||
space: string;
|
||||
externalId: string;
|
||||
}
|
||||
|
||||
interface CogniteAssetProperties {
|
||||
space: string;
|
||||
externalId: string;
|
||||
object3D: DmsUniqueIdentifier;
|
||||
}
|
||||
|
||||
interface CogniteCADNodeProperties {
|
||||
space: string;
|
||||
externalId: string;
|
||||
object3D: DmsUniqueIdentifier;
|
||||
model3D: DmsUniqueIdentifier;
|
||||
revisions: DmsUniqueIdentifier[];
|
||||
treeIndexes: number[];
|
||||
subTreeSizes: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch FDM asset mappings using model IDs from the query result,
|
||||
* NOT waiting for models to be loaded into the viewer.
|
||||
*
|
||||
* This eliminates 1-3 seconds from the critical path by running mapping fetch
|
||||
* in parallel with model loading instead of sequentially after.
|
||||
*/
|
||||
export function usePrefetchedFdmMappings(
|
||||
instances: DMInstanceRef[],
|
||||
modelRefs: ModelRef[]
|
||||
) {
|
||||
const { sdk } = useRevealContext();
|
||||
|
||||
const instancesKey = useMemo(
|
||||
() => instances.map((i) => `${i.space}:${i.externalId}`).join(','),
|
||||
[instances]
|
||||
);
|
||||
const modelRefsKey = useMemo(
|
||||
() => modelRefs.map((m) => `${m.modelId}:${m.revisionId}`).join(','),
|
||||
[modelRefs]
|
||||
);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['fdm-cad-connections-prefetched', instancesKey, modelRefsKey],
|
||||
queryFn: async (): Promise<ThreeDModelFdmMappings[]> => {
|
||||
if (instances.length === 0 || modelRefs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const queryResult = await sdk.instances.query({
|
||||
with: {
|
||||
assets: {
|
||||
nodes: {
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
in: {
|
||||
property: ['node', 'space'],
|
||||
values: [
|
||||
...new Set(instances.map((inst) => inst.space)),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
in: {
|
||||
property: ['node', 'externalId'],
|
||||
values: instances.map((inst) => inst.externalId),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
object_3ds: {
|
||||
nodes: {
|
||||
from: 'assets',
|
||||
through: {
|
||||
view: { type: 'view', ...ASSET_VIEW },
|
||||
identifier: 'object3D',
|
||||
},
|
||||
direction: 'outwards',
|
||||
filter: {
|
||||
hasData: [{ type: 'view', ...COGNITE_3D_OBJECT_VIEW }],
|
||||
},
|
||||
},
|
||||
},
|
||||
cad_nodes: {
|
||||
nodes: {
|
||||
from: 'object_3ds',
|
||||
through: {
|
||||
view: { type: 'view', ...COGNITE_CAD_NODE_VIEW },
|
||||
identifier: 'object3D',
|
||||
},
|
||||
direction: 'inwards',
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
assets: {
|
||||
sources: [
|
||||
{
|
||||
source: { type: 'view', ...ASSET_VIEW },
|
||||
properties: ['object3D'],
|
||||
},
|
||||
],
|
||||
},
|
||||
cad_nodes: {
|
||||
sources: [
|
||||
{
|
||||
source: { type: 'view', ...COGNITE_CAD_NODE_VIEW },
|
||||
properties: [
|
||||
'object3D',
|
||||
'model3D',
|
||||
'revisions',
|
||||
'treeIndexes',
|
||||
'subTreeSizes',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mappingsByModel = new Map<string, Map<string, Node3D[]>>();
|
||||
const cadNodes = queryResult.items.cad_nodes || [];
|
||||
|
||||
const object3DToAssets = new Map<string, DMInstanceRef[]>();
|
||||
for (const asset of queryResult.items.assets || []) {
|
||||
const props = unwrapProperties<CogniteAssetProperties>(
|
||||
asset as CDFNode,
|
||||
ASSET_VIEW
|
||||
);
|
||||
if (props?.object3D) {
|
||||
const key = `${props.object3D.space}/${props.object3D.externalId}`;
|
||||
const existing = object3DToAssets.get(key) || [];
|
||||
existing.push({ space: asset.space, externalId: asset.externalId });
|
||||
object3DToAssets.set(key, existing);
|
||||
}
|
||||
}
|
||||
|
||||
interface NodeRequest {
|
||||
modelId: number;
|
||||
revisionId: number;
|
||||
treeIndex: number;
|
||||
assetInstances: DMInstanceRef[];
|
||||
}
|
||||
|
||||
const nodeRequests: NodeRequest[] = [];
|
||||
|
||||
for (const cadNode of cadNodes) {
|
||||
const props = unwrapProperties<CogniteCADNodeProperties>(
|
||||
cadNode as CDFNode,
|
||||
COGNITE_CAD_NODE_VIEW
|
||||
);
|
||||
if (!props) continue;
|
||||
|
||||
const { model3D, revisions, treeIndexes, object3D } = props;
|
||||
if (!model3D || !revisions || !treeIndexes) continue;
|
||||
|
||||
const object3DKey = `${object3D.space}/${object3D.externalId}`;
|
||||
const relatedAssets = object3DToAssets.get(object3DKey);
|
||||
if (!relatedAssets) continue;
|
||||
|
||||
const modelId = extractModelId(model3D.externalId);
|
||||
|
||||
for (let i = 0; i < revisions.length; i++) {
|
||||
const revision = revisions[i];
|
||||
const treeIndex = treeIndexes[i];
|
||||
const revisionId = extractRevisionId(revision.externalId);
|
||||
|
||||
const matchingModel = modelRefs.find(
|
||||
(m) => m.modelId === modelId && m.revisionId === revisionId
|
||||
);
|
||||
if (!matchingModel) continue;
|
||||
|
||||
nodeRequests.push({
|
||||
modelId,
|
||||
revisionId,
|
||||
treeIndex,
|
||||
assetInstances: relatedAssets,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const nodesByRevision = new Map<string, NodeRequest[]>();
|
||||
for (const req of nodeRequests) {
|
||||
const key = `${req.modelId}/${req.revisionId}`;
|
||||
const existing = nodesByRevision.get(key) || [];
|
||||
existing.push(req);
|
||||
nodesByRevision.set(key, existing);
|
||||
}
|
||||
|
||||
const revisionFetchPromises = Array.from(nodesByRevision.entries()).map(
|
||||
async ([revisionKey, requests]) => {
|
||||
const [modelId, revisionId] = revisionKey.split('/').map(Number);
|
||||
const treeIndexes = requests.map((r) => r.treeIndex);
|
||||
|
||||
const nodes = await fetchNodesByTreeIndex(
|
||||
sdk,
|
||||
modelId,
|
||||
revisionId,
|
||||
treeIndexes
|
||||
);
|
||||
|
||||
return { revisionKey, nodes, requests };
|
||||
}
|
||||
);
|
||||
|
||||
const allRevisionData = await Promise.all(revisionFetchPromises);
|
||||
|
||||
for (const { revisionKey, nodes, requests } of allRevisionData) {
|
||||
const treeIndexToNode = new Map(
|
||||
nodes.map((node) => [node.treeIndex, node])
|
||||
);
|
||||
|
||||
const modelMappings =
|
||||
mappingsByModel.get(revisionKey) ?? new Map<string, Node3D[]>();
|
||||
mappingsByModel.set(revisionKey, modelMappings);
|
||||
|
||||
for (const req of requests) {
|
||||
const node3D = treeIndexToNode.get(req.treeIndex);
|
||||
if (!node3D) continue;
|
||||
|
||||
for (const instance of req.assetInstances) {
|
||||
const instanceKey = `${instance.space}:${instance.externalId}`;
|
||||
const arr = modelMappings.get(instanceKey) ?? [];
|
||||
arr.push(node3D);
|
||||
modelMappings.set(instanceKey, arr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const results: ThreeDModelFdmMappings[] = [];
|
||||
for (const modelRef of modelRefs) {
|
||||
const modelKey = `${modelRef.modelId}/${modelRef.revisionId}`;
|
||||
results.push({
|
||||
modelId: modelRef.modelId,
|
||||
revisionId: modelRef.revisionId,
|
||||
mappings: mappingsByModel.get(modelKey) ?? new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error prefetching FDM CAD connections:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
enabled: !!sdk && instances.length > 0 && modelRefs.length > 0,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
function extractModelId(externalId: string): number {
|
||||
const match = externalId.match(/model_(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : -1;
|
||||
}
|
||||
|
||||
function extractRevisionId(externalId: string): number {
|
||||
const match = externalId.match(/revision_(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : -1;
|
||||
}
|
||||
|
||||
async function fetchNodesByTreeIndex(
|
||||
sdk: CogniteClient,
|
||||
modelId: number,
|
||||
revisionId: number,
|
||||
treeIndexes: number[]
|
||||
): Promise<Node3D[]> {
|
||||
if (treeIndexes.length === 0) return [];
|
||||
|
||||
const uniqueTreeIndexes = Array.from(new Set(treeIndexes));
|
||||
const treeIndexChunks = chunk(uniqueTreeIndexes, TREE_INDEX_CHUNK_SIZE);
|
||||
|
||||
const results = await executeParallel(
|
||||
treeIndexChunks.map((indexChunk) => async () => {
|
||||
const nodeIdResponse = await sdk.post<{ items: number[] }>(
|
||||
`/api/v1/projects/${sdk.project}/3d/models/${modelId}/revisions/${revisionId}/nodes/internalids/bytreeindices`,
|
||||
{
|
||||
data: { items: indexChunk },
|
||||
}
|
||||
);
|
||||
|
||||
const nodeIds = nodeIdResponse.data.items;
|
||||
if (nodeIds.length === 0) return [];
|
||||
|
||||
const nodeIdChunks = chunk(nodeIds, NODE_ID_CHUNK_SIZE);
|
||||
const nodeResults = await executeParallel(
|
||||
nodeIdChunks.map((idChunk) => async () => {
|
||||
const nodes = await sdk.revisions3D.retrieve3DNodes(
|
||||
modelId,
|
||||
revisionId,
|
||||
idChunk.map((id) => ({ id }))
|
||||
);
|
||||
return Array.isArray(nodes) ? nodes : [];
|
||||
}),
|
||||
3
|
||||
);
|
||||
|
||||
return nodeResults.flat().filter((node): node is Node3D => node !== undefined);
|
||||
}),
|
||||
3
|
||||
);
|
||||
|
||||
return results.flat().filter((node): node is Node3D => node !== undefined);
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { DMInstanceRef } from '@cognite/reveal';
|
||||
import type { ViewDefinition, ViewReference } from '@cognite/sdk';
|
||||
import { useRevealContext } from './useRevealContext';
|
||||
import {
|
||||
COGNITE_VISUALIZABLE_VIEW,
|
||||
COGNITE_3D_OBJECT_VIEW,
|
||||
} from '../utils/views';
|
||||
|
||||
type DmsUniqueIdentifier = {
|
||||
space: string;
|
||||
externalId: string;
|
||||
};
|
||||
|
||||
export function use3dRelatedEdgeConnections(instance: DMInstanceRef) {
|
||||
const { sdk } = useRevealContext();
|
||||
return useQuery({
|
||||
queryKey: ['3d-related-edges', instance.space, instance.externalId],
|
||||
queryFn: async (): Promise<DMInstanceRef[]> => {
|
||||
try {
|
||||
// Query for nodes connected via edges that have 3D data
|
||||
const response = await sdk.instances.query({
|
||||
with: {
|
||||
start_instance: {
|
||||
nodes: {
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
equals: {
|
||||
property: ['node', 'externalId'],
|
||||
value: instance.externalId,
|
||||
},
|
||||
},
|
||||
{
|
||||
equals: {
|
||||
property: ['node', 'space'],
|
||||
value: instance.space,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
limit: 1,
|
||||
},
|
||||
start_to_object_edges: {
|
||||
edges: {
|
||||
from: 'start_instance',
|
||||
maxDistance: 1,
|
||||
direction: 'outwards',
|
||||
},
|
||||
limit: 1000,
|
||||
},
|
||||
objects_connected_with_3d: {
|
||||
nodes: {
|
||||
from: 'start_to_object_edges',
|
||||
chainTo: 'destination',
|
||||
filter: {
|
||||
exists: {
|
||||
property: [
|
||||
COGNITE_VISUALIZABLE_VIEW.space,
|
||||
COGNITE_VISUALIZABLE_VIEW.externalId,
|
||||
'object3D',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
object_3ds: {
|
||||
nodes: {
|
||||
from: 'objects_connected_with_3d',
|
||||
through: {
|
||||
view: { type: 'view', ...COGNITE_VISUALIZABLE_VIEW },
|
||||
identifier: 'object3D',
|
||||
},
|
||||
},
|
||||
limit: 1000,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
start_instance: {},
|
||||
start_to_object_edges: {},
|
||||
objects_connected_with_3d: {},
|
||||
object_3ds: {},
|
||||
},
|
||||
});
|
||||
|
||||
// Return the instances that are connected to 3D objects
|
||||
return (
|
||||
response.items?.objects_connected_with_3d?.map((node) => ({
|
||||
space: node.space,
|
||||
externalId: node.externalId,
|
||||
})) ?? []
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error fetching related edge connections:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
enabled: !!sdk,
|
||||
});
|
||||
}
|
||||
|
||||
export function use3dRelatedDirectConnections(instance: DMInstanceRef) {
|
||||
const { sdk } = useRevealContext();
|
||||
return useQuery({
|
||||
queryKey: ['3d-related-direct', instance.space, instance.externalId],
|
||||
queryFn: async (): Promise<DMInstanceRef[]> => {
|
||||
try {
|
||||
// Step 1: Inspect the instance to find its views
|
||||
const views = await sdk.instances.inspect({
|
||||
inspectionOperations: { involvedViews: {} },
|
||||
items: [
|
||||
{
|
||||
instanceType: 'node',
|
||||
externalId: instance.externalId,
|
||||
space: instance.space,
|
||||
},
|
||||
],
|
||||
});
|
||||
const view = views.items[0]?.inspectionResults?.involvedViews?.[0];
|
||||
|
||||
// Step 2: Get the instance content with its views
|
||||
const instanceResponse = await sdk.instances.retrieve({
|
||||
items: [
|
||||
{
|
||||
instanceType: 'node',
|
||||
externalId: instance.externalId,
|
||||
space: instance.space,
|
||||
},
|
||||
],
|
||||
sources: view ? [{ source: view }] : undefined,
|
||||
});
|
||||
|
||||
const instanceContent = instanceResponse.items[0];
|
||||
if (!instanceContent?.properties) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Step 3: Extract all direct relation properties
|
||||
const directlyRelatedObjects = Object.values(
|
||||
instanceContent.properties
|
||||
).flatMap((spaceScope) => {
|
||||
if (typeof spaceScope !== 'object' || !spaceScope) return [];
|
||||
return Object.values(spaceScope).flatMap((fieldValues) => {
|
||||
if (typeof fieldValues !== 'object' || !fieldValues) return [];
|
||||
return Object.values(fieldValues).filter(
|
||||
(value): value is DmsUniqueIdentifier =>
|
||||
typeof value === 'object' &&
|
||||
'externalId' in value &&
|
||||
'space' in value &&
|
||||
typeof value.externalId === 'string' &&
|
||||
typeof value.space === 'string'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (directlyRelatedObjects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Step 4: Inspect all related objects to get their views
|
||||
const relatedObjectInspectionsResult = await sdk.instances.inspect({
|
||||
inspectionOperations: { involvedViews: {} },
|
||||
items: directlyRelatedObjects.map((fdmId) => ({
|
||||
...fdmId,
|
||||
instanceType: 'node',
|
||||
})),
|
||||
});
|
||||
|
||||
const relatedObjectsViewLists =
|
||||
relatedObjectInspectionsResult.items.map(
|
||||
(item) => item.inspectionResults?.involvedViews ?? []
|
||||
);
|
||||
|
||||
// Step 5: Create a mapping of object index to views
|
||||
const relatedObjectViewsWithObjectIndex = relatedObjectsViewLists
|
||||
.map((viewList, idx) => viewList.map((view) => [idx, view] as const))
|
||||
.flat();
|
||||
|
||||
// Step 6: Deduplicate views and fetch their definitions
|
||||
const [deduplicatedViews, viewToDeduplicatedIndexMap] =
|
||||
createDeduplicatedViewToIndexMap(relatedObjectViewsWithObjectIndex);
|
||||
|
||||
const viewProps = await sdk.views.retrieve(
|
||||
deduplicatedViews.map((view) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { type, ...viewWithoutType } = view;
|
||||
return viewWithoutType;
|
||||
}),
|
||||
{ includeInheritedProperties: true }
|
||||
);
|
||||
|
||||
// Step 7: Filter to only 3D-related views
|
||||
const threeDRelatedViews = relatedObjectViewsWithObjectIndex.filter(
|
||||
([, view]) => {
|
||||
const viewResultIndex = viewToDeduplicatedIndexMap.get(
|
||||
createViewKey(view)
|
||||
);
|
||||
if (viewResultIndex === undefined) return false;
|
||||
|
||||
const propsForView = viewProps.items[viewResultIndex];
|
||||
return is3dView(propsForView);
|
||||
}
|
||||
);
|
||||
|
||||
// Step 8: Return the 3D-related instances
|
||||
return threeDRelatedViews.map(([idx]) => directlyRelatedObjects[idx]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching related direct connections:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
enabled: !!sdk,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
type ViewKey = `${string}/${string}/${string}`;
|
||||
|
||||
function createViewKey(source: ViewReference): ViewKey {
|
||||
return `${source.externalId}/${source.space}/${source.version}`;
|
||||
}
|
||||
|
||||
function createDeduplicatedViewToIndexMap(
|
||||
viewsWithObjectIndex: Array<readonly [number, ViewReference]>
|
||||
): [Array<ViewReference>, Map<ViewKey, number>] {
|
||||
const deduplicatedViews: Array<ViewReference> = [];
|
||||
const viewToDeduplicatedIndexMap = new Map<ViewKey, number>();
|
||||
viewsWithObjectIndex.forEach(([, view]) => {
|
||||
const viewKey = createViewKey(view);
|
||||
if (!viewToDeduplicatedIndexMap.has(viewKey)) {
|
||||
viewToDeduplicatedIndexMap.set(viewKey, deduplicatedViews.length);
|
||||
deduplicatedViews.push(view);
|
||||
}
|
||||
});
|
||||
return [deduplicatedViews, viewToDeduplicatedIndexMap];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a view is 3D-related by checking if it implements Cognite3DObject
|
||||
*/
|
||||
function is3dView(view: ViewDefinition): boolean {
|
||||
return (view.implements ?? []).some(
|
||||
(type) =>
|
||||
type.externalId === COGNITE_3D_OBJECT_VIEW.externalId &&
|
||||
type.space === COGNITE_3D_OBJECT_VIEW.space &&
|
||||
type.version === COGNITE_3D_OBJECT_VIEW.version
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { Cognite3DViewer } from '@cognite/reveal';
|
||||
|
||||
/**
|
||||
* Automatically removes models from the viewer that are no longer referenced
|
||||
* by the current component tree. Prevents memory accumulation when using
|
||||
* RevealKeepAlive for viewer persistence (50-70% memory reduction on navigation).
|
||||
*/
|
||||
export function useRemoveNonReferencedModels(
|
||||
viewer: Cognite3DViewer | null,
|
||||
activeModelKeys: Set<string>
|
||||
) {
|
||||
const prevKeysRef = useRef<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewer) return;
|
||||
|
||||
const removed = [...prevKeysRef.current].filter((k) => !activeModelKeys.has(k));
|
||||
|
||||
for (const key of removed) {
|
||||
const [modelIdStr, revisionIdStr] = key.split('-');
|
||||
const modelId = parseInt(modelIdStr, 10);
|
||||
const revisionId = parseInt(revisionIdStr, 10);
|
||||
|
||||
if (isNaN(modelId) || isNaN(revisionId)) continue;
|
||||
|
||||
const model = viewer.models.find(
|
||||
(m) => m.modelId === modelId && m.revisionId === revisionId
|
||||
);
|
||||
|
||||
if (model) {
|
||||
try {
|
||||
viewer.removeModel(model);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[useRemoveNonReferencedModels] Error removing model ${key}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevKeysRef.current = new Set(activeModelKeys);
|
||||
}, [viewer, activeModelKeys]);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
InstanceStylingContext,
|
||||
type InstanceStylingController,
|
||||
} from '../context/instanceStylingContext';
|
||||
|
||||
export interface RenderTarget {
|
||||
instanceStylingController: InstanceStylingController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access the render target which includes the instance styling controller.
|
||||
* This allows components to read and react to centralized styling state.
|
||||
*
|
||||
* The controller provides methods to:
|
||||
* - registerStylingGroup(group): Register a new styling group and get its ID
|
||||
* - unregisterStylingGroup(id): Remove a styling group by ID
|
||||
* - getStylingGroups(): Get all current styling groups
|
||||
* - addEventListener(callback): Subscribe to styling changes
|
||||
* - removeEventListener(callback): Unsubscribe from styling changes
|
||||
*/
|
||||
export function useRenderTarget(): RenderTarget {
|
||||
const stylingContext = useContext(InstanceStylingContext);
|
||||
if (!stylingContext) {
|
||||
throw new Error(
|
||||
'useRenderTarget must be used within an InstanceStylingProvider'
|
||||
);
|
||||
}
|
||||
return { instanceStylingController: stylingContext };
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Cognite3DViewer } from '@cognite/reveal';
|
||||
import { useRevealContext } from './useRevealContext';
|
||||
|
||||
/**
|
||||
* Hook to access the Reveal viewer instance.
|
||||
* Must be used within a RevealProvider.
|
||||
*/
|
||||
export function useReveal(): Cognite3DViewer {
|
||||
const { viewer } = useRevealContext();
|
||||
return viewer;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
RevealContext,
|
||||
type RevealContextValue,
|
||||
} from '../context/revealContext';
|
||||
|
||||
export function useRevealContext(): RevealContextValue {
|
||||
const context = useContext(RevealContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useRevealContext must be used within a RevealProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user