From 0d89ab4bedb9d2324e3ecb440b54cb03120feb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Fri, 6 Sep 2024 16:50:18 +0200 Subject: [PATCH 1/6] feat(react-components): Move global camera controls to Reveal context --- .../{ViewerContext.ts => ViewerContext.tsx} | 16 +++++- .../hooks/useCameraStateControl.tsx | 48 ++++++++++++++++ .../RevealContext/RevealContext.tsx | 8 ++- .../withCameraStateUrlParam.tsx | 56 ------------------- react-components/src/index.ts | 2 +- 5 files changed, 71 insertions(+), 59 deletions(-) rename react-components/src/components/RevealCanvas/{ViewerContext.ts => ViewerContext.tsx} (54%) create mode 100644 react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx delete mode 100644 react-components/src/higher-order-components/withCameraStateUrlParam.tsx diff --git a/react-components/src/components/RevealCanvas/ViewerContext.ts b/react-components/src/components/RevealCanvas/ViewerContext.tsx similarity index 54% rename from react-components/src/components/RevealCanvas/ViewerContext.ts rename to react-components/src/components/RevealCanvas/ViewerContext.tsx index 77ce6fee6f8..0b148f50bfd 100644 --- a/react-components/src/components/RevealCanvas/ViewerContext.ts +++ b/react-components/src/components/RevealCanvas/ViewerContext.tsx @@ -4,8 +4,22 @@ import { type Cognite3DViewer } from '@cognite/reveal'; import { createContext, useContext } from 'react'; import { type RevealRenderTarget } from '../../architecture/base/renderTarget/RevealRenderTarget'; +import { CameraStateParameters, useCameraStateControl } from './hooks/useCameraStateControl'; -export const ViewerContext = createContext(null); +const ViewerContext = createContext(null); + +export const ViewerContextProvider = ({ + cameraState, + setCameraState, + value +}: { + cameraState?: CameraStateParameters; + setCameraState?: (cameraState?: CameraStateParameters) => void; + value: RevealRenderTarget | null; +}) => { + useCameraStateControl(cameraState, setCameraState); + return ; +}; export const useReveal = (): Cognite3DViewer => { const renderTarget = useRenderTarget(); diff --git a/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx b/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx new file mode 100644 index 00000000000..5caa3eba712 --- /dev/null +++ b/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx @@ -0,0 +1,48 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { useEffect } from 'react'; +import { useReveal } from '../../..'; +import { type CameraState } from '@cognite/reveal'; + +export type CameraStateParameters = Omit, 'rotation'>; + +export const useCameraStateControl = ( + externalCameraState?: CameraStateParameters, + setCameraState?: (cameraState?: CameraStateParameters) => void +): void => { + const reveal = useReveal(); + + const setUrlParamOnCameraStop = (): void => { + const currentCameraManagerState = reveal.cameraManager.getCameraState(); + if ( + externalCameraState !== undefined && + !hasCameraStateChanged(externalCameraState, currentCameraManagerState) + ) { + return; + } + + setCameraState?.(currentCameraManagerState); + }; + + useEffect(() => { + reveal.cameraManager.on('cameraStop', setUrlParamOnCameraStop); + return () => { + reveal.cameraManager.off('cameraStop', setUrlParamOnCameraStop); + }; + }, []); +}; + +function hasCameraStateChanged( + previous: CameraStateParameters, + current: CameraStateParameters +): boolean { + const epsilon = 0.001; + const { position: previousPosition, target: previousTarget } = previous; + const { position: currentPosition, target: currentTarget } = current; + return ( + previousPosition.distanceToSquared(currentPosition) > epsilon || + previousTarget.distanceToSquared(currentTarget) > epsilon + ); +} diff --git a/react-components/src/components/RevealContext/RevealContext.tsx b/react-components/src/components/RevealContext/RevealContext.tsx index 680eb100024..fe590b4e548 100644 --- a/react-components/src/components/RevealContext/RevealContext.tsx +++ b/react-components/src/components/RevealContext/RevealContext.tsx @@ -17,6 +17,7 @@ import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; import { Image360AnnotationCacheProvider } from '../CacheProvider/Image360AnnotationCacheProvider'; import { RevealRenderTarget } from '../../architecture/base/renderTarget/RevealRenderTarget'; import { LoadedSceneProvider } from '../SceneContainer/LoadedSceneContext'; +import { CameraStateParameters } from '../RevealCanvas/hooks/useCameraStateControl'; export type RevealContextProps = { color?: Color; @@ -24,6 +25,8 @@ export type RevealContextProps = { appLanguage?: string; children?: ReactNode; useCoreDm?: boolean; + cameraState?: CameraStateParameters; + setCameraState?: (cameraState?: CameraStateParameters) => void; viewerOptions?: Pick< Cognite3DViewerOptions, | 'antiAliasingHint' @@ -51,7 +54,10 @@ export const RevealContext = (props: RevealContextProps): ReactElement => { - + diff --git a/react-components/src/higher-order-components/withCameraStateUrlParam.tsx b/react-components/src/higher-order-components/withCameraStateUrlParam.tsx deleted file mode 100644 index 36730c4761b..00000000000 --- a/react-components/src/higher-order-components/withCameraStateUrlParam.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/*! - * Copyright 2023 Cognite AS - */ - -import { useEffect, type ReactElement, type FunctionComponent } from 'react'; -import { useReveal } from '..'; -import { type CameraState } from '@cognite/reveal'; - -export type CameraStateParameters = Omit, 'rotation'>; -export type CameraStateProps = { - cameraState: CameraStateParameters | undefined; - setCameraState: (value: CameraStateParameters | undefined) => void; -}; - -export function withCameraStateControl( - Component: FunctionComponent -): FunctionComponent { - return function CameraStateUrlParam(props: T & CameraStateProps): ReactElement { - const reveal = useReveal(); - const externalCameraState = props.cameraState; - - const setUrlParamOnCameraStop = (): void => { - const currentCameraManagerState = reveal.cameraManager.getCameraState(); - if ( - externalCameraState !== undefined && - !hasCameraStateChanged(externalCameraState, currentCameraManagerState) - ) { - return; - } - - props.setCameraState(currentCameraManagerState); - }; - - useEffect(() => { - reveal.cameraManager.on('cameraStop', setUrlParamOnCameraStop); - return () => { - reveal.cameraManager.off('cameraStop', setUrlParamOnCameraStop); - }; - }, []); - - return ; - }; - - function hasCameraStateChanged( - previous: CameraStateParameters, - current: CameraStateParameters - ): boolean { - const epsilon = 0.001; - const { position: previousPosition, target: previousTarget } = previous; - const { position: currentPosition, target: currentTarget } = current; - return ( - previousPosition.distanceToSquared(currentPosition) > epsilon || - previousTarget.distanceToSquared(currentTarget) > epsilon - ); - } -} diff --git a/react-components/src/index.ts b/react-components/src/index.ts index c9e847d8e2a..abcc3b69044 100644 --- a/react-components/src/index.ts +++ b/react-components/src/index.ts @@ -86,7 +86,7 @@ export { withCameraStateControl, type CameraStateParameters, type CameraStateProps -} from './higher-order-components/withCameraStateUrlParam'; +} from './components/RevealCanvas/hooks/useCameraStateControl'; // Types export { type PointCloudModelStyling, From ac6abe61856f4c11c9270490f0a9ee0160f59661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 9 Sep 2024 10:01:10 +0200 Subject: [PATCH 2/6] chore: implement state updates properly --- .../components/RevealCanvas/ViewerContext.tsx | 23 +++++- .../hooks/useCameraStateControl.tsx | 77 +++++++++++++++---- .../RevealContext/RevealContext.tsx | 6 +- react-components/src/index.ts | 6 +- 4 files changed, 87 insertions(+), 25 deletions(-) diff --git a/react-components/src/components/RevealCanvas/ViewerContext.tsx b/react-components/src/components/RevealCanvas/ViewerContext.tsx index 0b148f50bfd..14576b94f03 100644 --- a/react-components/src/components/RevealCanvas/ViewerContext.tsx +++ b/react-components/src/components/RevealCanvas/ViewerContext.tsx @@ -2,7 +2,7 @@ * Copyright 2023 Cognite AS */ import { type Cognite3DViewer } from '@cognite/reveal'; -import { createContext, useContext } from 'react'; +import { createContext, ReactNode, useContext } from 'react'; import { type RevealRenderTarget } from '../../architecture/base/renderTarget/RevealRenderTarget'; import { CameraStateParameters, useCameraStateControl } from './hooks/useCameraStateControl'; @@ -11,14 +11,31 @@ const ViewerContext = createContext(null); export const ViewerContextProvider = ({ cameraState, setCameraState, - value + value, + children }: { cameraState?: CameraStateParameters; setCameraState?: (cameraState?: CameraStateParameters) => void; value: RevealRenderTarget | null; + children: ReactNode; +}) => { + return ( + + + {children} + + ); +}; + +const ViewerControls = ({ + cameraState, + setCameraState +}: { + cameraState?: CameraStateParameters; + setCameraState?: (cameraState?: CameraStateParameters) => void; }) => { useCameraStateControl(cameraState, setCameraState); - return ; + return <>; }; export const useReveal = (): Cognite3DViewer => { diff --git a/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx b/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx index 5caa3eba712..0b253e7ec2d 100644 --- a/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx +++ b/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx @@ -2,7 +2,7 @@ * Copyright 2023 Cognite AS */ -import { useEffect } from 'react'; +import { MutableRefObject, useEffect, useRef } from 'react'; import { useReveal } from '../../..'; import { type CameraState } from '@cognite/reveal'; @@ -12,37 +12,86 @@ export const useCameraStateControl = ( externalCameraState?: CameraStateParameters, setCameraState?: (cameraState?: CameraStateParameters) => void ): void => { + const lastSetExternalState = useRef( + externalCameraState === undefined + ? undefined + : { + position: externalCameraState.position.clone(), + target: externalCameraState.target.clone() + } + ); + + useSetInternalCameraStateOnExternalUpdate(externalCameraState, lastSetExternalState); + + useSetExternalCameraStateOnCameraMove(setCameraState, externalCameraState, lastSetExternalState); +}; + +const useSetInternalCameraStateOnExternalUpdate = ( + externalCameraState: CameraStateParameters | undefined, + lastSetExternalState: MutableRefObject +) => { const reveal = useReveal(); - const setUrlParamOnCameraStop = (): void => { - const currentCameraManagerState = reveal.cameraManager.getCameraState(); + useEffect(() => { if ( - externalCameraState !== undefined && - !hasCameraStateChanged(externalCameraState, currentCameraManagerState) + externalCameraState === undefined || + isCameraStatesEqual(externalCameraState, lastSetExternalState.current) ) { return; } - setCameraState?.(currentCameraManagerState); - }; + reveal.cameraManager.setCameraState(externalCameraState); + }, [externalCameraState]); +}; +const useSetExternalCameraStateOnCameraMove = ( + setCameraState: ((cameraState?: CameraStateParameters) => void) | undefined, + externalCameraState: CameraStateParameters | undefined, + lastSetExternalState: MutableRefObject +) => { + const reveal = useReveal(); useEffect(() => { - reveal.cameraManager.on('cameraStop', setUrlParamOnCameraStop); + const updateStateOnCameraStop = (): void => { + const currentCameraManagerState = reveal.cameraManager.getCameraState(); + if ( + externalCameraState !== undefined && + isCameraStatesEqual(externalCameraState, currentCameraManagerState) + ) { + return; + } + + lastSetExternalState.current = { + position: currentCameraManagerState.position.clone(), + target: currentCameraManagerState.target.clone() + }; + + setCameraState?.(currentCameraManagerState); + }; + + reveal.cameraManager.on('cameraStop', updateStateOnCameraStop); return () => { - reveal.cameraManager.off('cameraStop', setUrlParamOnCameraStop); + reveal.cameraManager.off('cameraStop', updateStateOnCameraStop); }; }, []); }; -function hasCameraStateChanged( - previous: CameraStateParameters, - current: CameraStateParameters +function isCameraStatesEqual( + previous: CameraStateParameters | undefined, + current: CameraStateParameters | undefined ): boolean { + if (previous === undefined && current === undefined) { + return true; + } + + if (previous === undefined || current === undefined) { + return false; + } + const epsilon = 0.001; const { position: previousPosition, target: previousTarget } = previous; const { position: currentPosition, target: currentTarget } = current; return ( - previousPosition.distanceToSquared(currentPosition) > epsilon || - previousTarget.distanceToSquared(currentTarget) > epsilon + previousPosition.distanceToSquared(currentPosition) <= epsilon && + previousTarget.distanceToSquared(currentTarget) <= epsilon ); } diff --git a/react-components/src/components/RevealContext/RevealContext.tsx b/react-components/src/components/RevealContext/RevealContext.tsx index fe590b4e548..7de6244d3d3 100644 --- a/react-components/src/components/RevealContext/RevealContext.tsx +++ b/react-components/src/components/RevealContext/RevealContext.tsx @@ -6,7 +6,7 @@ import { type CogniteClient } from '@cognite/sdk/dist/src'; import { type ReactNode, useEffect, useMemo, useState, type ReactElement } from 'react'; import { type Color } from 'three'; import { I18nContextProvider } from '../i18n/I18n'; -import { ViewerContext } from '../RevealCanvas/ViewerContext'; +import { ViewerContextProvider } from '../RevealCanvas/ViewerContext'; import { NodeCacheProvider } from '../CacheProvider/NodeCacheProvider'; import { AssetMappingAndNode3DCacheProvider } from '../CacheProvider/AssetMappingAndNode3DCacheProvider'; import { PointCloudAnnotationCacheProvider } from '../CacheProvider/PointCloudAnnotationCacheProvider'; @@ -54,7 +54,7 @@ export const RevealContext = (props: RevealContextProps): ReactElement => { - @@ -69,7 +69,7 @@ export const RevealContext = (props: RevealContextProps): ReactElement => { - + diff --git a/react-components/src/index.ts b/react-components/src/index.ts index abcc3b69044..370001fd6d3 100644 --- a/react-components/src/index.ts +++ b/react-components/src/index.ts @@ -82,11 +82,7 @@ export { useModelsForInstanceQuery } from './query/useModelsForInstanceQuery'; // Higher order components export { withSuppressRevealEvents } from './higher-order-components/withSuppressRevealEvents'; -export { - withCameraStateControl, - type CameraStateParameters, - type CameraStateProps -} from './components/RevealCanvas/hooks/useCameraStateControl'; +export { type CameraStateParameters } from './components/RevealCanvas/hooks/useCameraStateControl'; // Types export { type PointCloudModelStyling, From fec00d08706501b055c5a36e301a0beb54315c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 9 Sep 2024 14:13:40 +0200 Subject: [PATCH 3/6] test: add test file --- .../hooks/useCameraStateControl.test.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 react-components/tests/unit-tests/components/RevealCanvas/hooks/useCameraStateControl.test.ts diff --git a/react-components/tests/unit-tests/components/RevealCanvas/hooks/useCameraStateControl.test.ts b/react-components/tests/unit-tests/components/RevealCanvas/hooks/useCameraStateControl.test.ts new file mode 100644 index 00000000000..06293a43cd0 --- /dev/null +++ b/react-components/tests/unit-tests/components/RevealCanvas/hooks/useCameraStateControl.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, test, vi, beforeEach, beforeAll, afterAll, Mock } from 'vitest'; + +import { act, renderHook } from '@testing-library/react'; + +import { + viewerMock, + viewerCameraOn, + viewerCameraOff, + viewerGlobalCameraEvents +} from '../../../fixtures/viewer'; +import { CameraManagerEventType, CameraStopDelegate } from '@cognite/reveal'; +import { + CameraStateParameters, + useCameraStateControl +} from '../../../../../src/components/RevealCanvas/hooks/useCameraStateControl'; +import { remove } from 'lodash'; +import { Vector3 } from 'three'; + +vi.mock('../../../../../src/components/RevealCanvas/ViewerContext', () => ({ + useReveal: () => viewerMock +})); + +describe(useCameraStateControl.name, () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + beforeAll(() => { + vi.useFakeTimers(); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + test('does nothing when inputs are undefined', () => { + const { rerender } = renderHook(() => useCameraStateControl()); + + vi.runAllTimers(); + rerender(); + vi.runAllTimers(); + + viewerGlobalCameraEvents.cameraStop.forEach((mockCallback) => + expect(mockCallback).not.toBeCalled() + ); + }); + + test('does nothing if external camera state is undefined', () => { + const setter = vi.fn(); + const { rerender } = renderHook(() => useCameraStateControl(undefined, setter)); + + vi.runAllTimers(); + rerender(); + vi.runAllTimers(); + + expect(setter).not.toBeCalled(); + }); + + test('calls internal cameraStop delegates but not external setter on applying external camera state', () => { + const setter = vi.fn<[CameraStateParameters | undefined], void>(); + + const { rerender } = renderHook(() => + useCameraStateControl( + { position: new Vector3(0, 0, 0), target: new Vector3(1, 1, 1) }, + setter + ) + ); + + vi.runAllTimers(); + renderHook(() => + useCameraStateControl( + { position: new Vector3(1, 0, 0), target: new Vector3(1, 1, 1) }, + setter + ) + ); + + act(() => { + vi.advanceTimersByTime(1000); + vi.runAllTimers(); + }); + + vi.runAllTimers(); + expect(setter).not.toBeCalled(); + + viewerGlobalCameraEvents.cameraStop.forEach((mockCallback) => + expect(mockCallback).toBeCalledTimes(1) + ); + }); + + test('provided setter is called after updating camera state internally', () => { + const setter = vi.fn<[CameraStateParameters | undefined], void>(); + + const { rerender } = renderHook(() => + useCameraStateControl( + { position: new Vector3(0, 0, 0), target: new Vector3(1, 1, 1) }, + setter + ) + ); + + vi.runAllTimers(); + + viewerMock.cameraManager.setCameraState({ + position: new Vector3(1, 0, 0), + target: new Vector3(1, 1, 1) + }); + + rerender(); + vi.runAllTimers(); + + expect(setter).toHaveBeenCalled(); + }); +}); From 3e3d16c5260e64a64c05f393647d9a99aa281d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 9 Sep 2024 15:58:32 +0200 Subject: [PATCH 4/6] chore: add changes related to test --- .../components/RevealCanvas/ViewerContext.tsx | 10 ++--- ...teControl.tsx => useCameraStateControl.ts} | 9 ++-- .../RevealContext/RevealContext.tsx | 2 +- .../hooks/useCameraStateControl.test.ts | 41 ++++++----------- .../unit-tests/fixtures/cameraManager.ts | 44 +++++++++++++++++++ .../tests/unit-tests/fixtures/viewer.ts | 14 +++++- 6 files changed, 80 insertions(+), 40 deletions(-) rename react-components/src/components/RevealCanvas/hooks/{useCameraStateControl.tsx => useCameraStateControl.ts} (94%) create mode 100644 react-components/tests/unit-tests/fixtures/cameraManager.ts diff --git a/react-components/src/components/RevealCanvas/ViewerContext.tsx b/react-components/src/components/RevealCanvas/ViewerContext.tsx index 14576b94f03..985d4ea677f 100644 --- a/react-components/src/components/RevealCanvas/ViewerContext.tsx +++ b/react-components/src/components/RevealCanvas/ViewerContext.tsx @@ -2,9 +2,9 @@ * Copyright 2023 Cognite AS */ import { type Cognite3DViewer } from '@cognite/reveal'; -import { createContext, ReactNode, useContext } from 'react'; +import { createContext, type ReactElement, type ReactNode, useContext } from 'react'; import { type RevealRenderTarget } from '../../architecture/base/renderTarget/RevealRenderTarget'; -import { CameraStateParameters, useCameraStateControl } from './hooks/useCameraStateControl'; +import { type CameraStateParameters, useCameraStateControl } from './hooks/useCameraStateControl'; const ViewerContext = createContext(null); @@ -18,7 +18,7 @@ export const ViewerContextProvider = ({ setCameraState?: (cameraState?: CameraStateParameters) => void; value: RevealRenderTarget | null; children: ReactNode; -}) => { +}): ReactElement => { return ( @@ -33,9 +33,9 @@ const ViewerControls = ({ }: { cameraState?: CameraStateParameters; setCameraState?: (cameraState?: CameraStateParameters) => void; -}) => { +}): ReactNode => { useCameraStateControl(cameraState, setCameraState); - return <>; + return null; }; export const useReveal = (): Cognite3DViewer => { diff --git a/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx b/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.ts similarity index 94% rename from react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx rename to react-components/src/components/RevealCanvas/hooks/useCameraStateControl.ts index 0b253e7ec2d..3155f6f06a5 100644 --- a/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.tsx +++ b/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.ts @@ -2,7 +2,7 @@ * Copyright 2023 Cognite AS */ -import { MutableRefObject, useEffect, useRef } from 'react'; +import { type MutableRefObject, useEffect, useRef } from 'react'; import { useReveal } from '../../..'; import { type CameraState } from '@cognite/reveal'; @@ -29,7 +29,7 @@ export const useCameraStateControl = ( const useSetInternalCameraStateOnExternalUpdate = ( externalCameraState: CameraStateParameters | undefined, lastSetExternalState: MutableRefObject -) => { +): void => { const reveal = useReveal(); useEffect(() => { @@ -48,11 +48,12 @@ const useSetExternalCameraStateOnCameraMove = ( setCameraState: ((cameraState?: CameraStateParameters) => void) | undefined, externalCameraState: CameraStateParameters | undefined, lastSetExternalState: MutableRefObject -) => { +): void => { const reveal = useReveal(); useEffect(() => { const updateStateOnCameraStop = (): void => { const currentCameraManagerState = reveal.cameraManager.getCameraState(); + if ( externalCameraState !== undefined && isCameraStatesEqual(externalCameraState, currentCameraManagerState) @@ -72,7 +73,7 @@ const useSetExternalCameraStateOnCameraMove = ( return () => { reveal.cameraManager.off('cameraStop', updateStateOnCameraStop); }; - }, []); + }, [externalCameraState, setCameraState, lastSetExternalState]); }; function isCameraStatesEqual( diff --git a/react-components/src/components/RevealContext/RevealContext.tsx b/react-components/src/components/RevealContext/RevealContext.tsx index 7de6244d3d3..4733eac8f9e 100644 --- a/react-components/src/components/RevealContext/RevealContext.tsx +++ b/react-components/src/components/RevealContext/RevealContext.tsx @@ -17,7 +17,7 @@ import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; import { Image360AnnotationCacheProvider } from '../CacheProvider/Image360AnnotationCacheProvider'; import { RevealRenderTarget } from '../../architecture/base/renderTarget/RevealRenderTarget'; import { LoadedSceneProvider } from '../SceneContainer/LoadedSceneContext'; -import { CameraStateParameters } from '../RevealCanvas/hooks/useCameraStateControl'; +import { type CameraStateParameters } from '../RevealCanvas/hooks/useCameraStateControl'; export type RevealContextProps = { color?: Color; diff --git a/react-components/tests/unit-tests/components/RevealCanvas/hooks/useCameraStateControl.test.ts b/react-components/tests/unit-tests/components/RevealCanvas/hooks/useCameraStateControl.test.ts index 06293a43cd0..00f1819d587 100644 --- a/react-components/tests/unit-tests/components/RevealCanvas/hooks/useCameraStateControl.test.ts +++ b/react-components/tests/unit-tests/components/RevealCanvas/hooks/useCameraStateControl.test.ts @@ -1,20 +1,14 @@ -import { describe, expect, test, vi, beforeEach, beforeAll, afterAll, Mock } from 'vitest'; +import { describe, expect, test, vi, beforeEach, beforeAll, afterAll } from 'vitest'; -import { act, renderHook } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; -import { - viewerMock, - viewerCameraOn, - viewerCameraOff, - viewerGlobalCameraEvents -} from '../../../fixtures/viewer'; -import { CameraManagerEventType, CameraStopDelegate } from '@cognite/reveal'; +import { viewerMock } from '../../../fixtures/viewer'; import { CameraStateParameters, useCameraStateControl } from '../../../../../src/components/RevealCanvas/hooks/useCameraStateControl'; -import { remove } from 'lodash'; import { Vector3 } from 'three'; +import { cameraManagerGlobalCameraEvents } from '../../../fixtures/cameraManager'; vi.mock('../../../../../src/components/RevealCanvas/ViewerContext', () => ({ useReveal: () => viewerMock @@ -40,7 +34,7 @@ describe(useCameraStateControl.name, () => { rerender(); vi.runAllTimers(); - viewerGlobalCameraEvents.cameraStop.forEach((mockCallback) => + cameraManagerGlobalCameraEvents.cameraStop.forEach((mockCallback) => expect(mockCallback).not.toBeCalled() ); }); @@ -59,30 +53,19 @@ describe(useCameraStateControl.name, () => { test('calls internal cameraStop delegates but not external setter on applying external camera state', () => { const setter = vi.fn<[CameraStateParameters | undefined], void>(); - const { rerender } = renderHook(() => - useCameraStateControl( - { position: new Vector3(0, 0, 0), target: new Vector3(1, 1, 1) }, - setter - ) + const { rerender } = renderHook( + ({ position }: { position: Vector3 }) => + useCameraStateControl({ position: position.clone(), target: new Vector3(1, 1, 1) }, setter), + { initialProps: { position: new Vector3(0, 0, 0) } } ); vi.runAllTimers(); - renderHook(() => - useCameraStateControl( - { position: new Vector3(1, 0, 0), target: new Vector3(1, 1, 1) }, - setter - ) - ); - - act(() => { - vi.advanceTimersByTime(1000); - vi.runAllTimers(); - }); + rerender({ position: new Vector3(1, 0, 0) }); vi.runAllTimers(); expect(setter).not.toBeCalled(); - viewerGlobalCameraEvents.cameraStop.forEach((mockCallback) => + cameraManagerGlobalCameraEvents.cameraStop.forEach((mockCallback) => expect(mockCallback).toBeCalledTimes(1) ); }); @@ -104,6 +87,8 @@ describe(useCameraStateControl.name, () => { target: new Vector3(1, 1, 1) }); + vi.runAllTimers(); + rerender(); vi.runAllTimers(); diff --git a/react-components/tests/unit-tests/fixtures/cameraManager.ts b/react-components/tests/unit-tests/fixtures/cameraManager.ts new file mode 100644 index 00000000000..7612a8c9774 --- /dev/null +++ b/react-components/tests/unit-tests/fixtures/cameraManager.ts @@ -0,0 +1,44 @@ +import { CameraManager, CameraManagerEventType, CameraState } from '@cognite/reveal'; +import { remove } from 'lodash'; +import { Mock } from 'moq.ts'; +import { Vector3 } from 'three'; + +import { vi, Mock as viMock } from 'vitest'; + +export const cameraManagerGlobalCameraEvents: Record< + CameraManagerEventType, + viMock<[Vector3, Vector3], void>[] +> = { + cameraChange: [], + cameraStop: [] +}; + +const cameraManagerGlobalCurrentCameraState: CameraState = {}; + +export const cameraManagerMock = new Mock() + .setup((p) => p.on) + .returns((eventType, callback) => + cameraManagerGlobalCameraEvents[eventType].push(vi.fn().mockImplementation(callback)) + ) + .setup((p) => p.off) + .returns((eventType, callback) => + remove( + cameraManagerGlobalCameraEvents[eventType], + (element) => element.getMockImplementation() === callback + ) + ) + .setup((p) => p.setCameraState) + .returns(({ position, target }) => { + cameraManagerGlobalCurrentCameraState.position = position; + cameraManagerGlobalCurrentCameraState.target = target; + setTimeout( + () => + cameraManagerGlobalCameraEvents.cameraStop.forEach((callback) => + callback(position!, target!) + ), + 50 + ); + }) + .setup((p) => p.getCameraState()) + .returns(cameraManagerGlobalCurrentCameraState as Required) + .object(); diff --git a/react-components/tests/unit-tests/fixtures/viewer.ts b/react-components/tests/unit-tests/fixtures/viewer.ts index c86f1401729..5c9f797f883 100644 --- a/react-components/tests/unit-tests/fixtures/viewer.ts +++ b/react-components/tests/unit-tests/fixtures/viewer.ts @@ -1,7 +1,15 @@ -import { vi } from 'vitest'; +import { vi, Mock as viMock } from 'vitest'; -import { Cognite3DViewer, CogniteModel, Image360Collection } from '@cognite/reveal'; +import { + CameraManagerEventType, + Cognite3DViewer, + CogniteModel, + Image360Collection +} from '@cognite/reveal'; import { Mock, It } from 'moq.ts'; +import { Vector3 } from 'three'; +import { remove } from 'lodash'; +import { cameraManagerMock } from './cameraManager'; const domElement = document.createElement('div').appendChild(document.createElement('canvas')); @@ -20,4 +28,6 @@ export const viewerMock = new Mock() .callback(viewerImage360CollectionsMock) .setup((p) => p.removeModel) .returns(viewerRemoveModelsMock) + .setup((p) => p.cameraManager) + .returns(cameraManagerMock) .object(); From 9b5a83e6326b97d031f12eaf37fc5b82f53ffd51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 9 Sep 2024 16:28:01 +0200 Subject: [PATCH 5/6] chore: move out viewer provider props into its own type --- .../src/components/RevealCanvas/ViewerContext.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/react-components/src/components/RevealCanvas/ViewerContext.tsx b/react-components/src/components/RevealCanvas/ViewerContext.tsx index 985d4ea677f..9a7f61ddce9 100644 --- a/react-components/src/components/RevealCanvas/ViewerContext.tsx +++ b/react-components/src/components/RevealCanvas/ViewerContext.tsx @@ -8,17 +8,19 @@ import { type CameraStateParameters, useCameraStateControl } from './hooks/useCa const ViewerContext = createContext(null); +export type ViewerContextProviderProps = { + cameraState?: CameraStateParameters; + setCameraState?: (cameraState?: CameraStateParameters) => void; + value: RevealRenderTarget | null; + children: ReactNode; +}; + export const ViewerContextProvider = ({ cameraState, setCameraState, value, children -}: { - cameraState?: CameraStateParameters; - setCameraState?: (cameraState?: CameraStateParameters) => void; - value: RevealRenderTarget | null; - children: ReactNode; -}): ReactElement => { +}: ViewerContextProviderProps): ReactElement => { return ( From 80e4ddf5c243fda69834a0df63ea5d6270b2e017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 9 Sep 2024 16:33:48 +0200 Subject: [PATCH 6/6] chore: improve path --- .../src/components/RevealCanvas/hooks/useCameraStateControl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.ts b/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.ts index 3155f6f06a5..30ed98d3dc4 100644 --- a/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.ts +++ b/react-components/src/components/RevealCanvas/hooks/useCameraStateControl.ts @@ -3,7 +3,7 @@ */ import { type MutableRefObject, useEffect, useRef } from 'react'; -import { useReveal } from '../../..'; +import { useReveal } from '../ViewerContext'; import { type CameraState } from '@cognite/reveal'; export type CameraStateParameters = Omit, 'rotation'>;