Skip to content

Commit

Permalink
feat: add a higher order component for storing camera state to URL (#…
Browse files Browse the repository at this point in the history
…3682)

* feat: add HOC for adding camerastate url param

* feat: add HOC for adding camerastate url param

* feat: add storing / restoring camera state from URL

* fix: only update url if camera state has changed

* fix: linting

---------

Co-authored-by: cognite-bulldozer[bot] <51074376+cognite-bulldozer[bot]@users.noreply.github.com>
  • Loading branch information
christjt and cognite-bulldozer[bot] authored Sep 7, 2023
1 parent 160f3a5 commit 143f24f
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*!
* Copyright 2023 Cognite AS
*/

import { useEffect, type ReactElement, type FunctionComponent } from 'react';
import { useReveal } from '..';
import { Vector3 } from 'three';
import { type CameraState } from '@cognite/reveal';

type CameraStateTransform = Omit<Required<CameraState>, 'rotation'>;

export function withCameraStateUrlParam<T extends object>(
Component: FunctionComponent<T>
): FunctionComponent<T> {
return function CameraStateUrlParam(props: T): ReactElement {
const reveal = useReveal();
const getCameraStateFromUrlParam = useGetCameraStateFromUrlParam();

const setUrlParamOnCameraStop = (): void => {
const currentUrlCameraState = getCameraStateFromUrlParam();
const currentCameraManagerState = reveal.cameraManager.getCameraState();
if (
currentUrlCameraState !== undefined &&
!hasCameraStateChanged(currentUrlCameraState, currentCameraManagerState)
) {
return;
}
const { position, target } = currentCameraManagerState;
const url = new URL(window.location.toString());
url.searchParams.set('cameraPosition', `[${position.x},${position.y},${position.z}]`);
url.searchParams.set('cameraTarget', `[${target.x},${target.y},${target.z}]`);
window.history.pushState({}, '', url);
};

useEffect(() => {
reveal.cameraManager.on('cameraStop', setUrlParamOnCameraStop);
return () => {
reveal.cameraManager.off('cameraStop', setUrlParamOnCameraStop);
};
}, []);

return <Component {...props} />;
};

function hasCameraStateChanged(
previous: CameraStateTransform,
current: CameraStateTransform
): 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
);
}
}

