Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-components): Move global camera controls to Reveal context #4745

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in hooks folder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, the convention we have been using so far, is that hooks that are sort-of exposing values from a Context, are put together with that Context. That's how this hook was placed before this PR as well, in ViewerContext.ts

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
);
}
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
Loading