diff --git a/react-components/src/components/CadModelContainer/CadModelContainer.tsx b/react-components/src/components/CadModelContainer/CadModelContainer.tsx index 45e1fcc06d7..fbdfefc55b5 100644 --- a/react-components/src/components/CadModelContainer/CadModelContainer.tsx +++ b/react-components/src/components/CadModelContainer/CadModelContainer.tsx @@ -2,38 +2,17 @@ * Copyright 2023 Cognite AS */ import { type ReactElement, useEffect, useState } from 'react'; -import { - type NodeAppearance, - type AddModelOptions, - type CogniteCadModel, - TreeIndexNodeCollection, - NodeIdNodeCollection, - DefaultNodeAppearance, - type NodeCollection, - type Cognite3DViewer -} from '@cognite/reveal'; +import { type AddModelOptions, type CogniteCadModel } from '@cognite/reveal'; import { useReveal } from '../RevealContainer/RevealContext'; import { Matrix4 } from 'three'; -import { useSDK } from '../RevealContainer/SDKProvider'; -import { type CogniteClient } from '@cognite/sdk'; import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; +import { + type CadModelStyling, + useApplyCadModelStyling, + modelExists +} from './useApplyCadModelStyling'; -export type NodeStylingGroup = { - nodeIds: number[]; - style?: NodeAppearance; -}; - -export type TreeIndexStylingGroup = { - treeIndices: number[]; - style?: NodeAppearance; -}; - -export type CadModelStyling = { - defaultStyle?: NodeAppearance; - groups?: Array; -}; - -type CogniteCadModelProps = { +export type CogniteCadModelProps = { addModelOptions: AddModelOptions; styling?: CadModelStyling; transform?: Matrix4; @@ -47,13 +26,13 @@ export function CadModelContainer({ onLoad }: CogniteCadModelProps): ReactElement { const cachedViewerRef = useRevealKeepAlive(); - const [model, setModel] = useState(); - const viewer = useReveal(); - const sdk = useSDK(); - const defaultStyle = styling?.defaultStyle ?? DefaultNodeAppearance.Default; - const styleGroups = styling?.groups; + const [model, setModel] = useState( + viewer.models.find( + (m) => m.modelId === addModelOptions.modelId && m.revisionId === addModelOptions.revisionId + ) as CogniteCadModel | undefined + ); const { modelId, revisionId, geometryFilter } = addModelOptions; @@ -63,33 +42,11 @@ export function CadModelContainer({ useEffect(() => { if (!modelExists(model, viewer) || transform === undefined) return; + model.setModelTransformation(transform); }, [transform, model]); - useEffect(() => { - if (!modelExists(model, viewer) || styleGroups === undefined) return; - const stylingCollections = applyStyling(sdk, model, styleGroups); - - return () => { - if (!modelExists(model, viewer)) return; - void stylingCollections.then((nodeCollections) => { - nodeCollections.forEach((nodeCollection) => { - model.unassignStyledNodeCollection(nodeCollection); - }); - }); - }; - }, [styleGroups, model]); - - useEffect(() => { - if (!modelExists(model, viewer)) return; - model.setDefaultNodeAppearance(defaultStyle); - return () => { - if (!modelExists(model, viewer)) { - return; - } - model.setDefaultNodeAppearance(DefaultNodeAppearance.Default); - }; - }, [defaultStyle, model]); + useApplyCadModelStyling(model, styling); useEffect(() => removeModel, [model]); @@ -134,31 +91,3 @@ export function CadModelContainer({ setModel(undefined); } } - -async function applyStyling( - sdk: CogniteClient, - model: CogniteCadModel, - stylingGroups: Array -): Promise { - const collections: NodeCollection[] = []; - for (const group of stylingGroups) { - if ('treeIndices' in group && group.style !== undefined) { - const nodes = new TreeIndexNodeCollection(group.treeIndices); - model.assignStyledNodeCollection(nodes, group.style); - collections.push(nodes); - } else if ('nodeIds' in group && group.style !== undefined) { - const nodes = new NodeIdNodeCollection(sdk, model); - await nodes.executeFilter(group.nodeIds); - model.assignStyledNodeCollection(nodes, group.style); - collections.push(nodes); - } - } - return collections; -} - -function modelExists( - model: CogniteCadModel | undefined, - viewer: Cognite3DViewer -): model is CogniteCadModel { - return model !== undefined && viewer.models.includes(model); -} diff --git a/react-components/src/components/CadModelContainer/useApplyCadModelStyling.tsx b/react-components/src/components/CadModelContainer/useApplyCadModelStyling.tsx new file mode 100644 index 00000000000..0392ea81226 --- /dev/null +++ b/react-components/src/components/CadModelContainer/useApplyCadModelStyling.tsx @@ -0,0 +1,156 @@ +/*! + * Copyright 2023 Cognite AS + */ +import { + type CogniteCadModel, + DefaultNodeAppearance, + type NodeAppearance, + type NodeCollection, + NodeIdNodeCollection, + TreeIndexNodeCollection, + type Cognite3DViewer +} from '@cognite/reveal'; +import { useEffect } from 'react'; +import { useSDK } from '../RevealContainer/SDKProvider'; +import { type CogniteClient } from '@cognite/sdk'; +import { isEqual } from 'lodash'; +import { useReveal } from '../RevealContainer/RevealContext'; + +export type NodeStylingGroup = { + nodeIds: number[]; + style?: NodeAppearance; +}; + +export type TreeIndexStylingGroup = { + treeIndices: number[]; + style?: NodeAppearance; +}; + +export type CadModelStyling = { + defaultStyle?: NodeAppearance; + groups?: Array; +}; + +export const useApplyCadModelStyling = ( + model?: CogniteCadModel, + modelStyling?: CadModelStyling +): void => { + const viewer = useReveal(); + const sdk = useSDK(); + + const defaultStyle = modelStyling?.defaultStyle ?? DefaultNodeAppearance.Default; + const styleGroups = modelStyling?.groups; + + useEffect(() => { + if (!modelExists(model, viewer) || styleGroups === undefined) return; + + void applyStyling(sdk, model, styleGroups); + }, [styleGroups, model]); + + useEffect(() => { + if (!modelExists(model, viewer)) return; + + model.setDefaultNodeAppearance(defaultStyle); + }, [defaultStyle, model]); +}; + +async function applyStyling( + sdk: CogniteClient, + model: CogniteCadModel, + stylingGroups: Array +): Promise { + const firstChangeIndex = getFirstChangeIndex(); + + for (let i = firstChangeIndex; i < model.styledNodeCollections.length; i++) { + const viewerStyledNodeCollection = model.styledNodeCollections[i]; + model.unassignStyledNodeCollection(viewerStyledNodeCollection.nodeCollection); + } + + for (let i = firstChangeIndex; i < stylingGroups.length; i++) { + const stylingGroup = stylingGroups[i]; + + if (stylingGroup.style === undefined) continue; + + if ('treeIndices' in stylingGroup) { + const nodes = new TreeIndexNodeCollection(stylingGroup.treeIndices); + model.assignStyledNodeCollection(nodes, stylingGroup.style); + } + + if ('nodeIds' in stylingGroup) { + const nodes = new NodeIdNodeCollection(sdk, model); + await nodes.executeFilter(stylingGroup.nodeIds); + model.assignStyledNodeCollection(nodes, stylingGroup.style); + } + } + + function getFirstChangeIndex(): number { + for (let i = 0; i < model.styledNodeCollections.length; i++) { + const stylingGroup = stylingGroups[i]; + const viewerStyledNodeCollection = model.styledNodeCollections[i]; + + const areEqual = isEqualStylingGroupAndCollection(stylingGroup, viewerStyledNodeCollection); + + if (!areEqual) { + return i; + } + } + + return model.styledNodeCollections.length; + } +} + +function isEqualStylingGroupAndCollection( + group: NodeStylingGroup | TreeIndexStylingGroup, + collection: { + nodeCollection: NodeCollection; + appearance: NodeAppearance; + } +): boolean { + if (group?.style === undefined) return false; + + const isEqualGroupStyle = isEqualStyle(collection.appearance, group.style); + + if (collection.nodeCollection instanceof TreeIndexNodeCollection && 'treeIndices' in group) { + const compareCollection = new TreeIndexNodeCollection(group.treeIndices); + const isEqualContent = isEqualTreeIndex(collection.nodeCollection, compareCollection); + + return isEqualGroupStyle && isEqualContent; + } + + if (collection.nodeCollection instanceof NodeIdNodeCollection && 'nodeIds' in group) { + const collectionNodeIds = collection.nodeCollection.serialize().state.nodeIds as number[]; + const isEqualContent = isEqual(collectionNodeIds, group.nodeIds); + + return isEqualGroupStyle && isEqualContent; + } + + return false; +} + +function isEqualTreeIndex( + collectionA: TreeIndexNodeCollection, + collectionB: TreeIndexNodeCollection +): boolean { + const isEqualContent = + collectionA.getIndexSet().differenceWith(collectionB.getIndexSet()).count === 0; + return isEqualContent; +} + +function isEqualStyle(styleA: NodeAppearance, styleB: NodeAppearance): boolean { + const { color: colorA, ...restA } = styleA; + const { color: colorB, ...restB } = styleB; + + const color = + colorA === undefined || colorB === undefined + ? Boolean(colorA ?? colorB) + : colorA.equals(colorB); + + return color && isEqual(restA, restB); +} + +export function modelExists( + model: CogniteCadModel | undefined, + viewer: Cognite3DViewer +): model is CogniteCadModel { + return model !== undefined && viewer.models.includes(model); +} diff --git a/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx b/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx index 05b70935eb4..81a42d5227c 100644 --- a/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx +++ b/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx @@ -3,7 +3,8 @@ */ import { useRef, type ReactElement, useState, useEffect } from 'react'; import { type Cognite3DViewer } from '@cognite/reveal'; -import { CadModelContainer, type CadModelStyling } from '../CadModelContainer/CadModelContainer'; +import { CadModelContainer } from '../CadModelContainer/CadModelContainer'; +import { type CadModelStyling } from '../CadModelContainer/useApplyCadModelStyling'; import { PointCloudContainer, type PointCloudModelStyling diff --git a/react-components/src/hooks/useCalculateModelsStyling.tsx b/react-components/src/hooks/useCalculateModelsStyling.tsx index 0e60723696d..66a0cfbfc6f 100644 --- a/react-components/src/hooks/useCalculateModelsStyling.tsx +++ b/react-components/src/hooks/useCalculateModelsStyling.tsx @@ -6,10 +6,6 @@ import { type TypedReveal3DModel } from '../components/Reveal3DResources/types'; import { type PointCloudModelStyling } from '../components/PointCloudContainer/PointCloudContainer'; -import { - type NodeStylingGroup, - type CadModelStyling -} from '../components/CadModelContainer/CadModelContainer'; import { type InModel3dEdgeProperties } from '../utilities/globalDataModels'; import { type EdgeItem } from '../utilities/FdmSDK'; import { type NodeAppearance } from '@cognite/reveal'; @@ -18,6 +14,10 @@ import { type CogniteExternalId, type CogniteInternalId } from '@cognite/sdk'; import { useFdmAssetMappings } from './useFdmAssetMappings'; import { useEffect, useMemo } from 'react'; import { useMappedEdgesForRevisions } from '../components/NodeCacheProvider/NodeCacheProvider'; +import { + type CadModelStyling, + type NodeStylingGroup +} from '../components/CadModelContainer/useApplyCadModelStyling'; type ModelStyleGroup = { model: TypedReveal3DModel; diff --git a/react-components/src/index.ts b/react-components/src/index.ts index fdbf75da5c1..bddbb559deb 100644 --- a/react-components/src/index.ts +++ b/react-components/src/index.ts @@ -21,6 +21,7 @@ export { use3DModelName } from './hooks/use3DModelName'; export { useFdmAssetMappings } from './hooks/useFdmAssetMappings'; export { useClickedNodeData, type ClickedNodeData } from './hooks/useClickedNode'; export { useCameraNavigation } from './hooks/useCameraNavigation'; +export { useMappedEdgesForRevisions } from './components/NodeCacheProvider/NodeCacheProvider'; // Higher order components export { withSuppressRevealEvents } from './higher-order-components/withSuppressRevealEvents'; @@ -30,11 +31,12 @@ export { type PointCloudModelStyling, type AnnotationIdStylingGroup } from './components/PointCloudContainer/PointCloudContainer'; +export { type CogniteCadModelProps } from './components/CadModelContainer/CadModelContainer'; export { type CadModelStyling, type TreeIndexStylingGroup, type NodeStylingGroup -} from './components/CadModelContainer/CadModelContainer'; +} from './components/CadModelContainer/useApplyCadModelStyling'; export { type Reveal3DResourcesProps, type FdmAssetStylingGroup, diff --git a/react-components/stories/CadStylingCache.stories.tsx b/react-components/stories/CadStylingCache.stories.tsx new file mode 100644 index 00000000000..d8306d4b194 --- /dev/null +++ b/react-components/stories/CadStylingCache.stories.tsx @@ -0,0 +1,139 @@ +/*! + * Copyright 2023 Cognite AS + */ +import type { Meta, StoryObj } from '@storybook/react'; +import { + CadModelContainer, + type CadModelStyling, + type CogniteCadModelProps, + type NodeStylingGroup, + RevealContainer, + useReveal +} from '../src'; +import { Color, Matrix4, Vector3 } from 'three'; +import { createSdkByUrlToken } from './utilities/createSdkByUrlToken'; +import { type CogniteCadModel, DefaultNodeAppearance } from '@cognite/reveal'; +import { useEffect, useMemo, useState, type JSX } from 'react'; +import { useMappedEdgesForRevisions } from '../src/components/NodeCacheProvider/NodeCacheProvider'; + +const meta = { + title: 'Example/CadStylingCache', + component: CadModelContainer, + tags: ['autodocs'] +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const sdk = createSdkByUrlToken(); + +export const Main: Story = { + args: { + addModelOptions: { + modelId: 2231774635735416, + revisionId: 912809199849811 + }, + styling: {}, + transform: new Matrix4().makeTranslation(0, 10, 0) + }, + render: ({ addModelOptions, transform, styling }) => ( + + + + ) +}; + +const Models = ({ addModelOptions }: CogniteCadModelProps): JSX.Element => { + const platformModelOptions = addModelOptions; + + const viewer = useReveal(); + + const [platformStyling, setPlatformStyling] = useState(); + + const { data } = useMappedEdgesForRevisions([platformModelOptions], true); + + const nodeIds = useMemo( + () => + data + ?.get(`${platformModelOptions.modelId}-${platformModelOptions.revisionId}`) + ?.map((edgeWithNode) => edgeWithNode.edge.properties.revisionNodeId), + [data] + ); + + useEffect(() => { + const callback = (): void => { + if (platformStyling === undefined || nodeIds === undefined) return; + + setPlatformStyling((prev): CadModelStyling | undefined => { + if (prev?.groups === undefined) return prev; + console.log('New group', prev.groups); + + const newNodeIds = getRandomSubset(nodeIds, nodeIds.length * 0.8); + + return { + groups: [ + ...prev.groups, + { + nodeIds: newNodeIds.slice(0, newNodeIds.length / 2), + style: { + color: new Color().setFromVector3( + new Vector3(Math.random(), Math.random(), Math.random()) + ), + prioritizedForLoadingHint: 5 + } + } + ], + defaultStyle: prev.defaultStyle + }; + }); + }; + + viewer.on('click', callback); + return () => { + viewer.off('click', callback); + }; + }, [viewer, platformStyling, setPlatformStyling]); + + useEffect(() => { + if (nodeIds === undefined) return; + + const stylingGroupRed: NodeStylingGroup = { + nodeIds: nodeIds.slice(0, nodeIds.length), + style: { + color: new Color('red'), + renderInFront: true + } + }; + + setPlatformStyling({ + defaultStyle: DefaultNodeAppearance.Ghosted, + groups: [stylingGroupRed] + }); + }, [viewer, data]); + + const onModelLoaded = (model: CogniteCadModel): void => { + viewer.fitCameraToModel(model); + }; + + return ( + <> + + + ); +}; + +function getRandomSubset(array: T[], size: number): T[] { + const subset: T[] = []; + + for (let i = 0; i < size; i++) { + const index = Math.floor(Math.random() * array.length); + + subset.push(array[index]); + } + + return subset; +} diff --git a/react-components/stories/HighlightNode.stories.tsx b/react-components/stories/HighlightNode.stories.tsx index 5bd142ec153..0510e2e20f4 100644 --- a/react-components/stories/HighlightNode.stories.tsx +++ b/react-components/stories/HighlightNode.stories.tsx @@ -13,7 +13,7 @@ import { useCameraNavigation } from '../src'; import { Color } from 'three'; -import { type ReactElement, useState, useEffect, useRef } from 'react'; +import { type ReactElement, useState, useEffect } from 'react'; import { DefaultNodeAppearance } from '@cognite/reveal'; import { createSdkByUrlToken } from './utilities/createSdkByUrlToken'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; @@ -58,37 +58,25 @@ export const Main: Story = { }; const StoryContent = ({ resources }: { resources: AddResourceOptions[] }): ReactElement => { - const [highlightedId, setHighlightedId] = useState(undefined); - const stylingGroupsRef = useRef([]); - - const nodeData = useClickedNodeData(); + const [stylingGroups, setStylingGroups] = useState([]); const cameraNavigation = useCameraNavigation(); + const nodeData = useClickedNodeData(); useEffect(() => { - setHighlightedId(nodeData?.fdmNode.externalId); - if (nodeData === undefined) return; + setStylingGroups([ + { + fdmAssetExternalIds: [{ externalId: nodeData.fdmNode.externalId, space: 'pdms-mapping' }], + style: { cad: DefaultNodeAppearance.Highlighted } + } + ]); void cameraNavigation.fitCameraToInstance(nodeData.fdmNode.externalId, 'pdms-mapping'); - }, [nodeData?.fdmNode.externalId]); - - if (stylingGroupsRef.current.length === 1) { - stylingGroupsRef.current.pop(); - } - - if (highlightedId !== undefined) { - stylingGroupsRef.current.push({ - fdmAssetExternalIds: [{ externalId: highlightedId, space: 'pdms-mapping' }], - style: { cad: DefaultNodeAppearance.Highlighted } - }); - } + }, [nodeData?.fdmNode]); return ( <> - + );