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([]); 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, loadedModels: CogniteCadModel[], instanceStyling: InstanceStylingGroup[] ) { const appliedCollectionsRef = useRef< Map >(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]); }