diff --git a/documentation/package.json b/documentation/package.json index 184e527c6de..717d0de434e 100644 --- a/documentation/package.json +++ b/documentation/package.json @@ -43,10 +43,10 @@ "@types/react": "18.2.12", "@types/styled-components": "5.1.26", "copyfiles": "2.4.1", - "docusaurus-plugin-typedoc": "0.19.2", + "docusaurus-plugin-typedoc": "0.20.1", "replace": "1.2.2", "rimraf": "5.0.1", - "typedoc": "0.24.8", + "typedoc": "0.25.0", "typedoc-plugin-markdown": "3.16.0", "typedoc-plugin-no-inherit": "1.4.0", "typescript": "5.2.2" diff --git a/documentation/yarn.lock b/documentation/yarn.lock index 9ea0c08eedc..a028fd1c249 100644 --- a/documentation/yarn.lock +++ b/documentation/yarn.lock @@ -2160,7 +2160,7 @@ __metadata: clsx: 2.0.0 copyfiles: 2.4.1 cross-env: 7.0.3 - docusaurus-plugin-typedoc: 0.19.2 + docusaurus-plugin-typedoc: 0.20.1 ieee754: 1.2.1 query-string: 8.1.0 react: 18.2.0 @@ -2168,7 +2168,7 @@ __metadata: replace: 1.2.2 rimraf: 5.0.1 styled-components: 5.3.11 - typedoc: 0.24.8 + typedoc: 0.25.0 typedoc-plugin-markdown: 3.16.0 typedoc-plugin-no-inherit: 1.4.0 typescript: 5.2.2 @@ -6030,13 +6030,13 @@ __metadata: languageName: node linkType: hard -"docusaurus-plugin-typedoc@npm:0.19.2": - version: 0.19.2 - resolution: "docusaurus-plugin-typedoc@npm:0.19.2" +"docusaurus-plugin-typedoc@npm:0.20.1": + version: 0.20.1 + resolution: "docusaurus-plugin-typedoc@npm:0.20.1" peerDependencies: typedoc: ">=0.24.0" typedoc-plugin-markdown: ">=3.15.0" - checksum: 347c1b6b509e14dabbb8b4d1224682ea2281995a60b4cf48e456dbbdbcecb4b73cf4fe3e031f1e01ae72dfb3bec935c6eaac7e4ddf3f63587fad2ac8ea028cec + checksum: dfc0fa1b33e5142ebc939969944f68488c303ab5083e369052c9f40d5a70fe84237c23fa47012ee410bb23d20980ffe5675eb37f3a2ee0dfabdc5a1ff66f38d1 languageName: node linkType: hard @@ -9054,21 +9054,21 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.0": - version: 9.0.0 - resolution: "minimatch@npm:9.0.0" +"minimatch@npm:^9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" dependencies: brace-expansion: ^2.0.1 - checksum: 7bd57899edd1d1b0560f50b5b2d1ea4ad2a366c5a2c8e0a943372cf2f200b64c256bae45a87a80915adbce27fa36526264296ace0da57b600481fe5ea3e372e5 + checksum: 97f5f5284bb57dc65b9415dec7f17a0f6531a33572193991c60ff18450dcfad5c2dad24ffeaf60b5261dccd63aae58cc3306e2209d57e7f88c51295a532d8ec3 languageName: node linkType: hard -"minimatch@npm:^9.0.1": - version: 9.0.1 - resolution: "minimatch@npm:9.0.1" +"minimatch@npm:^9.0.3": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" dependencies: brace-expansion: ^2.0.1 - checksum: 97f5f5284bb57dc65b9415dec7f17a0f6531a33572193991c60ff18450dcfad5c2dad24ffeaf60b5261dccd63aae58cc3306e2209d57e7f88c51295a532d8ec3 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 languageName: node linkType: hard @@ -12542,19 +12542,19 @@ __metadata: languageName: node linkType: hard -"typedoc@npm:0.24.8": - version: 0.24.8 - resolution: "typedoc@npm:0.24.8" +"typedoc@npm:0.25.0": + version: 0.25.0 + resolution: "typedoc@npm:0.25.0" dependencies: lunr: ^2.3.9 marked: ^4.3.0 - minimatch: ^9.0.0 + minimatch: ^9.0.3 shiki: ^0.14.1 peerDependencies: - typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x bin: typedoc: bin/typedoc - checksum: a46a14497f789fb3594e6c3af2e45276934ac46df40b7ed15a504ee51dc7a8013a2ffb3a54fd73abca6a2b71f97d3ec9ad356fa9aa81d29743e4645a965a2ae0 + checksum: f0c5980017858969f037b136d9f305fa70162be891e7b169a92701cff67e1eed5c33895121265ec7da5b4a19d0042e649c103a6719c2b42736325b22fd85ff6a languageName: node linkType: hard diff --git a/react-components/src/higher-order-components/withCameraStateUrlParam.tsx b/react-components/src/higher-order-components/withCameraStateUrlParam.tsx new file mode 100644 index 00000000000..d5562ee2f13 --- /dev/null +++ b/react-components/src/higher-order-components/withCameraStateUrlParam.tsx @@ -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, 'rotation'>; + +export function withCameraStateUrlParam( + Component: FunctionComponent +): FunctionComponent { + 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 ; + }; + + 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; + } + } +} diff --git a/react-components/src/higher-order-components/withSuppressRevealEvents.tsx b/react-components/src/higher-order-components/withSuppressRevealEvents.tsx index 7f440c405a4..f02281c7735 100644 --- a/react-components/src/higher-order-components/withSuppressRevealEvents.tsx +++ b/react-components/src/higher-order-components/withSuppressRevealEvents.tsx @@ -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( Component: FunctionComponent -): ComponentType { +): FunctionComponent { return function SuppressRevealEvents(props: T): ReactElement { const divRef = useRef(null); diff --git a/react-components/src/hooks/useCameraNavigation.tsx b/react-components/src/hooks/useCameraNavigation.tsx index 7af9b336306..2218f059fe3 100644 --- a/react-components/src/hooks/useCameraNavigation.tsx +++ b/react-components/src/hooks/useCameraNavigation.tsx @@ -2,7 +2,7 @@ * 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'; @@ -10,6 +10,7 @@ export type CameraNavigationActions = { fitCameraToAllModels: () => void; fitCameraToModelNode: (revisionId: number, nodeId: number) => Promise; fitCameraToInstance: (externalId: string, space: string) => Promise; + fitCameraToState: (cameraState: CameraState) => void; }; export const useCameraNavigation = (): CameraNavigationActions => { @@ -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 }; }; diff --git a/react-components/src/hooks/useIsRevealInitialized.tsx b/react-components/src/hooks/useIsRevealInitialized.tsx new file mode 100644 index 00000000000..3da6268428c --- /dev/null +++ b/react-components/src/hooks/useIsRevealInitialized.tsx @@ -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; +}; diff --git a/react-components/src/index.ts b/react-components/src/index.ts index 9ba183226f8..7a487841275 100644 --- a/react-components/src/index.ts +++ b/react-components/src/index.ts @@ -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, diff --git a/react-components/stories/Toolbar.stories.tsx b/react-components/stories/Toolbar.stories.tsx index d4313f9710e..8ef04324ead 100644 --- a/react-components/stories/Toolbar.stories.tsx +++ b/react-components/stories/Toolbar.stories.tsx @@ -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', @@ -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; @@ -103,6 +106,7 @@ export const Main: Story = { }, render: ({ addModelOptions }) => ( + ) }; + +function FitToUrlCameraState(): ReactElement { + const getCameraState = useGetCameraStateFromUrlParam(); + const cameraNavigation = useCameraNavigation(); + + useEffect(() => { + const currentCameraState = getCameraState(); + if (currentCameraState === undefined) return; + cameraNavigation.fitCameraToState(currentCameraState); + }, []); + + return <>; +}