export function useGetCameraStateFromUrlParam(): () => CameraStateTransform | undefined {
return () => {
const url = new URL(window.location.toString());
const position = url.searchParams.get('cameraPosition');
const target = url.searchParams.get('cameraTarget');

if (position === null || target === null) {
return;
}

const parsedPosition = getParsedVector(position);
const parsedTarget = getParsedVector(target);

if (parsedPosition === undefined || parsedTarget === undefined) {
return;
}

return { position: parsedPosition, target: parsedTarget };
};

function getParsedVector(s: string): Vector3 | undefined {
try {
return new Vector3().fromArray(JSON.parse(s));
} catch (e) {
return undefined;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,11 @@
* Copyright 2023 Cognite AS
*/

import {
useRef,
type ComponentType,
useEffect,
type ReactElement,
type FunctionComponent
} from 'react';
import { useRef, useEffect, type ReactElement, type FunctionComponent } from 'react';

export function withSuppressRevealEvents<T extends object>(
Component: FunctionComponent<T>
): ComponentType<T> {
): FunctionComponent<T> {
return function SuppressRevealEvents(props: T): ReactElement {
const divRef = useRef<HTMLDivElement>(null);

Expand Down
10 changes: 8 additions & 2 deletions react-components/src/hooks/useCameraNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
* Copyright 2023 Cognite AS
*/

import { type CogniteCadModel } from '@cognite/reveal';
import { type CameraState, type CogniteCadModel } from '@cognite/reveal';
import { useReveal } from '../components/RevealContainer/RevealContext';
import { useFdmNodeCache } from '../components/NodeCacheProvider/NodeCacheProvider';

export type CameraNavigationActions = {
fitCameraToAllModels: () => void;
fitCameraToModelNode: (revisionId: number, nodeId: number) => Promise<void>;
fitCameraToInstance: (externalId: string, space: string) => Promise<void>;
fitCameraToState: (cameraState: CameraState) => void;
};

export const useCameraNavigation = (): CameraNavigationActions => {
Expand Down Expand Up @@ -56,9 +57,14 @@ export const useCameraNavigation = (): CameraNavigationActions => {
await fitCameraToModelNode(modelMappings.revisionId, nodeId.id);
};

const fitCameraToState = (cameraState: CameraState): void => {
viewer.cameraManager.setCameraState(cameraState);
};

return {
fitCameraToAllModels,
fitCameraToInstance,
fitCameraToModelNode
fitCameraToModelNode,
fitCameraToState
};
};
9 changes: 9 additions & 0 deletions react-components/src/hooks/useIsRevealInitialized.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*!
* Copyright 2023 Cognite AS
*/
import { useRevealKeepAlive } from '../components/RevealKeepAlive/RevealKeepAliveContext';

export const useIsRevealInitialized = (): boolean => {
const revealKeepAliveData = useRevealKeepAlive();
return revealKeepAliveData?.viewerRef.current !== undefined;
};
6 changes: 5 additions & 1 deletion react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ export {
} from './hooks/useClickedNode';
export { useCameraNavigation } from './hooks/useCameraNavigation';
export { useMappedEdgesForRevisions } from './components/NodeCacheProvider/NodeCacheProvider';
export { useIsRevealInitialized } from './hooks/useIsRevealInitialized';
export { use3dNodeByExternalId } from './hooks/use3dNodeByExternalId';

// Higher order components
export { withSuppressRevealEvents } from './higher-order-components/withSuppressRevealEvents';

export {
withCameraStateUrlParam,
useGetCameraStateFromUrlParam
} from './higher-order-components/withCameraStateUrlParam';
// Types
export {
type PointCloudModelStyling,
Expand Down
23 changes: 20 additions & 3 deletions react-components/stories/Toolbar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import {
type QualitySettings,
RevealContainer,
RevealToolbar,
withSuppressRevealEvents
withSuppressRevealEvents,
withCameraStateUrlParam,
useGetCameraStateFromUrlParam,
useCameraNavigation
} from '../src';
import { CogniteClient } from '@cognite/sdk';
import { Color } from 'three';
import styled from 'styled-components';
import { Button, Menu, ToolBar, type ToolBarButton } from '@cognite/cogs.js';
import { type ReactElement, useState } from 'react';
import { type ReactElement, useState, useEffect } from 'react';

const meta = {
title: 'Example/Toolbar',
Expand All @@ -33,7 +36,7 @@ const sdk = new CogniteClient({
getToken: async () => await Promise.resolve(token)
});

const MyCustomToolbar = styled(withSuppressRevealEvents(ToolBar))`
const MyCustomToolbar = styled(withSuppressRevealEvents(withCameraStateUrlParam(ToolBar)))`
position: absolute;
right: 20px;
top: 70px;
Expand Down Expand Up @@ -103,6 +106,7 @@ export const Main: Story = {
},
render: ({ addModelOptions }) => (
<RevealContainer sdk={sdk} color={new Color(0x4a4a4a)}>
<FitToUrlCameraState />
<CadModelContainer addModelOptions={addModelOptions} />
<RevealToolbar
customSettingsContent={exampleCustomSettingElements()}
Expand All @@ -117,3 +121,16 @@ export const Main: Story = {
</RevealContainer>
)
};

function FitToUrlCameraState(): ReactElement {
const getCameraState = useGetCameraStateFromUrlParam();
const cameraNavigation = useCameraNavigation();

useEffect(() => {
const currentCameraState = getCameraState();
if (currentCameraState === undefined) return;
cameraNavigation.fitCameraToState(currentCameraState);
}, []);

return <></>;
}

0 comments on commit 143f24f

Please sign in to comment.