This commit is contained in:
Ole
2026-05-31 20:25:41 +00:00
commit 0a07ab8593
275 changed files with 52660 additions and 0 deletions
@@ -0,0 +1,213 @@
import { useEffect, useRef } from 'react';
import {
type CogniteCadModel,
TreeIndexNodeCollection,
NumericRange,
} from '@cognite/reveal';
import { useReveal } from '../hooks/useReveal';
import type { AddCadResourceOptions, InstanceStylingGroup } from '../types';
import { useFdmAssetMappings } from '../hooks/useFdmMappings';
interface Reveal3DResourcesProps {
resources: AddCadResourceOptions[];
instanceStyling?: InstanceStylingGroup[];
onModelsLoaded?: () => void;
}
export function Reveal3DResources({
resources,
instanceStyling = [],
onModelsLoaded,
}: Reveal3DResourcesProps) {
const viewer = useReveal();
const loadedModelsRef = useRef<CogniteCadModel[]>([]);
useEffect(() => {
if (!viewer || resources.length === 0) {
return;
}
const cancelledRef = { current: false };
const loadModels = async () => {
const modelPromises = resources.map(async (resource) => {
if (cancelledRef.current) return null;
try {
const existing = viewer.models.find(
(m) =>
m.modelId === resource.modelId &&
m.revisionId === resource.revisionId
);
if (existing) {
return existing as CogniteCadModel;
}
const addModelOptions: {
modelId: number;
revisionId: number;
geometryFilter?: typeof resource.geometryFilter;
} = {
modelId: resource.modelId,
revisionId: resource.revisionId,
};
if (resource.geometryFilter) {
addModelOptions.geometryFilter = resource.geometryFilter;
}
const model = await viewer.addCadModel(addModelOptions);
if (cancelledRef.current) {
viewer.removeModel(model);
return null;
}
if (resource.styling?.default) {
const { renderGhosted, renderInFront } = resource.styling.default;
if (renderGhosted !== undefined) {
model.setDefaultNodeAppearance({
renderGhosted,
renderInFront: renderInFront ?? false,
});
}
}
return model;
} catch (error) {
console.error('Error loading CAD model:', error);
return null;
}
});
const loadedModels = (await Promise.all(modelPromises)).filter(
(model): model is CogniteCadModel => model !== null
);
if (!cancelledRef.current) {
loadedModelsRef.current = loadedModels;
if (loadedModels.length > 0) {
viewer.fitCameraToModels(loadedModels, 0);
}
if (onModelsLoaded) {
onModelsLoaded();
}
}
};
loadModels();
return () => {
cancelledRef.current = true;
const modelsToRemove = loadedModelsRef.current;
for (const model of modelsToRemove) {
try {
viewer.removeModel(model);
} catch (error) {
console.error('Error removing model:', error);
}
}
loadedModelsRef.current = [];
};
}, [viewer, resources, onModelsLoaded]);
useApplyInstanceStyling(viewer, loadedModelsRef.current, instanceStyling);
return null;
}
function useApplyInstanceStyling(
viewer: ReturnType<typeof useReveal>,
loadedModels: CogniteCadModel[],
instanceStyling: InstanceStylingGroup[]
) {
const appliedCollectionsRef = useRef<
Map<CogniteCadModel, TreeIndexNodeCollection[]>
>(new Map());
const fdmInstances =
instanceStyling.flatMap((group) => group.fdmAssetExternalIds || []) || [];
const modelOptions = loadedModels.map((model) => ({
type: 'cad' as const,
modelId: model.modelId,
revisionId: model.revisionId,
}));
const { data: assetMappings } = useFdmAssetMappings(
fdmInstances,
modelOptions
);
useEffect(() => {
if (!viewer || loadedModels.length === 0) return;
const hasFdmInstances = instanceStyling.some(
(g) => g.fdmAssetExternalIds && g.fdmAssetExternalIds.length > 0
);
if (!hasFdmInstances) {
for (const [model, collections] of appliedCollectionsRef.current.entries()) {
for (const collection of collections) {
try { model.unassignStyledNodeCollection(collection); } catch { /* already removed */ }
}
}
appliedCollectionsRef.current.clear();
return;
}
if (!assetMappings) return;
for (const [model, collections] of appliedCollectionsRef.current.entries()) {
for (const collection of collections) {
try { model.unassignStyledNodeCollection(collection); } catch { /* already removed */ }
}
}
appliedCollectionsRef.current.clear();
for (const stylingGroup of instanceStyling) {
if (!stylingGroup.fdmAssetExternalIds || !stylingGroup.style.cad) {
continue;
}
const appearance = stylingGroup.style.cad;
for (const model of loadedModels) {
const modelMapping = assetMappings.find(
(m) =>
m.modelId === model.modelId && m.revisionId === model.revisionId
);
if (!modelMapping) continue;
const nodeCollection = new TreeIndexNodeCollection();
const indexSet = nodeCollection.getIndexSet();
for (const instance of stylingGroup.fdmAssetExternalIds) {
const nodes = modelMapping.mappings.get(`${instance.space}:${instance.externalId}`);
if (!nodes) continue;
for (const node of nodes) {
if (
node.treeIndex !== undefined &&
node.subtreeSize !== undefined
) {
const range = new NumericRange(node.treeIndex, node.subtreeSize);
indexSet.addRange(range);
}
}
}
if (indexSet.count > 0) {
nodeCollection.updateSet(indexSet);
model.assignStyledNodeCollection(nodeCollection, appearance);
const existing = appliedCollectionsRef.current.get(model) ?? [];
existing.push(nodeCollection);
appliedCollectionsRef.current.set(model, existing);
}
}
}
}, [viewer, loadedModels, instanceStyling, assetMappings]);
}
@@ -0,0 +1,27 @@
import { type ReactNode, type ReactElement, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useRevealContext } from '../hooks/useRevealContext';
export function RevealCanvas({
children,
}: {
children?: ReactNode;
}): ReactElement {
const { viewer } = useRevealContext();
const parentElement = useRef<HTMLDivElement>(null);
useEffect(() => {
if (parentElement.current !== null) {
parentElement.current.appendChild(viewer.domElement);
}
}, [viewer]);
return (
<div
style={{ width: '100%', height: '100%', overflow: 'hidden' }}
ref={parentElement}
>
{createPortal(children, viewer.domElement)}
</div>
);
}
@@ -0,0 +1,84 @@
import { useRef, useEffect, createContext, useContext, useCallback } from 'react';
import type { Cognite3DViewer } from '@cognite/reveal';
import type { CogniteClient } from '@cognite/sdk';
interface RevealKeepAliveContextValue {
getOrCreateViewer: (
sdk: CogniteClient,
createViewer: () => Cognite3DViewer
) => Cognite3DViewer;
isMounted: () => boolean;
}
const RevealKeepAliveContext = createContext<RevealKeepAliveContextValue | null>(
null
);
export function useRevealKeepAlive() {
const context = useContext(RevealKeepAliveContext);
if (!context) {
throw new Error('useRevealKeepAlive must be used within RevealKeepAlive');
}
return context;
}
/**
* Returns null when not inside a RevealKeepAlive provider.
* Used by RevealProvider to conditionally reuse a kept-alive viewer.
*/
export function useOptionalRevealKeepAlive(): RevealKeepAliveContextValue | null {
return useContext(RevealKeepAliveContext);
}
interface RevealKeepAliveProps {
children: React.ReactNode;
}
/**
* Keeps the Cognite3DViewer instance alive across component unmounts,
* eliminating viewer reinitialization when navigating between assets (~2-3s saving).
*
* The viewer persists in a ref that survives child unmount/remount cycles.
* Models are managed separately (added/removed as needed).
* Disposal is deferred to survive React StrictMode's mount→unmount→remount cycle.
*/
export function RevealKeepAlive({ children }: RevealKeepAliveProps) {
const viewerRef = useRef<Cognite3DViewer | null>(null);
const mountedRef = useRef(false);
const getOrCreateViewer = useCallback(
(_sdk: CogniteClient, createViewer: () => Cognite3DViewer) => {
if (!viewerRef.current) {
viewerRef.current = createViewer();
}
return viewerRef.current;
},
[]
);
const isMounted = useCallback(() => {
return mountedRef.current;
}, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
useEffect(() => {
return () => {
if (viewerRef.current) {
viewerRef.current.dispose();
viewerRef.current = null;
}
};
}, []);
return (
<RevealKeepAliveContext.Provider value={{ getOrCreateViewer, isMounted }}>
{children}
</RevealKeepAliveContext.Provider>
);
}