From 04ac5aa42f56ecd07c8f3b7951f062c5055cb813 Mon Sep 17 00:00:00 2001 From: "Christopher J. Tannum" Date: Tue, 15 Aug 2023 13:36:43 +0200 Subject: [PATCH 1/2] feat(react-components): add useCameraNavigation hook (#3581) * feat: add cameraNavigation hook * fix: restore stories and fix styling cleanup issue * chore: bump react components version to 0.10.0 * chore: import string --- react-components/.eslintrc.cjs | 1 + react-components/package.json | 2 +- .../CadModelContainer/CadModelContainer.tsx | 25 ++++--- .../CameraController/CameraController.tsx | 60 ---------------- .../PointCloudContainer.tsx | 2 +- .../Reveal3DResources/ModelsLoadingContext.ts | 13 ---- .../Reveal3DResources/Reveal3DResources.tsx | 11 ++- .../src/components/Reveal3DResources/types.ts | 1 + .../RevealContainer/RevealContainer.tsx | 15 +--- .../RevealToolbar/FitModelsButton.tsx | 14 ++-- .../src/hooks/useCameraNavigation.tsx | 68 +++++++++++++++++++ react-components/src/index.ts | 3 +- .../stories/CadModelContainer.stories.tsx | 10 +-- .../stories/HighlightNode.stories.tsx | 42 +++++------- .../stories/ModelsStyling.stories.tsx | 14 ++-- .../stories/PointCloudContainer.stories.tsx | 13 +--- .../stories/Reveal3DResources.stories.tsx | 13 +--- .../stories/RevealKeepAlive.stories.tsx | 13 ++-- .../stories/ViewerAnchor.stories.tsx | 14 +--- .../with3dResoursesFitCameraOnLoad.tsx | 23 +++++++ 20 files changed, 159 insertions(+), 198 deletions(-) delete mode 100644 react-components/src/components/CameraController/CameraController.tsx delete mode 100644 react-components/src/components/Reveal3DResources/ModelsLoadingContext.ts create mode 100644 react-components/src/hooks/useCameraNavigation.tsx create mode 100644 react-components/stories/utilities/with3dResoursesFitCameraOnLoad.tsx diff --git a/react-components/.eslintrc.cjs b/react-components/.eslintrc.cjs index 5d1d664f654..626bc37f36e 100644 --- a/react-components/.eslintrc.cjs +++ b/react-components/.eslintrc.cjs @@ -19,6 +19,7 @@ module.exports = { rules: { 'react/react-in-jsx-scope': 'off', '@typescript-eslint/consistent-type-definitions': ['error', 'type'], + '@typescript-eslint/no-misused-promises': 'off', eqeqeq: ['error', 'always'] }, settings: { diff --git a/react-components/package.json b/react-components/package.json index 2afb4b393f5..dec7489ca12 100644 --- a/react-components/package.json +++ b/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@cognite/reveal-react-components", - "version": "0.9.0", + "version": "0.10.0", "exports": "./dist/index.js", "types": "./dist/index.d.ts", "type": "module", diff --git a/react-components/src/components/CadModelContainer/CadModelContainer.tsx b/react-components/src/components/CadModelContainer/CadModelContainer.tsx index 0add639357a..45e1fcc06d7 100644 --- a/react-components/src/components/CadModelContainer/CadModelContainer.tsx +++ b/react-components/src/components/CadModelContainer/CadModelContainer.tsx @@ -9,7 +9,8 @@ import { TreeIndexNodeCollection, NodeIdNodeCollection, DefaultNodeAppearance, - type NodeCollection + type NodeCollection, + type Cognite3DViewer } from '@cognite/reveal'; import { useReveal } from '../RevealContainer/RevealContext'; import { Matrix4 } from 'three'; @@ -61,16 +62,16 @@ export function CadModelContainer({ }, [modelId, revisionId, geometryFilter]); useEffect(() => { - if (model === undefined || transform === undefined) return; + if (!modelExists(model, viewer) || transform === undefined) return; model.setModelTransformation(transform); }, [transform, model]); useEffect(() => { - if (model === undefined || styleGroups === undefined) return; + if (!modelExists(model, viewer) || styleGroups === undefined) return; const stylingCollections = applyStyling(sdk, model, styleGroups); return () => { - if (model === undefined) return; + if (!modelExists(model, viewer)) return; void stylingCollections.then((nodeCollections) => { nodeCollections.forEach((nodeCollection) => { model.unassignStyledNodeCollection(nodeCollection); @@ -80,12 +81,13 @@ export function CadModelContainer({ }, [styleGroups, model]); useEffect(() => { - if (model === undefined) return; + if (!modelExists(model, viewer)) return; model.setDefaultNodeAppearance(defaultStyle); return () => { - if (model !== undefined) { - model.setDefaultNodeAppearance(DefaultNodeAppearance.Default); + if (!modelExists(model, viewer)) { + return; } + model.setDefaultNodeAppearance(DefaultNodeAppearance.Default); }; }, [defaultStyle, model]); @@ -123,7 +125,7 @@ export function CadModelContainer({ } function removeModel(): void { - if (model === undefined || !viewer.models.includes(model)) return; + if (!modelExists(model, viewer)) return; if (cachedViewerRef !== undefined && !cachedViewerRef.isRevealContainerMountedRef.current) return; @@ -153,3 +155,10 @@ async function applyStyling( } 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/CameraController/CameraController.tsx b/react-components/src/components/CameraController/CameraController.tsx deleted file mode 100644 index 1b92df1bd4d..00000000000 --- a/react-components/src/components/CameraController/CameraController.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/*! - * Copyright 2023 Cognite AS - */ -import { type ReactElement, useEffect, useContext, useRef } from 'react'; -import { useReveal } from '../RevealContainer/RevealContext'; -import { ModelsLoadingStateContext } from '../Reveal3DResources/ModelsLoadingContext'; -import { - DefaultCameraManager, - type CameraControlsOptions, - type CameraState -} from '@cognite/reveal'; - -export type CameraControllerProps = { - initialFitCamera?: FittingStrategy; - cameraControlsOptions?: CameraControlsOptions; -}; - -type FittingStrategy = - | { to: 'cameraState'; state: CameraState } - | { to: 'allModels' } - | { to: 'none' }; - -export function CameraController({ - initialFitCamera, - cameraControlsOptions -}: CameraControllerProps): ReactElement { - const initialCameraSet = useRef(false); - const viewer = useReveal(); - const { modelsAdded } = useContext(ModelsLoadingStateContext); - - const fittingStrategy: Required = initialFitCamera ?? { to: 'allModels' }; - - useEffect(() => { - if (cameraControlsOptions === undefined) return; - - if (!(viewer.cameraManager instanceof DefaultCameraManager)) - throw new Error('CameraControlsOptions can be set only on default CameraManager'); - - viewer.cameraManager.setCameraControlsOptions(cameraControlsOptions); - }, [cameraControlsOptions]); - - useEffect(() => { - if (initialCameraSet.current) return; - if (fittingStrategy.to === 'none') { - initialCameraSet.current = true; - return; - } - if (fittingStrategy.to === 'cameraState') { - viewer.cameraManager.setCameraState(fittingStrategy.state); - initialCameraSet.current = true; - return; - } - if (fittingStrategy.to === 'allModels' && modelsAdded) { - viewer.fitCameraToModels(undefined, undefined, true); - initialCameraSet.current = true; - } - }, [modelsAdded]); - - return <>; -} diff --git a/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx b/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx index 7a10e872c37..96c9b860a7b 100644 --- a/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx +++ b/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx @@ -97,7 +97,7 @@ export function PointCloudContainer({ } function cleanStyling(): void { - if (model === undefined) return; + if (model === undefined || !viewer.models.includes(model)) return; model.setDefaultPointCloudAppearance(DefaultPointCloudAppearance); model.removeAllStyledObjectCollections(); diff --git a/react-components/src/components/Reveal3DResources/ModelsLoadingContext.ts b/react-components/src/components/Reveal3DResources/ModelsLoadingContext.ts deleted file mode 100644 index 6ee0a25325d..00000000000 --- a/react-components/src/components/Reveal3DResources/ModelsLoadingContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -/*! - * Copyright 2023 Cognite AS - */ -import { createContext } from 'react'; - -export type ModelsLoadingState = { - modelsAdded: boolean; - setModelsAdded: (value: boolean) => void; -}; -export const ModelsLoadingStateContext = createContext({ - modelsAdded: false, - setModelsAdded: () => {} -}); diff --git a/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx b/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx index 88eba782bde..17fe396efb0 100644 --- a/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx +++ b/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx @@ -1,9 +1,8 @@ /*! * Copyright 2023 Cognite AS */ -import { useRef, type ReactElement, useContext, useState, useEffect } from 'react'; +import { useRef, type ReactElement, useState, useEffect } from 'react'; import { type Cognite3DViewer, type PointerEventData } from '@cognite/reveal'; -import { ModelsLoadingStateContext } from './ModelsLoadingContext'; import { CadModelContainer, type CadModelStyling } from '../CadModelContainer/CadModelContainer'; import { PointCloudContainer, @@ -27,11 +26,11 @@ export const Reveal3DResources = ({ resources, defaultResourceStyling, instanceStyling, - onNodeClick + onNodeClick, + onResourcesAdded }: Reveal3DResourcesProps): ReactElement => { const [reveal3DModels, setReveal3DModels] = useState([]); - const { setModelsAdded } = useContext(ModelsLoadingStateContext); const viewer = useReveal(); const fdmSdk = useFdmSdk(); const client = useSDK(); @@ -73,8 +72,8 @@ export const Reveal3DResources = ({ const onModelLoaded = (): void => { numModelsLoaded.current += 1; - if (numModelsLoaded.current === resources.length) { - setModelsAdded(true); + if (numModelsLoaded.current === resources.length && onResourcesAdded !== undefined) { + onResourcesAdded(); } }; diff --git a/react-components/src/components/Reveal3DResources/types.ts b/react-components/src/components/Reveal3DResources/types.ts index d6c06ff69f0..3ca1b91216e 100644 --- a/react-components/src/components/Reveal3DResources/types.ts +++ b/react-components/src/components/Reveal3DResources/types.ts @@ -47,4 +47,5 @@ export type Reveal3DResourcesProps = { defaultResourceStyling?: DefaultResourceStyling; instanceStyling?: FdmAssetStylingGroup[]; onNodeClick?: (node: Promise) => void; + onResourcesAdded?: () => void; }; diff --git a/react-components/src/components/RevealContainer/RevealContainer.tsx b/react-components/src/components/RevealContainer/RevealContainer.tsx index bf2401ee122..825214d4308 100644 --- a/react-components/src/components/RevealContainer/RevealContainer.tsx +++ b/react-components/src/components/RevealContainer/RevealContainer.tsx @@ -7,7 +7,6 @@ import { createPortal } from 'react-dom'; import { Cognite3DViewer, type Cognite3DViewerOptions } from '@cognite/reveal'; import { RevealContext } from './RevealContext'; import { type Color } from 'three'; -import { ModelsLoadingStateContext } from '../Reveal3DResources/ModelsLoadingContext'; import { SDKProvider } from './SDKProvider'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; @@ -78,9 +77,7 @@ export function RevealContainer({ <> - - {createPortal(children, viewerDomElement.current)} - + {createPortal(children, viewerDomElement.current)} @@ -104,13 +101,3 @@ export function RevealContainer({ return viewer; } } - -function ModelsLoadingProvider({ children }: { children?: ReactNode }): ReactElement { - const [modelsLoading, setModelsLoading] = useState(false); - return ( - - {children} - - ); -} diff --git a/react-components/src/components/RevealToolbar/FitModelsButton.tsx b/react-components/src/components/RevealToolbar/FitModelsButton.tsx index 242b4e2c662..00d3fbf78f7 100644 --- a/react-components/src/components/RevealToolbar/FitModelsButton.tsx +++ b/react-components/src/components/RevealToolbar/FitModelsButton.tsx @@ -4,21 +4,15 @@ import { type ReactElement, useCallback } from 'react'; -import { Box3 } from 'three'; - -import { useReveal } from '../RevealContainer/RevealContext'; import { Button } from '@cognite/cogs.js'; +import { useCameraNavigation } from '../../hooks/useCameraNavigation'; export const FitModelsButton = (): ReactElement => { - const viewer = useReveal(); + const cameraNavigation = useCameraNavigation(); const updateCamera = useCallback(() => { - const box = new Box3(); - - viewer.models.forEach((model) => box.union(model.getModelBoundingBox())); - - viewer.cameraManager.fitCameraToBoundingBox(box); - }, [viewer, ...viewer.models]); + cameraNavigation.fitCameraToAllModels(); + }, []); return (