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();