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: add a higher order component for storing camera state to URL #3682

Merged
merged 10 commits into from
Sep 7, 2023
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;
christjt marked this conversation as resolved.
Show resolved Hide resolved
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);
christjt marked this conversation as resolved.
Show resolved Hide resolved
};

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