init
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user