Skip to content

Commit

Permalink
feat(react-components): Move global camera controls to Reveal context (
Browse files Browse the repository at this point in the history
…#4745)

* feat(react-components): Move global camera controls to Reveal context
  • Loading branch information
haakonflatval-cognite authored Sep 9, 2024
1 parent e751fbb commit bcb8cff
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 87 deletions.
21 changes: 0 additions & 21 deletions react-components/src/components/RevealCanvas/ViewerContext.ts

This file was deleted.

54 changes: 54 additions & 0 deletions react-components/src/components/RevealCanvas/ViewerContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*!
* Copyright 2023 Cognite AS
*/
import { type Cognite3DViewer } from '@cognite/reveal';
import { createContext, type ReactElement, type ReactNode, useContext } from 'react';
import { type RevealRenderTarget } from '../../architecture/base/renderTarget/RevealRenderTarget';
import { type CameraStateParameters, useCameraStateControl } from './hooks/useCameraStateControl';

const ViewerContext = createContext<RevealRenderTarget | null>(null);

export type ViewerContextProviderProps = {
cameraState?: CameraStateParameters;
setCameraState?: (cameraState?: CameraStateParameters) => void;
value: RevealRenderTarget | null;
children: ReactNode;
};

export const ViewerContextProvider = ({
cameraState,
setCameraState,
value,
children
}: ViewerContextProviderProps): ReactElement => {
return (
<ViewerContext.Provider value={value}>
<ViewerControls cameraState={cameraState} setCameraState={setCameraState} />
{children}
</ViewerContext.Provider>
);
};

const ViewerControls = ({
cameraState,
setCameraState
}: {
cameraState?: CameraStateParameters;
setCameraState?: (cameraState?: CameraStateParameters) => void;
}): ReactNode => {
useCameraStateControl(cameraState, setCameraState);
return null;
};

export const useReveal = (): Cognite3DViewer => {
const renderTarget = useRenderTarget();
return renderTarget.viewer;
};

export const useRenderTarget = (): RevealRenderTarget => {
const renderTarget = useContext(ViewerContext);
if (renderTarget === null) {
throw new Error('useRenderTarget must be used within a ViewerProvider');
}
return renderTarget;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*!
* Copyright 2023 Cognite AS
*/

import { type MutableRefObject, useEffect, useRef } from 'react';
import { useReveal } from '../ViewerContext';
import { type CameraState } from '@cognite/reveal';

export type CameraStateParameters = Omit<Required<CameraState>, 'rotation'>;

export const useCameraStateControl = (
externalCameraState?: CameraStateParameters,
setCameraState?: (cameraState?: CameraStateParameters) => void
): void => {
const lastSetExternalState = useRef<CameraStateParameters | undefined>(
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<CameraStateParameters | undefined>
): void => {
const reveal = useReveal();

useEffect(() => {
if (
externalCameraState === undefined ||
isCameraStatesEqual(externalCameraState, lastSetExternalState.current)
) {
return;
}

reveal.cameraManager.setCameraState(externalCameraState);
}, [externalCameraState]);
};

const useSetExternalCameraStateOnCameraMove = (
setCameraState: ((cameraState?: CameraStateParameters) => void) | undefined,
externalCameraState: CameraStateParameters | undefined,
lastSetExternalState: MutableRefObject<CameraStateParameters | undefined>
): void => {
const reveal = useReveal();
useEffect(() => {
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', updateStateOnCameraStop);
};
}, [externalCameraState, setCameraState, lastSetExternalState]);
};

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
);
}
12 changes: 9 additions & 3 deletions react-components/src/components/RevealContext/RevealContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,13 +17,16 @@ import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext';
import { Image360AnnotationCacheProvider } from '../CacheProvider/Image360AnnotationCacheProvider';
import { RevealRenderTarget } from '../../architecture/base/renderTarget/RevealRenderTarget';
import { LoadedSceneProvider } from '../SceneContainer/LoadedSceneContext';
import { type CameraStateParameters } from '../RevealCanvas/hooks/useCameraStateControl';

export type RevealContextProps = {
color?: Color;
sdk: CogniteClient;
appLanguage?: string;
children?: ReactNode;
useCoreDm?: boolean;
cameraState?: CameraStateParameters;
setCameraState?: (cameraState?: CameraStateParameters) => void;
viewerOptions?: Pick<
Cognite3DViewerOptions,
| 'antiAliasingHint'
Expand Down Expand Up @@ -51,7 +54,10 @@ export const RevealContext = (props: RevealContextProps): ReactElement => {
<QueryClientProvider client={queryClient}>
<I18nContextProvider appLanguage={props.appLanguage}>
<LoadedSceneProvider>
<ViewerContext.Provider value={viewer}>
<ViewerContextProvider
cameraState={props.cameraState}
setCameraState={props.setCameraState}
value={viewer}>
<NodeCacheProvider>
<AssetMappingAndNode3DCacheProvider>
<PointCloudAnnotationCacheProvider>
Expand All @@ -63,7 +69,7 @@ export const RevealContext = (props: RevealContextProps): ReactElement => {
</PointCloudAnnotationCacheProvider>
</AssetMappingAndNode3DCacheProvider>
</NodeCacheProvider>
</ViewerContext.Provider>
</ViewerContextProvider>
</LoadedSceneProvider>
</I18nContextProvider>
</QueryClientProvider>
Expand Down

This file was deleted.

6 changes: 1 addition & 5 deletions react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 './higher-order-components/withCameraStateUrlParam';
export { type CameraStateParameters } from './components/RevealCanvas/hooks/useCameraStateControl';
// Types
export {
type PointCloudModelStyling,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, expect, test, vi, beforeEach, beforeAll, afterAll } from 'vitest';

import { renderHook } from '@testing-library/react';

import { viewerMock } from '../../../fixtures/viewer';
import {
CameraStateParameters,
useCameraStateControl
} from '../../../../../src/components/RevealCanvas/hooks/useCameraStateControl';
import { Vector3 } from 'three';
import { cameraManagerGlobalCameraEvents } from '../../../fixtures/cameraManager';

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

cameraManagerGlobalCameraEvents.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(
({ position }: { position: Vector3 }) =>
useCameraStateControl({ position: position.clone(), target: new Vector3(1, 1, 1) }, setter),
{ initialProps: { position: new Vector3(0, 0, 0) } }
);

vi.runAllTimers();
rerender({ position: new Vector3(1, 0, 0) });

vi.runAllTimers();
expect(setter).not.toBeCalled();

cameraManagerGlobalCameraEvents.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)
});

vi.runAllTimers();

rerender();
vi.runAllTimers();

expect(setter).toHaveBeenCalled();
});
});
Loading

0 comments on commit bcb8cff

Please sign in to comment.