
Reveal 3d
- 1.9k installs
- 5 repo stars
- Updated August 4, 2026
- cognitedata/builder-skills
reveal-3d is an agent skill that Integrates a local Cognite Reveal 3D CAD viewer bundle into Flows apps by copying app-local source code. Use when adding.
About
Add a Cognite Reveal 3D viewer to a Flows app by copying the bundled source into the target app Renders CAD models from CDF with support for model browsing direct model revision IDs or FDM linked assets FDM instance to visualize ARGUMENTS The user wants to embed an interactive Cognite Reveal viewer for CDF 3D CAD content in a Flows app Do not use this skill for static diagrams graph visualizations or unrelated custom Three js scenes The app uses React TypeScript and is wrapped in cognite dune auth Flows auth The app has a QueryClientProvider from tanstack react query The CDF project has 3D models or the user has supplied direct model revision IDs For FDM linked 3D the instance must be linked through Core DM CogniteVisualizable object3D CogniteCADNode
- description: "Integrates a local Cognite Reveal 3D CAD viewer bundle into Flows apps by copying app-local source code. U
- argument-hint: "[FDM instance variable name or description, e.g. 'asset' or 'selectedEquipment']"
- Add a Cognite Reveal 3D viewer to a Flows app by copying the bundled source into the target app. Renders CAD models from
- Follow reveal-3d SKILL.md steps and documented constraints.
- Follow reveal-3d SKILL.md steps and documented constraints.
Reveal 3d by the numbers
- 1,888 all-time installs (skills.sh)
- +2 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #675 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
reveal-3d capabilities & compatibility
- Capabilities
- description: "integrates a local cognite reveal · argument hint: "[fdm instance variable name or d · add a cognite reveal 3d viewer to a flows app by · follow reveal 3d skill.md steps and documented c
- Use cases
- orchestration
What reveal-3d says it does
description: "Integrates a local Cognite Reveal 3D CAD viewer bundle into Flows apps by copying app-local source code. Use when adding 3D viewer, 3D visualization, Reveal, CAD model, RevealProvider, R
argument-hint: "[FDM instance variable name or description, e.g. 'asset' or 'selectedEquipment']"
Add a Cognite Reveal 3D viewer to a Flows app by copying the bundled source into the target app. Renders CAD models from CDF, with support for model browsing, direct model/revision IDs, or FDM-linked
npx skills add https://github.com/cognitedata/builder-skills --skill reveal-3dAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.9k |
|---|---|
| repo stars | ★ 5 |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | cognitedata/builder-skills ↗ |
When should an agent use reveal-3d and what problem does it solve?
Integrates a local Cognite Reveal 3D CAD viewer bundle into Flows apps by copying app-local source code. Use when adding 3D viewer, 3D visualization, Reveal, CAD model, RevealProvider, RevealCanvas, R
Who is it for?
Developers invoking reveal-3d as documented in the skill source.
Skip if: Skip when requirements fall outside reveal-3d documented scope.
When should I use this skill?
Integrates a local Cognite Reveal 3D CAD viewer bundle into Flows apps by copying app-local source code. Use when adding 3D viewer, 3D visualization, Reveal, CAD model, RevealProvider, RevealCanvas, R
What you get
Outputs aligned with the reveal-3d SKILL.md workflow and stated deliverables.
- src/features/reveal-3d/ component bundle
- configured 3D viewer
By the numbers
- Exports 5 React components: CacheProvider, Reveal3DResources, RevealCanvas, RevealKeepAlive, RevealProvider
- Requires 6 core app dependencies including @cognite/reveal and three
Files
Reveal 3D Viewer
Add a Cognite Reveal 3D viewer to a Flows app by copying the bundled source into the target app. Renders CAD models from CDF, with support for model browsing, direct model/revision IDs, or FDM-linked assets.
FDM instance to visualize: $ARGUMENTS
Use This When
The user wants to embed an interactive Cognite Reveal viewer for CDF 3D/CAD content in a Flows app.
Do not use this skill for static diagrams, graph visualizations, or unrelated custom Three.js scenes.
Prerequisites
- The app uses React + TypeScript and is wrapped in
@cognite/duneauth (Flows auth). - The app has a
QueryClientProviderfrom@tanstack/react-query. - The CDF project has 3D models, or the user has supplied direct model/revision IDs.
- For FDM-linked 3D, the instance must be linked through Core DM (
CogniteVisualizable.object3D->CogniteCADNode).
Integration Workflow
Follow these steps in order. Adapt paths to the target app's conventions instead of inventing new ones.
1. Inspect the target app. Read package.json, vite.config.ts, src/main.tsx, and the app's folder/alias conventions. 2. Install missing dependencies with the app's package manager. See Dependencies. Reuse existing pinned React, Flows, SDK, and React Query versions. 3. Copy the bundle into the app. Copy every file from skills/reveal-3d/code/reveal/ into an app-local feature folder, typically:
src/features/reveal-3d/4. Import from the local folder, never from the skill directory or the old external package. With a typical @/* alias:
import { CacheProvider, RevealKeepAlive, RevealProvider } from '@/features/reveal-3d';5. Configure Vite and `main.tsx`. Read vite-config.md and apply the process polyfill, manual process/util/assert aliases, three alias, dedupe settings, and worker.format: 'es'. 6. Choose the implementation pattern. Use Pattern B (model browser or direct model ID) unless you already have a DMInstanceRef and confirmed Core DM 3D linkage. For full examples, read implementation.md. 7. Keep provider placement stable. CacheProvider and RevealKeepAlive are always mounted at page/app level. RevealProvider is conditional, only when a model is selected or linked. 8. Run typecheck and build (tsc --noEmit, pnpm build, etc.) and fix any copied-import or dependency issues.
Minimal Example
import { useCallback, useMemo } from 'react';
import type { CogniteClient } from '@cognite/sdk';
import {
CacheProvider,
Reveal3DResources,
RevealCanvas,
RevealKeepAlive,
RevealProvider,
type AddCadResourceOptions,
} from '@/features/reveal-3d';
type SelectedModel = { modelId: number; revisionId: number };
function ViewerContent({ modelId, revisionId }: SelectedModel) {
const resources = useMemo<AddCadResourceOptions[]>(
() => [{ modelId, revisionId }],
[modelId, revisionId]
);
const onLoaded = useCallback(() => {}, []);
return (
<RevealCanvas>
<Reveal3DResources resources={resources} onModelsLoaded={onLoaded} />
</RevealCanvas>
);
}
export function ViewerPage({
sdk,
selected,
}: {
sdk: CogniteClient;
selected: SelectedModel | null;
}) {
const memoizedSdk = useMemo(() => sdk, [sdk.project]);
return (
<CacheProvider>
<RevealKeepAlive>
<div style={{ width: '100%', height: '70vh', position: 'relative' }}>
{selected && (
<RevealProvider sdk={memoizedSdk}>
<ViewerContent
modelId={selected.modelId}
revisionId={selected.revisionId}
/>
</RevealProvider>
)}
</div>
</RevealKeepAlive>
</CacheProvider>
);
}Dependencies
Suggested versions are starting points. If the target app already pins compatible versions, defer to the app.
| Package | Suggested version | Purpose |
|---|---|---|
react / react-dom | app version | UI framework |
@cognite/dune | app version | Authenticated SDK via useDune() |
@cognite/reveal | ^4.30.0 | Reveal viewer runtime |
@cognite/sdk | ^10.0.0 | CDF API client |
@tanstack/react-query | ^5.90.21 | Reveal/FDM data fetching hooks |
three | ^0.180.0 | Three.js singleton used by Reveal |
process, util, assert | latest | Browser polyfills for Reveal dependencies |
ajv | ^8 | Avoids older transitive AJV resolution in monorepos |
@types/three | latest dev dep | TypeScript types |
Example install (pnpm; adapt to the app's package manager):
pnpm add @cognite/reveal @cognite/sdk @tanstack/react-query three process util assert ajv
pnpm add -D @types/threeAfter install, check @cognite/reveal's three peer requirement and align three if needed.
Do not install vite-plugin-node-polyfills; use the explicit Vite aliases in vite-config.md.
Critical Rules
ViewerContentcontains onlyRevealCanvasandReveal3DResources; no providers.resourcespassed toReveal3DResourcesmust be memoized withuseMemo.onModelsLoaded,onSelect, and similar callbacks must be memoized withuseCallback.- The SDK passed to
RevealProvidermust be memoized withuseMemokeyed onclient.project. RevealCanvasfills its parent; the parent must have an explicit height.- Lazy-load canvas-heavy viewer content with
React.lazy+Suspensewhen adding a route/page.
Advanced Reference
For the copied bundle API and exports, read code/README.md.
For model browser and FDM-linked implementations, read references/implementation.md.
For Vite, worker, polyfill, and troubleshooting details, read references/vite-config.md.
Verification Checklist
- [ ] All files from
skills/reveal-3d/code/reveal/were copied into an app-local feature folder. - [ ] Imports point to the app-local folder (e.g.
@/features/reveal-3d). - [ ] The app does not import Reveal helpers from the old external package.
- [ ] Required dependencies are present in
package.json. - [ ]
main.tsxstarts with theprocesspolyfill before other imports. - [ ]
vite.config.tsuses manual aliases, dedupe,threesingleton alias, andworker.format: 'es'. - [ ]
CacheProviderandRevealKeepAliveare always mounted;RevealProvideris conditional when model selection is conditional. - [ ] The viewer container has an explicit height.
- [ ] Typecheck and build pass.
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:
src/features/reveal-3d/Then import from the app-local folder:
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-queryreactreact-domthree
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
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;
}
}
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>
);
}
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]);
}
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>
);
}
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>
);
}
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);
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>
);
}
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
);
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>
);
}
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;
}
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;
}
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]);
};
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 ?? []
);
};
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,
]
);
};
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]
);
};
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 || [];
}
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);
}
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
);
}
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]);
}
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 };
}
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;
}
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;
}
// 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';
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,
},
},
};
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;
}
}
}
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;
/**
* 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>;
}
/**
* 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));
}
/**
* 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;
}
/**
* 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,
};
{
"skill_name": "reveal-3d",
"evals": [
{
"id": 1,
"prompt": "I have a Dune app at apps/my-app. Add a 3D viewer page where I can browse and load CAD models from my CDF project. Add a button on the main page to reach it.",
"expected_output": "A working 3D viewer page with the local Reveal feature bundle copied into the app, model browsing (sdk.models3D.list), Vite configured correctly for Reveal with manual util/assert/process package aliases (NOT vite-plugin-node-polyfills), process polyfill in main.tsx, DuneAuthProvider from @cognite/dune as auth provider, and all provider nesting correct.",
"files": [],
"assertions": [
{
"text": "@cognite/reveal, @cognite/sdk, @tanstack/react-query, three, process, util, assert, and ajv are in package.json dependencies",
"type": "content"
},
{
"text": "All files from skills/reveal-3d/code/reveal are copied into an app-local feature folder such as src/features/reveal-3d",
"type": "content"
},
{
"text": "Reveal components and hooks are imported from the app-local feature folder, e.g. @/features/reveal-3d",
"type": "content"
},
{
"text": "vite-plugin-node-polyfills is NOT present in package.json or vite.config.ts",
"type": "content"
},
{
"text": "vite.config.ts has manual resolve.alias entries for util ('util/'), assert ('assert/'), and process ('process/browser')",
"type": "content"
},
{
"text": "src/dune-fe-auth-shim.ts does NOT exist (no longer needed — the copied Reveal bundle reads sdk from RevealContext directly)",
"type": "content"
},
{
"text": "vite.config.ts resolve.alias does NOT contain a 'dune-fe-auth' entry",
"type": "content"
},
{
"text": "vite.config.ts has resolve.alias.three pointing to node_modules/three/build/three.module.js",
"type": "content"
},
{
"text": "vite.config.ts has worker: { format: 'es' }",
"type": "content"
},
{
"text": "vite.config.ts has resolve.dedupe including react, react-dom, and @tanstack/react-query",
"type": "content"
},
{
"text": "vite.config.ts has optimizeDeps.include with process, util, assert, three, @cognite/reveal, react, react-dom, and @tanstack/react-query",
"type": "content"
},
{
"text": "vite.config.ts does NOT exclude the copied src/features/reveal-3d bundle from optimizeDeps",
"type": "content"
},
{
"text": "main.tsx starts with import process from 'process'; window.process = process; before any other imports",
"type": "content"
},
{
"text": "main.tsx uses DuneAuthProvider from @cognite/dune as the auth provider (NOT CDFAuthenticationProvider)",
"type": "content"
},
{
"text": "CacheProvider and RevealKeepAlive are rendered at the page/app level, NOT inside a conditionally-rendered component",
"type": "content"
},
{
"text": "RevealProvider is rendered conditionally (only when a model is selected), inside the stable RevealKeepAlive tree",
"type": "content"
},
{
"text": "The canvas content component (RevealCanvas + Reveal3DResources) contains NO provider components",
"type": "content"
},
{
"text": "SDK passed to RevealProvider is memoized with useMemo keyed on sdk.project",
"type": "content"
},
{
"text": "resources prop for Reveal3DResources is memoized with useMemo",
"type": "content"
},
{
"text": "onModelsLoaded callback is memoized with useCallback",
"type": "content"
},
{
"text": "The onLoad prop passed to ModelBrowser is wrapped in useCallback (not an inline arrow function)",
"type": "content"
},
{
"text": "The viewer container div has an explicit height (e.g. height: '70vh' or h-[65vh])",
"type": "content"
},
{
"text": "The canvas content component is lazy-loaded with React.lazy and wrapped in Suspense",
"type": "content"
}
]
},
{
"id": 2,
"prompt": "I have an FDM asset with space cdf_idm and externalId pump_001. Add a 3D viewer to my asset detail page that shows the CAD model linked to this asset. The detail page is at src/AssetDetail.tsx.",
"expected_output": "A viewer component using the app-local Reveal feature bundle and useModelsForInstanceQuery to discover linked 3D models, integrated into AssetDetail.tsx with CacheProvider+RevealKeepAlive at the page level and RevealProvider conditionally rendered.",
"files": [],
"assertions": [
{
"text": "useModelsForInstanceQuery is imported from the app-local Reveal feature folder, e.g. @/features/reveal-3d",
"type": "content"
},
{
"text": "A DMInstanceRef with space 'cdf_idm' and externalId 'pump_001' is passed to useModelsForInstanceQuery",
"type": "content"
},
{
"text": "CacheProvider and RevealKeepAlive are at the AssetDetail page level (not inside the conditional viewer area)",
"type": "content"
},
{
"text": "RevealProvider is conditionally rendered (only when models data exists)",
"type": "content"
},
{
"text": "resources is derived from useModelsForInstanceQuery result and memoized with useMemo",
"type": "content"
},
{
"text": "onModelsLoaded is wrapped in useCallback",
"type": "content"
},
{
"text": "SDK is memoized with useMemo keyed on sdk.project",
"type": "content"
},
{
"text": "The viewer container has an explicit height",
"type": "content"
},
{
"text": "main.tsx uses DuneAuthProvider from @cognite/dune as the auth provider (NOT CDFAuthenticationProvider)",
"type": "content"
},
{
"text": "vite.config.ts uses manual util/assert/process aliases and does NOT use vite-plugin-node-polyfills",
"type": "content"
},
{
"text": "src/dune-fe-auth-shim.ts does NOT exist and vite.config.ts resolve.alias has no 'dune-fe-auth' entry",
"type": "content"
}
]
},
{
"id": 3,
"prompt": "Add a full-screen 3D viewer that loads model ID 206509079235820 revision 576781257263693. Put it at a /viewer route.",
"expected_output": "A full-screen viewer component using the app-local Reveal feature bundle with direct modelId/revisionId (Pattern B), accessible at /viewer, with correct provider nesting and Vite config.",
"files": [],
"assertions": [
{
"text": "Reveal3DResources receives a resources array containing modelId 206509079235820 and revisionId 576781257263693",
"type": "content"
},
{
"text": "The resources array is memoized with useMemo",
"type": "content"
},
{
"text": "onModelsLoaded is wrapped in useCallback",
"type": "content"
},
{
"text": "CacheProvider and RevealKeepAlive are at the viewer page level, not co-located with RevealProvider in the same conditional block",
"type": "content"
},
{
"text": "The viewer container is full-screen (height: 100vh or h-screen or equivalent)",
"type": "content"
},
{
"text": "A route is configured at /viewer",
"type": "content"
},
{
"text": "vite.config.ts has worker: { format: 'es' } and uses manual util/assert/process aliases (NOT vite-plugin-node-polyfills)",
"type": "content"
},
{
"text": "main.tsx uses DuneAuthProvider from @cognite/dune as the auth provider (NOT CDFAuthenticationProvider)",
"type": "content"
},
{
"text": "The canvas content is lazy-loaded with React.lazy and Suspense",
"type": "content"
}
]
}
]
}
Implementation Reference — Reveal 3D Viewer
Full copy-paste ready implementations. Copy skills/reveal-3d/code/reveal/ into an app-local feature folder first, typically src/features/reveal-3d/, then import from that local folder. Pattern B (model browser — auto-loads models) is the default — use it unless the data has explicit FDM → CAD node linkage.
---
Pattern B (default) — model browser, auto-discovers models via sdk.models3D.list()
Canvas-only component — no providers. CacheProvider, RevealKeepAlive, and RevealProvider live in App.tsx.
import { useCallback, useMemo } from 'react';
import {
Reveal3DResources,
RevealCanvas,
type AddCadResourceOptions,
} from '@/features/reveal-3d';
export interface ViewerContentProps {
modelId: number;
revisionId: number;
}
/**
* Canvas-only — no CacheProvider, RevealKeepAlive, or RevealProvider here.
* All providers live in App.tsx so React StrictMode double-invoke completes
* at startup before any model loading starts.
*/
export function ViewerContent({ modelId, revisionId }: ViewerContentProps) {
// AddCadResourceOptions is just { modelId, revisionId } — no `type` field.
// Do NOT use { type: 'cad', modelId, revisionId } — that is TaggedAddResourceOptions.
const resources = useMemo<AddCadResourceOptions[]>(
() => [{ modelId, revisionId }],
[modelId, revisionId]
);
const onLoaded = useCallback(() => {}, []);
return (
<RevealCanvas>
<Reveal3DResources resources={resources} onModelsLoaded={onLoaded} />
</RevealCanvas>
);
}src/App.tsx — providers + model browser
import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
import * as THREE from 'three';
import { useDune } from '@cognite/dune';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import type { Model3D, Revision3D } from '@cognite/sdk';
import {
CacheProvider,
RevealKeepAlive,
RevealProvider,
type ViewerOptions,
} from '@/features/reveal-3d';
// Lazy-load canvas content — providers are eagerly imported
const ViewerContent = lazy(() =>
import('./components/ViewerContent').then((m) => ({ default: m.ViewerContent }))
);
// Module-level constants — stable references, never recreated on re-render
const BG = new THREE.Color(0x1a1a2e);
const VIEWER_OPTIONS: ViewerOptions = {
loadingIndicatorStyle: { placement: 'topRight', opacity: 0.1 },
antiAliasingHint: 'msaa2+fxaa',
ssaoQualityHint: 'medium',
};
type SelectedModel = { modelId: number; revisionId: number };
// --- Model discovery hooks ---
function useModels(query?: string) {
const { sdk } = useDune();
return useInfiniteQuery({
queryKey: ['3d-models', query],
queryFn: ({ pageParam }: { pageParam?: string }) =>
sdk.models3D.list({ limit: 1000, cursor: pageParam }) as Promise<{
items: Model3D[];
nextCursor?: string;
}>,
initialPageParam: undefined as string | undefined,
getNextPageParam: (page) => page.nextCursor,
enabled: !!sdk,
select: useCallback(
(data: any) => ({
...data,
pages: data.pages.map((p: any) => ({
...p,
items: p.items
.map((m: Model3D) => ({ ...m, name: m.name.trim() }))
.filter((m: Model3D) =>
query ? m.name.toLowerCase().includes(query.toLowerCase()) : true
),
})),
}),
[query]
),
});
}
function useBestRevision(modelId?: number) {
const { sdk } = useDune();
return useQuery({
queryKey: ['3d-revisions', modelId],
queryFn: async () => {
if (!modelId) return null;
const all: Revision3D[] = await sdk.revisions3D
.list(modelId)
.autoPagingToArray({ limit: -1 });
const published = all.filter((r) => r.published);
const candidates = published.length ? published : all;
return candidates.reduce((best, cur) =>
best.createdTime > cur.createdTime ? best : cur
) ?? null;
},
enabled: !!sdk && !!modelId,
});
}
// --- Model browser ---
//
// RULE 1: onSelect MUST be wrapped in useCallback at the call site.
// An inline arrow `(m) => setSelected(m)` creates a new reference every render.
// The useEffect([..., onSelect]) below fires on every render → infinite loop.
//
// RULE 2: call onSelect from useEffect, NOT during render.
// An if-block during render calling onSelect also causes infinite loops.
function ModelBrowser({ onSelect }: { onSelect: (m: SelectedModel) => void }) {
const [query, setQuery] = useState('');
const [pendingId, setPendingId] = useState<number>();
const { data } = useModels(query);
const { data: revision } = useBestRevision(pendingId);
const models = data?.pages.flatMap((p) => p.items) ?? [];
useEffect(() => {
if (revision && pendingId) {
onSelect({ modelId: pendingId, revisionId: revision.id });
}
}, [revision, pendingId, onSelect]);
return (
<div>
<input
placeholder="Search models…"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{models.map((m) => (
<button key={m.id} onClick={() => setPendingId(m.id)}>
{m.name}
</button>
))}
</div>
);
}
// --- App ---
export default function App() {
const { sdk: client, isLoading } = useDune();
// Memoize on sdk.project — prevents RevealProvider from remounting on
// unrelated sdk object reference changes
const sdk = useMemo(() => client, [client.project]);
const [selected, setSelected] = useState<SelectedModel | null>(null);
// useCallback is mandatory — see ModelBrowser RULE 1 above
const handleSelect = useCallback((m: SelectedModel) => {
setSelected((prev) => (!prev || prev.modelId !== m.modelId ? m : prev));
}, []);
if (isLoading) return <div>Connecting to CDF…</div>;
return (
// CacheProvider + RevealKeepAlive always mounted → StrictMode double-invoke
// completes at startup with no viewer to dispose.
// RevealProvider conditionally mounts → finds stable RevealKeepAlive viewerRef.
<CacheProvider>
<RevealKeepAlive>
<div style={{ display: 'flex', height: '100vh' }}>
<aside style={{ width: 280, overflowY: 'auto' }}>
<ModelBrowser onSelect={handleSelect} />
</aside>
<div style={{ flex: 1, position: 'relative' }}>
{selected && (
<RevealProvider sdk={sdk} color={BG} viewerOptions={VIEWER_OPTIONS}>
<Suspense fallback={<div>Loading viewer…</div>}>
<ViewerContent
modelId={selected.modelId}
revisionId={selected.revisionId}
/>
</Suspense>
</RevealProvider>
)}
</div>
</div>
</RevealKeepAlive>
</CacheProvider>
);
}---
Pattern A (fallback) — FDM auto-discover from an asset instance
Use only when you have a DMInstanceRef and the instance has CogniteVisualizable.object3D → CogniteCADNode linkage. Otherwise use Pattern B above.
src/components/ViewerContent.tsx (FDM variant)
import { useCallback, useMemo, useState } from 'react';
import type { DMInstanceRef } from '@cognite/reveal';
import {
Reveal3DResources,
RevealCanvas,
useModelsForInstanceQuery,
type AddCadResourceOptions,
type TaggedAddResourceOptions,
} from '@/features/reveal-3d';
function pickFirstCad(models: TaggedAddResourceOptions[]): AddCadResourceOptions | undefined {
const m = models[0];
return m?.type === 'cad'
? { ...m.addOptions, styling: { default: { renderGhosted: true } } }
: undefined;
}
export function ViewerContent({ instance }: { instance: DMInstanceRef }) {
const { data: models, isLoading } = useModelsForInstanceQuery(instance);
const [loaded, setLoaded] = useState(false);
const selected = useMemo(() => pickFirstCad(models ?? []), [models]);
const resources = useMemo(() => (selected ? [selected] : []), [selected]);
const onLoaded = useCallback(() => setLoaded(true), []);
if (isLoading) return <div>Loading 3D model…</div>;
if (!resources.length) return <div>No 3D data linked to this instance.</div>;
return (
<RevealCanvas>
<Reveal3DResources resources={resources} onModelsLoaded={onLoaded} />
</RevealCanvas>
);
}src/App.tsx (FDM variant)
Same CacheProvider / RevealKeepAlive / RevealProvider structure as Pattern B. Pass instance: DMInstanceRef to ViewerContent instead of modelId / revisionId.
import { useMemo, useState } from 'react';
import * as THREE from 'three';
import { useDune } from '@cognite/dune';
import type { DMInstanceRef } from '@cognite/reveal';
import {
CacheProvider,
RevealKeepAlive,
RevealProvider,
type ViewerOptions,
} from '@/features/reveal-3d';
import { ViewerContent } from './components/ViewerContent';
const BG = new THREE.Color(0x1a1a2e);
const OPTS: ViewerOptions = {
loadingIndicatorStyle: { placement: 'topRight', opacity: 0.1 },
antiAliasingHint: 'msaa2+fxaa',
ssaoQualityHint: 'medium',
};
export default function App() {
const { sdk: client, isLoading } = useDune();
const sdk = useMemo(() => client, [client.project]);
// Replace with however you receive the instance ref (prop, route param, selection, etc.)
const [instance] = useState<DMInstanceRef | null>(null);
if (isLoading) return <div>Connecting to CDF…</div>;
return (
<CacheProvider>
<RevealKeepAlive>
<div style={{ width: '100%', height: '100vh', position: 'relative' }}>
{instance && sdk.project && (
<RevealProvider sdk={sdk} color={BG} viewerOptions={OPTS}>
<ViewerContent instance={instance} />
</RevealProvider>
)}
</div>
</RevealKeepAlive>
</CacheProvider>
);
}Vite Configuration for @cognite/reveal in a Flows monorepo app
src/main.tsx — process polyfill must be the very first two lines
Prepend these two lines before any other import:
import process from 'process';
(window as unknown as Record<string, unknown>).process = process;
// all other existing imports below ↓The rest of main.tsx stays as-is. Order matters — the polyfill must run before any module that reads process.
---
vite.config.ts — standalone config (not mergeConfig)
Flows apps use a standalone vite.config.ts (not a shared base config from the monorepo root). Replace the file entirely with the following:
import path from 'node:path';
import { fusionOpenPlugin } from '@cognite/dune/vite';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import mkcert from 'vite-plugin-mkcert';
export default defineConfig({
base: './',
plugins: [react(), mkcert(), fusionOpenPlugin(), tailwindcss()],
define: {
// Some CJS deps use `global` instead of `globalThis`
'process.env': {},
'process.platform': JSON.stringify(''),
'process.version': JSON.stringify(''),
global: 'globalThis',
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
// --- Node built-in polyfills ---
// Use explicit package aliases, not vite-plugin-node-polyfills.
// The plugin introduces transitive dep conflicts ("Could not resolve 'inherits'").
// The `process`, `util`, and `assert` packages must be in app dependencies.
util: 'util/',
assert: 'assert/',
process: 'process/browser',
// --- Single Three.js instance ---
// @cognite/reveal bundles its own Three.js copy. Without this alias, the app
// and Reveal load two separate copies → "Multiple instances of Three.js" warning
// and broken rendering. Requires `three` as a direct app dependency.
three: path.resolve(__dirname, 'node_modules/three/build/three.module.js'),
},
// pnpm uses a virtual store with symlinks. Packages that symlink to different
// paths can resolve to separate module instances even for the same package.
// `dedupe` forces a single physical copy for all pre-bundles.
// Missing `react`/`react-dom` here causes ReactCurrentDispatcher errors.
// Missing `three` causes "Multiple instances" warnings.
dedupe: ['react', 'react-dom', 'react/jsx-runtime', '@tanstack/react-query', 'three'],
conditions: ['import', 'module', 'browser', 'default'],
},
optimizeDeps: {
// Do not exclude the copied Reveal feature bundle. Let Vite pre-bundle
// React, React Query, Three.js, and Reveal dependencies as one graph.
esbuildOptions: {
define: { global: 'globalThis' },
},
include: [
// Vite can't auto-discover bare polyfill imports (no source file imports them
// directly). List them explicitly so esbuild pre-bundles them.
'process',
'util',
'assert',
// Heavy/complex deps — explicit listing speeds up cold starts
'three',
'@cognite/reveal',
// React ecosystem — pre-bundling creates the CJS→ESM singleton all deps share.
// If any of these are missing, a dep that imports React raw can get a second copy.
'react',
'react-dom',
'@tanstack/react-query',
],
},
server: {
port: 3002,
},
worker: {
// @cognite/reveal spawns ES module web workers. Without 'es' format they fail
// silently — black screen with no console error.
format: 'es',
},
});---
Why each setting is needed
| Setting | Reason |
|---|---|
util/, assert/, process/browser aliases | Browser-compatible replacements for Node built-ins. Packages must be in dependencies. Do NOT use vite-plugin-node-polyfills — causes "Could not resolve 'inherits'" |
process polyfill in main.tsx first | @cognite/reveal deps call process.env at runtime (not build-time). The define replacements handle build-time; the window assignment handles runtime |
define.global = 'globalThis' | Some CJS deps use global instead of globalThis |
resolve.alias.three | Single Three.js instance — without this, Reveal's bundled copy and the app's copy conflict |
resolve.dedupe with react + react-dom + react/jsx-runtime | pnpm symlinks can create separate module instances. dedupe forces one copy. Critical — missing these causes ReactCurrentDispatcher errors |
resolve.dedupe with three | Ensures the copied feature bundle and Reveal share the same Three.js |
optimizeDeps.include for process/util/assert | No source file imports them, so Vite cannot auto-discover them for pre-bundling |
optimizeDeps.include for react + react-dom + @tanstack/react-query | Converts CJS → ESM and creates a single shared instance. All pre-bundled deps that import React get the same copy |
worker.format: 'es' | Reveal spawns ES module workers; Vite defaults to IIFE/UMD which breaks them |
conditions: ['import', 'module', 'browser', 'default'] | Ensures browser ESM variants are preferred over CJS/Node variants |
Common mistakes that break the setup
| Mistake | Symptom | Fix |
|---|---|---|
Installing three without checking @cognite/reveal's peer requirement | unmet peer three@0.180.0: found 0.177.x warning; potential rendering bugs | After install, compare versions; pnpm add three@^<peer-version> if mismatched |
Not adding ajv as a direct dependency | unmet peer ajv@>=8: found 6.x | pnpm add ajv (installs ^8) in the app |
Copied Reveal feature folder in optimizeDeps.exclude | ReactCurrentDispatcher undefined or No QueryClient set | Remove from exclude and let Vite pre-bundle shared deps |
vite-plugin-node-polyfills instead of manual aliases | Could not resolve "inherits" on transitive deps | Remove the plugin; add util, assert, process to dependencies and use aliases |
RevealKeepAlive inside conditional component | ObjectUnsubscribedError: object unsubscribed at model load | Move CacheProvider + RevealKeepAlive to always-mounted app/page level |
Inline arrow as onSelect/onLoad prop | Maximum update depth exceeded | useCallback at call site; call onSelect from useEffect inside model browser, never from render |
Model browser calls onSelect during render (if (revision) onSelect(...)) | Maximum update depth exceeded | Move to useEffect([revision, onSelect]) |
Missing worker.format: 'es' | Black screen, no error | Add worker: { format: 'es' } |
react/react-dom missing from resolve.dedupe | ReactCurrentDispatcher in pnpm monorepo | Add both to dedupe |
| Container has no height | Canvas collapses to 0px, nothing renders | Add height: '70vh' (or flex/grid height) to the parent element |
Related skills
FAQ
What is reveal-3d?
Integrates a local Cognite Reveal 3D CAD viewer bundle into Flows apps by copying app-local source code. Use when adding 3D viewer, 3D visualization, Reveal, CAD model, RevealProvi
When should I use reveal-3d?
Integrates a local Cognite Reveal 3D CAD viewer bundle into Flows apps by copying app-local source code. Use when adding 3D viewer, 3D visualization, Reveal, CAD model, RevealProvi
Is reveal-3d safe to install?
Review the Security Audits panel on this page before production use.