This commit is contained in:
Ole
2026-05-31 20:25:41 +00:00
commit 0a07ab8593
275 changed files with 52660 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
# Reveal 3D Code Bundle
This bundle is copied from the Reveal source tree.
Copy the contents of `code/reveal/` into an app-local feature folder, typically:
```text
src/features/reveal-3d/
```
Then import from the app-local folder:
```tsx
import {
CacheProvider,
Reveal3DResources,
RevealCanvas,
RevealKeepAlive,
RevealProvider,
} from '@/features/reveal-3d';
```
Do not import from `skills/reveal-3d/code/reveal` in the target app.
## Dependencies
The copied code expects these app dependencies:
- `@cognite/reveal`
- `@cognite/sdk`
- `@tanstack/react-query`
- `react`
- `react-dom`
- `three`
The Vite setup for Reveal also needs `process`, `util`, `assert`, and `ajv`.
Install `@types/three` as a dev dependency for TypeScript apps.
## Public Exports
The public API is exported from `index.ts`, including:
- Components: `RevealProvider`, `RevealCanvas`, `Reveal3DResources`, `RevealKeepAlive`
- Providers/hooks: `CacheProvider`, `useReveal`, `useModelsForInstanceQuery`, `useFdmAssetMappings`
- Types: `AddCadResourceOptions`, `TaggedAddResourceOptions`, `ViewerOptions`
@@ -0,0 +1,180 @@
import type { Node3D } from '@cognite/sdk';
import type { DMInstanceRef } from '@cognite/reveal';
import { chunk, executeParallel } from '../utils/executeParallel';
const ASSET_MAPPING_CHUNK_SIZE = 1000;
/**
* Multi-level asset mapping cache with split-chunk strategy.
*
* Three-way indexing:
* - By model+revision: Fast lookup for all mappings in a model
* - By asset instance (space:externalId): Fast lookup for all nodes belonging to an asset
* - By node ID: Fast lookup for individual node metadata
*/
export class AssetMappingCache {
private byModelCache = new Map<string, Map<string, Node3D[]>>();
private byAssetCache = new Map<string, Node3D[]>();
private byNodeCache = new Map<string, Node3D>();
async getOrFetch(
modelId: number,
revisionId: number,
assetInstances: DMInstanceRef[],
fetchFn: (
modelId: number,
revisionId: number,
instances: DMInstanceRef[]
) => Promise<Map<string, Node3D[]>>
): Promise<Map<string, Node3D[]>> {
const modelKey = this.createModelKey(modelId, revisionId);
const cachedModel = this.byModelCache.get(modelKey);
if (cachedModel) {
const { cached, uncached } = this.splitCachedAndMissing(
assetInstances,
cachedModel
);
if (uncached.length === 0) {
return cached;
}
const fetched = await this.fetchAndCache(
modelId,
revisionId,
uncached,
fetchFn
);
return this.mergeMappings(cached, fetched);
}
return this.fetchAndCache(modelId, revisionId, assetInstances, fetchFn);
}
getCachedAssetMapping(instance: DMInstanceRef): Node3D[] | undefined {
return this.byAssetCache.get(this.createAssetKey(instance));
}
getCachedNode(modelId: number, revisionId: number, treeIndex: number): Node3D | undefined {
const key = this.createNodeKey(modelId, revisionId, treeIndex);
return this.byNodeCache.get(key);
}
clear(): void {
this.byModelCache.clear();
this.byAssetCache.clear();
this.byNodeCache.clear();
}
clearModel(modelId: number, revisionId: number): void {
const modelKey = this.createModelKey(modelId, revisionId);
this.byModelCache.delete(modelKey);
}
private createAssetKey(instance: DMInstanceRef): string {
return `${instance.space}:${instance.externalId}`;
}
private createModelKey(modelId: number, revisionId: number): string {
return `${modelId}/${revisionId}`;
}
private createNodeKey(modelId: number, revisionId: number, treeIndex: number): string {
return `${modelId}/${revisionId}/${treeIndex}`;
}
private splitCachedAndMissing(
assetInstances: DMInstanceRef[],
cachedModel: Map<string, Node3D[]>
): {
cached: Map<string, Node3D[]>;
uncached: DMInstanceRef[];
} {
const cached = new Map<string, Node3D[]>();
const uncached: DMInstanceRef[] = [];
for (const instance of assetInstances) {
const assetKey = this.createAssetKey(instance);
const cachedNodes = cachedModel.get(assetKey);
if (cachedNodes) {
cached.set(assetKey, cachedNodes);
} else {
uncached.push(instance);
}
}
return { cached, uncached };
}
private async fetchAndCache(
modelId: number,
revisionId: number,
assetInstances: DMInstanceRef[],
fetchFn: (
modelId: number,
revisionId: number,
instances: DMInstanceRef[]
) => Promise<Map<string, Node3D[]>>
): Promise<Map<string, Node3D[]>> {
const chunks = chunk(assetInstances, ASSET_MAPPING_CHUNK_SIZE);
const results = await executeParallel(
chunks.map((chunkInstances) => async () => {
return fetchFn(modelId, revisionId, chunkInstances);
}),
3
);
const merged = new Map<string, Node3D[]>();
for (const result of results) {
if (result) {
for (const [assetKey, nodes] of result.entries()) {
merged.set(assetKey, nodes);
}
}
}
this.indexMappings(modelId, revisionId, merged);
return merged;
}
private indexMappings(
modelId: number,
revisionId: number,
mappings: Map<string, Node3D[]>
): void {
const modelKey = this.createModelKey(modelId, revisionId);
let modelCache = this.byModelCache.get(modelKey);
if (!modelCache) {
modelCache = new Map();
this.byModelCache.set(modelKey, modelCache);
}
for (const [assetKey, nodes] of mappings.entries()) {
modelCache.set(assetKey, nodes);
this.byAssetCache.set(assetKey, nodes);
for (const node of nodes) {
if (node.treeIndex !== undefined) {
const nodeKey = this.createNodeKey(modelId, revisionId, node.treeIndex);
this.byNodeCache.set(nodeKey, node);
}
}
}
}
private mergeMappings(
map1: Map<string, Node3D[]>,
map2: Map<string, Node3D[]>
): Map<string, Node3D[]> {
const merged = new Map(map1);
for (const [key, value] of map2.entries()) {
merged.set(key, value);
}
return merged;
}
}
@@ -0,0 +1,41 @@
import { createContext, useContext, useMemo } from 'react';
import { AssetMappingCache } from './AssetMappingCache';
interface CacheContextValue {
assetMappingCache: AssetMappingCache;
}
const CacheContext = createContext<CacheContextValue | null>(null);
export function useCacheContext() {
const context = useContext(CacheContext);
if (!context) {
throw new Error('useCacheContext must be used within CacheProvider');
}
return context;
}
export function useOptionalCacheContext() {
return useContext(CacheContext);
}
interface CacheProviderProps {
children: React.ReactNode;
}
/**
* Provides shared cache instances to the component tree.
* Wrap your app or 3D viewer area with this to enable cross-navigation caching
* (70-90% reduction in API calls on subsequent visits).
*/
export function CacheProvider({ children }: CacheProviderProps) {
const cacheValue = useMemo(() => ({
assetMappingCache: new AssetMappingCache(),
}), []);
return (
<CacheContext.Provider value={cacheValue}>
{children}
</CacheContext.Provider>
);
}
@@ -0,0 +1,213 @@
import { useEffect, useRef } from 'react';
import {
type CogniteCadModel,
TreeIndexNodeCollection,
NumericRange,
} from '@cognite/reveal';
import { useReveal } from '../hooks/useReveal';
import type { AddCadResourceOptions, InstanceStylingGroup } from '../types';
import { useFdmAssetMappings } from '../hooks/useFdmMappings';
interface Reveal3DResourcesProps {
resources: AddCadResourceOptions[];
instanceStyling?: InstanceStylingGroup[];
onModelsLoaded?: () => void;
}
export function Reveal3DResources({
resources,
instanceStyling = [],
onModelsLoaded,
}: Reveal3DResourcesProps) {
const viewer = useReveal();
const loadedModelsRef = useRef<CogniteCadModel[]>([]);
useEffect(() => {
if (!viewer || resources.length === 0) {
return;
}
const cancelledRef = { current: false };
const loadModels = async () => {
const modelPromises = resources.map(async (resource) => {
if (cancelledRef.current) return null;
try {
const existing = viewer.models.find(
(m) =>
m.modelId === resource.modelId &&
m.revisionId === resource.revisionId
);
if (existing) {
return existing as CogniteCadModel;
}
const addModelOptions: {
modelId: number;
revisionId: number;
geometryFilter?: typeof resource.geometryFilter;
} = {
modelId: resource.modelId,
revisionId: resource.revisionId,
};
if (resource.geometryFilter) {
addModelOptions.geometryFilter = resource.geometryFilter;
}
const model = await viewer.addCadModel(addModelOptions);
if (cancelledRef.current) {
viewer.removeModel(model);
return null;
}
if (resource.styling?.default) {
const { renderGhosted, renderInFront } = resource.styling.default;
if (renderGhosted !== undefined) {
model.setDefaultNodeAppearance({
renderGhosted,
renderInFront: renderInFront ?? false,
});
}
}
return model;
} catch (error) {
console.error('Error loading CAD model:', error);
return null;
}
});
const loadedModels = (await Promise.all(modelPromises)).filter(
(model): model is CogniteCadModel => model !== null
);
if (!cancelledRef.current) {
loadedModelsRef.current = loadedModels;
if (loadedModels.length > 0) {
viewer.fitCameraToModels(loadedModels, 0);
}
if (onModelsLoaded) {
onModelsLoaded();
}
}
};
loadModels();
return () => {
cancelledRef.current = true;
const modelsToRemove = loadedModelsRef.current;
for (const model of modelsToRemove) {
try {
viewer.removeModel(model);
} catch (error) {
console.error('Error removing model:', error);
}
}
loadedModelsRef.current = [];
};
}, [viewer, resources, onModelsLoaded]);
useApplyInstanceStyling(viewer, loadedModelsRef.current, instanceStyling);
return null;
}
function useApplyInstanceStyling(
viewer: ReturnType<typeof useReveal>,
loadedModels: CogniteCadModel[],
instanceStyling: InstanceStylingGroup[]
) {
const appliedCollectionsRef = useRef<
Map<CogniteCadModel, TreeIndexNodeCollection[]>
>(new Map());
const fdmInstances =
instanceStyling.flatMap((group) => group.fdmAssetExternalIds || []) || [];
const modelOptions = loadedModels.map((model) => ({
type: 'cad' as const,
modelId: model.modelId,
revisionId: model.revisionId,
}));
const { data: assetMappings } = useFdmAssetMappings(
fdmInstances,
modelOptions
);
useEffect(() => {
if (!viewer || loadedModels.length === 0) return;
const hasFdmInstances = instanceStyling.some(
(g) => g.fdmAssetExternalIds && g.fdmAssetExternalIds.length > 0
);
if (!hasFdmInstances) {
for (const [model, collections] of appliedCollectionsRef.current.entries()) {
for (const collection of collections) {
try { model.unassignStyledNodeCollection(collection); } catch { /* already removed */ }
}
}
appliedCollectionsRef.current.clear();
return;
}
if (!assetMappings) return;
for (const [model, collections] of appliedCollectionsRef.current.entries()) {
for (const collection of collections) {
try { model.unassignStyledNodeCollection(collection); } catch { /* already removed */ }
}
}
appliedCollectionsRef.current.clear();
for (const stylingGroup of instanceStyling) {
if (!stylingGroup.fdmAssetExternalIds || !stylingGroup.style.cad) {
continue;
}
const appearance = stylingGroup.style.cad;
for (const model of loadedModels) {
const modelMapping = assetMappings.find(
(m) =>
m.modelId === model.modelId && m.revisionId === model.revisionId
);
if (!modelMapping) continue;
const nodeCollection = new TreeIndexNodeCollection();
const indexSet = nodeCollection.getIndexSet();
for (const instance of stylingGroup.fdmAssetExternalIds) {
const nodes = modelMapping.mappings.get(`${instance.space}:${instance.externalId}`);
if (!nodes) continue;
for (const node of nodes) {
if (
node.treeIndex !== undefined &&
node.subtreeSize !== undefined
) {
const range = new NumericRange(node.treeIndex, node.subtreeSize);
indexSet.addRange(range);
}
}
}
if (indexSet.count > 0) {
nodeCollection.updateSet(indexSet);
model.assignStyledNodeCollection(nodeCollection, appearance);
const existing = appliedCollectionsRef.current.get(model) ?? [];
existing.push(nodeCollection);
appliedCollectionsRef.current.set(model, existing);
}
}
}
}, [viewer, loadedModels, instanceStyling, assetMappings]);
}
@@ -0,0 +1,27 @@
import { type ReactNode, type ReactElement, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useRevealContext } from '../hooks/useRevealContext';
export function RevealCanvas({
children,
}: {
children?: ReactNode;
}): ReactElement {
const { viewer } = useRevealContext();
const parentElement = useRef<HTMLDivElement>(null);
useEffect(() => {
if (parentElement.current !== null) {
parentElement.current.appendChild(viewer.domElement);
}
}, [viewer]);
return (
<div
style={{ width: '100%', height: '100%', overflow: 'hidden' }}
ref={parentElement}
>
{createPortal(children, viewer.domElement)}
</div>
);
}
@@ -0,0 +1,84 @@
import { useRef, useEffect, createContext, useContext, useCallback } from 'react';
import type { Cognite3DViewer } from '@cognite/reveal';
import type { CogniteClient } from '@cognite/sdk';
interface RevealKeepAliveContextValue {
getOrCreateViewer: (
sdk: CogniteClient,
createViewer: () => Cognite3DViewer
) => Cognite3DViewer;
isMounted: () => boolean;
}
const RevealKeepAliveContext = createContext<RevealKeepAliveContextValue | null>(
null
);
export function useRevealKeepAlive() {
const context = useContext(RevealKeepAliveContext);
if (!context) {
throw new Error('useRevealKeepAlive must be used within RevealKeepAlive');
}
return context;
}
/**
* Returns null when not inside a RevealKeepAlive provider.
* Used by RevealProvider to conditionally reuse a kept-alive viewer.
*/
export function useOptionalRevealKeepAlive(): RevealKeepAliveContextValue | null {
return useContext(RevealKeepAliveContext);
}
interface RevealKeepAliveProps {
children: React.ReactNode;
}
/**
* Keeps the Cognite3DViewer instance alive across component unmounts,
* eliminating viewer reinitialization when navigating between assets (~2-3s saving).
*
* The viewer persists in a ref that survives child unmount/remount cycles.
* Models are managed separately (added/removed as needed).
* Disposal is deferred to survive React StrictMode's mount→unmount→remount cycle.
*/
export function RevealKeepAlive({ children }: RevealKeepAliveProps) {
const viewerRef = useRef<Cognite3DViewer | null>(null);
const mountedRef = useRef(false);
const getOrCreateViewer = useCallback(
(_sdk: CogniteClient, createViewer: () => Cognite3DViewer) => {
if (!viewerRef.current) {
viewerRef.current = createViewer();
}
return viewerRef.current;
},
[]
);
const isMounted = useCallback(() => {
return mountedRef.current;
}, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
useEffect(() => {
return () => {
if (viewerRef.current) {
viewerRef.current.dispose();
viewerRef.current = null;
}
};
}, []);
return (
<RevealKeepAliveContext.Provider value={{ getOrCreateViewer, isMounted }}>
{children}
</RevealKeepAliveContext.Provider>
);
}
@@ -0,0 +1,83 @@
import { useRef, useCallback, type ReactNode, useMemo } from 'react';
import type { InstanceStylingGroup } from '../types';
import {
InstanceStylingContext,
type InstanceStylingController,
} from './instanceStylingContext';
interface InstanceStylingProviderProps {
children: ReactNode;
}
/**
* Provider for centralized instance styling management.
* Components can register/unregister styling groups and listen for changes.
*/
export function InstanceStylingProvider({
children,
}: InstanceStylingProviderProps) {
const stylingGroupsRef = useRef<Map<string, InstanceStylingGroup>>(new Map());
const listenersRef = useRef<Set<() => void>>(new Set());
const nextIdRef = useRef(0);
const getStylingGroups = useCallback(() => {
return Array.from(stylingGroupsRef.current.values());
}, []);
const addEventListener = useCallback((callback: () => void): void => {
listenersRef.current.add(callback);
// Call once immediately to initialize with current state
callback();
}, []);
const removeEventListener = useCallback((callback: () => void): void => {
listenersRef.current.delete(callback);
}, []);
const notifyListeners = useCallback(() => {
listenersRef.current.forEach((listener) => listener());
}, []);
const registerStylingGroup = useCallback(
(group: InstanceStylingGroup): string => {
const id = `styling-group-${nextIdRef.current++}`;
stylingGroupsRef.current.set(id, group);
notifyListeners();
return id;
},
[notifyListeners]
);
const unregisterStylingGroup = useCallback(
(id: string): void => {
const deleted = stylingGroupsRef.current.delete(id);
if (deleted) {
notifyListeners();
}
},
[notifyListeners]
);
const controller: InstanceStylingController = useMemo(
() => ({
getStylingGroups,
addEventListener,
removeEventListener,
registerStylingGroup,
unregisterStylingGroup,
}),
[
getStylingGroups,
addEventListener,
removeEventListener,
registerStylingGroup,
unregisterStylingGroup,
]
);
return (
<InstanceStylingContext.Provider value={controller}>
{children}
</InstanceStylingContext.Provider>
);
}
@@ -0,0 +1,63 @@
import { useEffect, useRef, useState } from 'react';
import { Cognite3DViewer } from '@cognite/reveal';
import type { RevealContextProps } from '../types';
import { RevealContext } from './revealContext';
import { InstanceStylingProvider } from './InstanceStylingProvider';
import { useOptionalRevealKeepAlive } from '../components/RevealKeepAlive';
import { RevealSettingsController } from '../settings/RevealSettingsController';
export function RevealProvider({
children,
sdk,
color,
viewerOptions,
}: RevealContextProps) {
const keepAlive = useOptionalRevealKeepAlive();
const keepAliveRef = useRef(keepAlive);
keepAliveRef.current = keepAlive;
const [viewerData] = useState(() => {
const createViewer = () =>
new Cognite3DViewer({
sdk,
useFlexibleCameraManager: true,
...viewerOptions,
});
const viewer = keepAlive
? keepAlive.getOrCreateViewer(sdk, createViewer)
: createViewer();
if (color) {
viewer.setBackgroundColor({ color, alpha: 1 });
}
return { viewer, sdk };
});
useEffect(() => {
const controller = new RevealSettingsController('medium');
controller.applyToViewer(viewerData.viewer);
return () => controller.dispose();
}, [viewerData.viewer]);
useEffect(() => {
if (color) {
viewerData.viewer.setBackgroundColor({ color, alpha: 1 });
}
}, [color, viewerData.viewer]);
useEffect(() => {
return () => {
if (!keepAliveRef.current) {
viewerData.viewer.dispose();
}
};
}, []);
return (
<RevealContext.Provider value={viewerData}>
<InstanceStylingProvider>{children}</InstanceStylingProvider>
</RevealContext.Provider>
);
}
@@ -0,0 +1,14 @@
import { createContext } from 'react';
import type { InstanceStylingGroup } from '../types';
export interface InstanceStylingController {
getStylingGroups: () => InstanceStylingGroup[];
addEventListener: (callback: () => void) => void;
removeEventListener: (callback: () => void) => void;
registerStylingGroup: (group: InstanceStylingGroup) => string;
unregisterStylingGroup: (id: string) => void;
}
export const InstanceStylingContext = createContext<
InstanceStylingController | undefined
>(undefined);
@@ -0,0 +1,12 @@
import { createContext } from 'react';
import type { Cognite3DViewer } from '@cognite/reveal';
import type { CogniteClient } from '@cognite/sdk';
export interface RevealContextValue {
viewer: Cognite3DViewer;
sdk: CogniteClient;
}
export const RevealContext = createContext<RevealContextValue | undefined>(
undefined
);
@@ -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;
}
@@ -0,0 +1,54 @@
// Context
export { RevealProvider } from './context/RevealProvider';
export { InstanceStylingProvider } from './context/InstanceStylingProvider';
export { useRevealContext } from './hooks/useRevealContext';
export type { InstanceStylingController } from './context/instanceStylingContext';
export { CacheProvider, useCacheContext, useOptionalCacheContext } from './cache/CacheProvider';
// Hooks
export { useReveal } from './hooks/useReveal';
export { useModelsForInstanceQuery, use3dModels } from './hooks/useModels';
export { useFdmAssetMappings } from './hooks/useFdmMappings';
export { usePrefetchedFdmMappings } from './hooks/usePrefetchedFdmMappings';
export {
use3dRelatedEdgeConnections,
use3dRelatedDirectConnections,
} from './hooks/useRelatedInstances';
export { useRenderTarget, type RenderTarget } from './hooks/useRenderTarget';
export {
use3dDataForSelectedInstance,
useInstancesWithBounds,
} from './hooks/use3dDataForSelectedInstance';
export { useFindRelated3dInstances } from './hooks/useFindRelated3dInstances';
export { useFocusCamera } from './hooks/useFocusCamera';
export { useInstanceStyling } from './hooks/useInstanceStyling';
export {
useInstancesWithBoundingBoxes,
getNodesFromModelsFdmMappings,
type InstanceWithBoundingBox,
type InstancesWithBoxesAndOriginalInstance,
type NodesWithModelInfo,
} from './hooks/useInstancesWithBoundingBoxes';
export { useRemoveNonReferencedModels } from './hooks/useRemoveNonReferencedModels';
// Components
export { RevealCanvas } from './components/RevealCanvas';
export { Reveal3DResources } from './components/Reveal3DResources';
export {
RevealKeepAlive,
useRevealKeepAlive,
useOptionalRevealKeepAlive,
} from './components/RevealKeepAlive';
// Types
export type {
ViewerOptions,
AddCadResourceOptions,
CadModelOptions,
TaggedAddResourceOptions,
ThreeDModelFdmMappings,
FdmAssetStylingGroup,
InstanceStylingGroup,
RevealContextProps,
CogniteModel,
} from './types';
@@ -0,0 +1,55 @@
export interface QualitySettings {
cadBudget: {
maximumRenderCost: number;
highDetailProximityThreshold: number;
};
pointCloudBudget: {
numberOfPoints: number;
};
resolutionOptions: {
maxRenderResolution: number;
movingCameraResolutionFactor: number;
};
}
export const QUALITY_PRESETS: Record<'low' | 'medium' | 'high', QualitySettings> = {
low: {
cadBudget: {
maximumRenderCost: 5_000_000,
highDetailProximityThreshold: 0,
},
pointCloudBudget: {
numberOfPoints: 1_000_000,
},
resolutionOptions: {
maxRenderResolution: 0.7e6,
movingCameraResolutionFactor: 0.3,
},
},
medium: {
cadBudget: {
maximumRenderCost: 15_000_000,
highDetailProximityThreshold: 0,
},
pointCloudBudget: {
numberOfPoints: 3_000_000,
},
resolutionOptions: {
maxRenderResolution: 1.4e6,
movingCameraResolutionFactor: 0.5,
},
},
high: {
cadBudget: {
maximumRenderCost: 45_000_000,
highDetailProximityThreshold: 10,
},
pointCloudBudget: {
numberOfPoints: 12_000_000,
},
resolutionOptions: {
maxRenderResolution: Infinity,
movingCameraResolutionFactor: 1.0,
},
},
};
@@ -0,0 +1,125 @@
import type { Cognite3DViewer } from '@cognite/reveal';
import { QUALITY_PRESETS, type QualitySettings } from './QualityPresets';
const CAMERA_IDLE_DEBOUNCE_MS = 200;
/**
* Centralized quality settings controller for the Reveal viewer.
*
* Manages CAD/point-cloud budgets, resolution caps, and dynamic resolution
* scaling during camera movement. All API calls are wrapped in try/catch
* so the controller never crashes viewer initialization if a method is
* unavailable on a particular Reveal version.
*/
export class RevealSettingsController {
private settings: QualitySettings;
private viewer: Cognite3DViewer | null = null;
private moveTimeout: ReturnType<typeof setTimeout> | null = null;
private cameraChangeHandler: (() => void) | null = null;
constructor(quality: 'low' | 'medium' | 'high' = 'medium') {
this.settings = QUALITY_PRESETS[quality];
}
applyToViewer(viewer: Cognite3DViewer): void {
this.removeEventListeners();
this.viewer = viewer;
try {
if ('cadBudget' in viewer) {
viewer.cadBudget = this.settings.cadBudget;
}
} catch (e) {
console.warn('[RevealSettingsController] Failed to set cadBudget:', e);
}
try {
if ('pointCloudBudget' in viewer) {
viewer.pointCloudBudget = this.settings.pointCloudBudget;
}
} catch (e) {
console.warn('[RevealSettingsController] Failed to set pointCloudBudget:', e);
}
try {
if (typeof viewer.setResolutionOptions === 'function') {
viewer.setResolutionOptions({
maxRenderResolution: this.settings.resolutionOptions.maxRenderResolution,
});
}
} catch (e) {
console.warn('[RevealSettingsController] Failed to set resolution:', e);
}
this.setupDynamicResolution(viewer);
}
setQuality(quality: 'low' | 'medium' | 'high'): void {
this.settings = QUALITY_PRESETS[quality];
if (this.viewer) {
this.applyToViewer(this.viewer);
}
}
getSettings(): QualitySettings {
return { ...this.settings };
}
dispose(): void {
if (this.moveTimeout) {
clearTimeout(this.moveTimeout);
this.moveTimeout = null;
}
this.removeEventListeners();
}
private removeEventListeners(): void {
if (this.viewer && this.cameraChangeHandler) {
try {
this.viewer.off('cameraChange', this.cameraChangeHandler);
} catch {
// Ignore if viewer is already disposed
}
this.cameraChangeHandler = null;
}
}
private setupDynamicResolution(viewer: Cognite3DViewer): void {
if (typeof viewer.setResolutionOptions !== 'function') {
return;
}
this.cameraChangeHandler = () => {
try {
viewer.setResolutionOptions({
maxRenderResolution:
this.settings.resolutionOptions.maxRenderResolution *
this.settings.resolutionOptions.movingCameraResolutionFactor,
});
if (this.moveTimeout) {
clearTimeout(this.moveTimeout);
}
this.moveTimeout = setTimeout(() => {
try {
viewer.setResolutionOptions({
maxRenderResolution: this.settings.resolutionOptions.maxRenderResolution,
});
} catch {
// Viewer may have been disposed
}
}, CAMERA_IDLE_DEBOUNCE_MS);
} catch {
// Viewer may have been disposed
}
};
try {
viewer.on('cameraChange', this.cameraChangeHandler);
} catch (e) {
console.warn('[RevealSettingsController] Failed to add cameraChange listener:', e);
this.cameraChangeHandler = null;
}
}
}
@@ -0,0 +1,90 @@
import type { CogniteClient, Node3D } from '@cognite/sdk';
import type {
CogniteCadModel,
CognitePointCloudModel,
DMInstanceRef,
NodeAppearance,
Cognite3DViewerOptions,
} from '@cognite/reveal';
import type * as THREE from 'three';
// Viewer Options - subset of Cognite3DViewerOptions
export type ViewerOptions = Pick<
Cognite3DViewerOptions,
'loadingIndicatorStyle' | 'antiAliasingHint' | 'ssaoQualityHint'
>;
// Geometry filter for partial model loading (matches Reveal SDK's GeometryFilter)
export interface RevealGeometryFilter {
boundingBox: THREE.Box3;
isBoundingBoxInModelCoordinates?: boolean;
}
// CAD Model Options
export interface AddCadResourceOptions {
modelId: number;
revisionId: number;
styling?: {
default?: {
renderGhosted?: boolean;
renderInFront?: boolean;
};
};
geometryFilter?: RevealGeometryFilter;
}
export interface CadModelOptions {
type: 'cad';
modelId: number;
revisionId: number;
}
// Tagged Resource Options
export type TaggedAddResourceOptions =
| {
type: 'cad';
addOptions: AddCadResourceOptions;
}
| {
type: 'pointcloud';
addOptions: {
modelId: number;
revisionId: number;
};
};
// FDM Mappings
export interface ThreeDModelFdmMappings {
modelId: number;
revisionId: number;
mappings: Map<string, Node3D[]>; // instanceKey (space:externalId) -> array of 3D nodes
}
// Styling
export interface FdmAssetStylingGroup {
fdmAssetExternalIds: DMInstanceRef[];
style: {
cad?: NodeAppearance;
pointcloud?: NodeAppearance;
};
}
export interface InstanceStylingGroup {
fdmAssetExternalIds?: DMInstanceRef[];
style: {
cad?: NodeAppearance;
pointcloud?: NodeAppearance;
};
}
// Reveal Context Props
export interface RevealContextProps {
children: React.ReactNode;
sdk: CogniteClient;
color?: THREE.Color;
viewerOptions?: ViewerOptions;
useCoreDm?: boolean;
}
// Model with type info
export type CogniteModel = CogniteCadModel | CognitePointCloudModel;
@@ -0,0 +1,24 @@
/**
* Core CDF Data Modeling Types
* These are the base types used across the application for working with CDF data models.
* They represent the raw structure of data returned from CDF APIs.
*/
/**
* Reference to a view in the data model
*/
export type ViewId = {
space: string;
externalId: string;
version: string;
};
/**
* Base interface for CDF nodes representing the raw structure from CDF APIs.
* CDF nests properties as: properties[space][view/version]
*/
export interface CDFNode {
space: string;
externalId: string;
properties: Record<string, unknown>;
}
@@ -0,0 +1,54 @@
/**
* Data Mapper Utilities
* Unwraps CDF's nested property structure to flat application types
*/
import type { CDFNode, ViewId } from './cdf-types';
/**
* Unwraps properties from a CDF node for a specific view.
* CDF nests properties as: properties[space][view/version]
* This function flattens them to: { space, externalId, ...properties }
*
* @example
* const batch = unwrapProperties<Batch>(cdfBatch, BATCH_VIEW);
* const material = unwrapProperties<Material>(cdfMaterial, MATERIAL_VIEW);
*/
export function unwrapProperties<
T extends { space: string; externalId: string },
>(node: CDFNode, view: ViewId): T {
const props: Record<string, unknown> = {};
if (node.properties) {
const spaceProps = node.properties[view.space] as
| Record<string, unknown>
| undefined;
if (spaceProps) {
const viewKey = `${view.externalId}/${view.version}`;
const viewProps = spaceProps[viewKey] as
| Record<string, unknown>
| undefined;
if (viewProps) {
Object.assign(props, viewProps);
}
}
}
return {
space: node.space,
externalId: node.externalId,
...props,
} as T;
}
/**
* Unwraps an array of CDF nodes
*
* @example
* const batches = unwrapPropertiesArray<Batch>(cdfBatches, BATCH_VIEW);
*/
export function unwrapPropertiesArray<
T extends { space: string; externalId: string },
>(nodes: CDFNode[], view: ViewId): T[] {
return nodes.map((node) => unwrapProperties<T>(node, view));
}
@@ -0,0 +1,42 @@
/**
* Execute async callbacks in parallel with a controlled concurrency limit.
* Prevents overwhelming the API while maximizing throughput.
*/
export async function executeParallel<T>(
callbacks: Array<() => Promise<T>>,
maxParallel: number
): Promise<Array<T | undefined>> {
const results: Array<T | undefined> = new Array(callbacks.length);
let nextIndex = 0;
async function runNext(): Promise<void> {
const currentIndex = nextIndex++;
if (currentIndex >= callbacks.length) return;
try {
results[currentIndex] = await callbacks[currentIndex]();
} catch (error) {
console.error(`executeParallel: callback ${currentIndex} failed:`, error);
results[currentIndex] = undefined;
}
await runNext();
}
await Promise.all(
Array.from({ length: Math.min(maxParallel, callbacks.length) }, () => runNext())
);
return results;
}
/**
* Split an array into smaller arrays of the given size.
*/
export function chunk<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
@@ -0,0 +1,70 @@
/**
* Core Data Model view constants
* These reference the standard Cognite Core Data Model views
*/
import { ViewId } from './cdf-types';
/**
* Core Data Model space constant
*/
export const CORE_DM_SPACE = 'cdf_cdm';
/**
* Core Data Model version
*/
export const CORE_DM_VERSION = 'v1';
/**
* View reference for CogniteAsset entities (from Core Data Model)
*/
export const ASSET_VIEW: ViewId = {
space: CORE_DM_SPACE,
externalId: 'CogniteAsset',
version: CORE_DM_VERSION,
};
/**
* View reference for Cognite3DObject entities (from Core Data Model)
*/
export const COGNITE_3D_OBJECT_VIEW: ViewId = {
space: CORE_DM_SPACE,
externalId: 'Cognite3DObject',
version: CORE_DM_VERSION,
};
/**
* View reference for CogniteCADNode entities (from Core Data Model)
*/
export const COGNITE_CAD_NODE_VIEW: ViewId = {
space: CORE_DM_SPACE,
externalId: 'CogniteCADNode',
version: CORE_DM_VERSION,
};
/**
* View reference for CogniteVisualizable entities (from Core Data Model)
*/
export const COGNITE_VISUALIZABLE_VIEW: ViewId = {
space: CORE_DM_SPACE,
externalId: 'CogniteVisualizable',
version: CORE_DM_VERSION,
};
/**
* View reference for Cognite3DModel entities (from Core Data Model)
*/
export const COGNITE_3D_MODEL_VIEW: ViewId = {
space: CORE_DM_SPACE,
externalId: 'Cognite3DModel',
version: CORE_DM_VERSION,
};
/**
* View reference for CogniteCADRevision entities (from Core Data Model)
*/
export const COGNITE_CAD_REVISION_VIEW: ViewId = {
space: CORE_DM_SPACE,
externalId: 'CogniteCADRevision',
version: CORE_DM_VERSION,
};