From b08cbafa4c529f302dc804d583794ba086d2aa96 Mon Sep 17 00:00:00 2001 From: "Christopher J. Tannum" Date: Tue, 8 Aug 2023 13:52:39 +0200 Subject: [PATCH] feat(react-components): add RevealKeepAlive component to cache the viewer when unmounting the RevealContainer (#3552) * feat: add RevealKeepAlive component to cache the viewer when unmounting the RevealContainer * fix: restore models based on transform as well to distinguish copies of the same model * chore: remove commented code * fix: broken stories --------- Co-authored-by: cognite-bulldozer[bot] <51074376+cognite-bulldozer[bot]@users.noreply.github.com> --- react-components/package.json | 2 + .../CadModelContainer/CadModelContainer.tsx | 26 ++++- .../Image360CollectionContainer.tsx | 18 ++- .../PointCloudContainer.tsx | 25 ++++- .../RevealContainer/RevealContainer.tsx | 49 +++++--- .../RevealKeepAlive/RevealKeepAlive.tsx | 23 ++++ .../RevealKeepAlive/RevealKeepAliveContext.ts | 16 +++ react-components/src/index.ts | 1 + .../stories/RevealKeepAlive.stories.tsx | 105 ++++++++++++++++++ .../stories/ViewerAnchor.stories.tsx | 17 +-- react-components/yarn.lock | 11 ++ 11 files changed, 258 insertions(+), 35 deletions(-) create mode 100644 react-components/src/components/RevealKeepAlive/RevealKeepAlive.tsx create mode 100644 react-components/src/components/RevealKeepAlive/RevealKeepAliveContext.ts create mode 100644 react-components/stories/RevealKeepAlive.stories.tsx diff --git a/react-components/package.json b/react-components/package.json index 794e0ceab45..f65937aa37d 100644 --- a/react-components/package.json +++ b/react-components/package.json @@ -20,6 +20,7 @@ "@cognite/cogs.js": ">=9", "@cognite/reveal": "4.4.0", "react": ">=18", + "react-dom": ">=18", "styled-components": ">=5" }, "devDependencies": { @@ -39,6 +40,7 @@ "@tanstack/react-query-devtools": "^4.29.19", "@types/lodash": "^4.14.190", "@types/react": "18.2.7", + "@types/react-dom": "^18.2.7", "@types/styled-components": "5.1.26", "@types/three": "0.155.0", "@typescript-eslint/eslint-plugin": "^5.50.0", diff --git a/react-components/src/components/CadModelContainer/CadModelContainer.tsx b/react-components/src/components/CadModelContainer/CadModelContainer.tsx index c55beaf43a2..d7b558604ea 100644 --- a/react-components/src/components/CadModelContainer/CadModelContainer.tsx +++ b/react-components/src/components/CadModelContainer/CadModelContainer.tsx @@ -11,9 +11,10 @@ import { DefaultNodeAppearance } from '@cognite/reveal'; import { useReveal } from '../RevealContainer/RevealContext'; -import { type Matrix4 } from 'three'; +import { Matrix4 } from 'three'; import { useSDK } from '../RevealContainer/SDKProvider'; import { type CogniteClient } from '@cognite/sdk'; +import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; export type NodeStylingGroup = { nodeIds: number[]; @@ -43,6 +44,7 @@ export function CadModelContainer({ styling, onLoad }: CogniteCadModelProps): ReactElement { + const cachedViewerRef = useRevealKeepAlive(); const [model, setModel] = useState(); const viewer = useReveal(); const sdk = useSDK(); @@ -51,7 +53,6 @@ export function CadModelContainer({ useEffect(() => { addModel(modelId, revisionId, transform, onLoad).catch(console.error); - return removeModel; }, [modelId, revisionId, geometryFilter]); useEffect(() => { @@ -70,6 +71,8 @@ export function CadModelContainer({ }; }, [styling, model]); + useEffect(() => removeModel, [model]); + return <>; async function addModel( @@ -78,7 +81,7 @@ export function CadModelContainer({ transform?: Matrix4, onLoad?: (model: CogniteCadModel) => void ): Promise { - const cadModel = await viewer.addCadModel({ modelId, revisionId }); + const cadModel = await getOrAddModel(); if (transform !== undefined) { cadModel.setModelTransformation(transform); } @@ -86,10 +89,27 @@ export function CadModelContainer({ onLoad?.(cadModel); return cadModel; + + async function getOrAddModel(): Promise { + const viewerModel = viewer.models.find( + (model) => + model.modelId === modelId && + model.revisionId === revisionId && + model.getModelTransformation().equals(transform ?? new Matrix4()) + ); + if (viewerModel !== undefined) { + return await Promise.resolve(viewerModel as CogniteCadModel); + } + return await viewer.addCadModel({ modelId, revisionId }); + } } function removeModel(): void { if (model === undefined || !viewer.models.includes(model)) return; + + if (cachedViewerRef !== undefined && !cachedViewerRef.isRevealContainerMountedRef.current) + return; + viewer.removeModel(model); setModel(undefined); } diff --git a/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx b/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx index b6f5f92d2c9..21177fdf543 100644 --- a/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx +++ b/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx @@ -4,6 +4,7 @@ import { type ReactElement, useEffect, useRef } from 'react'; import { useReveal } from '../RevealContainer/RevealContext'; import { type Image360Collection } from '@cognite/reveal'; +import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; type Image360CollectionContainerProps = { siteId: string; @@ -14,6 +15,7 @@ export function Image360CollectionContainer({ siteId, onLoad }: Image360CollectionContainerProps): ReactElement { + const cachedViewerRef = useRevealKeepAlive(); const modelRef = useRef(); const viewer = useReveal(); @@ -25,13 +27,27 @@ export function Image360CollectionContainer({ return <>; async function add360Collection(): Promise { - const image360Collection = await viewer.add360ImageSet('events', { site_id: siteId }); + const image360Collection = await getOrAdd360Collection(); modelRef.current = image360Collection; onLoad?.(image360Collection); + + async function getOrAdd360Collection(): Promise { + const collections = viewer.get360ImageCollections(); + const collection = collections.find((collection) => collection.id === siteId); + if (collection !== undefined) { + return collection; + } + + return await viewer.add360ImageSet('events', { site_id: siteId }); + } } function remove360Collection(): void { if (modelRef.current === undefined) return; + + if (cachedViewerRef !== undefined && !cachedViewerRef.isRevealContainerMountedRef.current) + return; + viewer.remove360ImageSet(modelRef.current); modelRef.current = undefined; } diff --git a/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx b/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx index 989b937f5e3..7a10e872c37 100644 --- a/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx +++ b/react-components/src/components/PointCloudContainer/PointCloudContainer.tsx @@ -10,8 +10,9 @@ import { } from '@cognite/reveal'; import { useEffect, type ReactElement, useState } from 'react'; -import { type Matrix4 } from 'three'; +import { Matrix4 } from 'three'; import { useReveal } from '../RevealContainer/RevealContext'; +import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; export type AnnotationIdStylingGroup = { annotationIds: number[]; @@ -36,13 +37,13 @@ export function PointCloudContainer({ transform, onLoad }: CognitePointCloudModelProps): ReactElement { + const cachedViewerRef = useRevealKeepAlive(); const [model, setModel] = useState(); const viewer = useReveal(); const { modelId, revisionId } = addModelOptions; useEffect(() => { addModel(modelId, revisionId, transform).catch(console.error); - return removeModel; }, [modelId, revisionId]); useEffect(() => { @@ -58,21 +59,39 @@ export function PointCloudContainer({ return cleanStyling; }, [styling, model]); + useEffect(() => removeModel, [model]); + return <>; async function addModel(modelId: number, revisionId: number, transform?: Matrix4): Promise { - const pointCloudModel = await viewer.addPointCloudModel({ modelId, revisionId }); + const pointCloudModel = await getOrAddModel(); if (transform !== undefined) { pointCloudModel.setModelTransformation(transform); } setModel(pointCloudModel); onLoad?.(pointCloudModel); + + async function getOrAddModel(): Promise { + const viewerModel = viewer.models.find( + (model) => + model.modelId === modelId && + model.revisionId === revisionId && + model.getModelTransformation().equals(transform ?? new Matrix4()) + ); + if (viewerModel !== undefined) { + return await Promise.resolve(viewerModel as CognitePointCloudModel); + } + return await viewer.addPointCloudModel({ modelId, revisionId }); + } } function removeModel(): void { if (model === undefined || !viewer.models.includes(model)) return; + if (cachedViewerRef !== undefined && !cachedViewerRef.isRevealContainerMountedRef.current) + return; + viewer.removeModel(model); setModel(undefined); } diff --git a/react-components/src/components/RevealContainer/RevealContainer.tsx b/react-components/src/components/RevealContainer/RevealContainer.tsx index 0d7f29b0b3d..c59ba344991 100644 --- a/react-components/src/components/RevealContainer/RevealContainer.tsx +++ b/react-components/src/components/RevealContainer/RevealContainer.tsx @@ -3,12 +3,14 @@ */ import { type CogniteClient } from '@cognite/sdk'; import { useEffect, useRef, type ReactNode, useState, type ReactElement } from 'react'; +import { createPortal } from 'react-dom'; import { Cognite3DViewer, type Cognite3DViewerOptions } from '@cognite/reveal'; import { RevealContext } from './RevealContext'; import { type Color } from 'three'; import { ModelsLoadingStateContext } from '../Reveal3DResources/ModelsLoadingContext'; import { SDKProvider } from './SDKProvider'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; type RevealContainerProps = { color?: Color; @@ -34,20 +36,30 @@ export function RevealContainer({ color, viewerOptions }: RevealContainerProps): ReactElement { + const revealKeepAliveData = useRevealKeepAlive(); const [viewer, setViewer] = useState(); - const revealDomElementRef = useRef(null); + const wrapperDomElement = useRef(null); + const viewerDomElement = useRef(null); useEffect(() => { - initializeViewer(); - return disposeViewer; + const initializedViewer = getOrInitializeViewer(); + if (revealKeepAliveData === undefined) { + return; + } + revealKeepAliveData.isRevealContainerMountedRef.current = true; + return () => { + if (revealKeepAliveData === undefined) { + initializedViewer.dispose(); + return; + } + revealKeepAliveData.isRevealContainerMountedRef.current = false; + }; }, []); return ( -
+
{mountChildren()}
@@ -55,30 +67,33 @@ export function RevealContainer({ ); function mountChildren(): ReactElement { - if (viewer === undefined) return <>; + if (viewer === undefined || viewerDomElement.current === null) return <>; return ( <> - {children} + + {createPortal(children, viewerDomElement.current)} + ); } - function initializeViewer(): void { - const domElement = revealDomElementRef.current; + function getOrInitializeViewer(): Cognite3DViewer { + const domElement = wrapperDomElement.current; if (domElement === null) { throw new Error('Failure in mounting RevealContainer to DOM.'); } - const viewer = new Cognite3DViewer({ ...viewerOptions, sdk, domElement }); + const viewer = + revealKeepAliveData?.viewerRef.current ?? new Cognite3DViewer({ ...viewerOptions, sdk }); + if (revealKeepAliveData !== undefined) { + revealKeepAliveData.viewerRef.current = viewer; + } + domElement.appendChild(viewer.domElement); + viewerDomElement.current = viewer.domElement; viewer.setBackgroundColor({ color, alpha: 1 }); setViewer(viewer); - } - - function disposeViewer(): void { - if (viewer === undefined) return; - viewer.dispose(); - setViewer(undefined); + return viewer; } } diff --git a/react-components/src/components/RevealKeepAlive/RevealKeepAlive.tsx b/react-components/src/components/RevealKeepAlive/RevealKeepAlive.tsx new file mode 100644 index 00000000000..15bc32eae95 --- /dev/null +++ b/react-components/src/components/RevealKeepAlive/RevealKeepAlive.tsx @@ -0,0 +1,23 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { type Cognite3DViewer } from '@cognite/reveal'; +import { type ReactNode, type ReactElement, useRef, useEffect } from 'react'; +import { RevealKeepAliveContext } from './RevealKeepAliveContext'; + +export function RevealKeepAlive({ children }: { children?: ReactNode }): ReactElement { + const viewerRef = useRef(); + const isRevealContainerMountedRef = useRef(false); + useEffect(() => { + return () => { + viewerRef.current?.dispose(); + viewerRef.current = undefined; + }; + }, []); + return ( + + {children} + + ); +} diff --git a/react-components/src/components/RevealKeepAlive/RevealKeepAliveContext.ts b/react-components/src/components/RevealKeepAlive/RevealKeepAliveContext.ts new file mode 100644 index 00000000000..edbabebd0f7 --- /dev/null +++ b/react-components/src/components/RevealKeepAlive/RevealKeepAliveContext.ts @@ -0,0 +1,16 @@ +/*! + * Copyright 2023 Cognite AS + */ +import { type Cognite3DViewer } from '@cognite/reveal'; +import { type MutableRefObject, createContext, useContext } from 'react'; + +export type RevealKeepAliveData = { + viewerRef: MutableRefObject; + isRevealContainerMountedRef: MutableRefObject; +}; + +export const RevealKeepAliveContext = createContext(undefined); + +export const useRevealKeepAlive = (): RevealKeepAliveData | undefined => { + return useContext(RevealKeepAliveContext); +}; diff --git a/react-components/src/index.ts b/react-components/src/index.ts index a897db6522d..38bc3754bec 100644 --- a/react-components/src/index.ts +++ b/react-components/src/index.ts @@ -13,6 +13,7 @@ export { Image360HistoricalDetails } from './components/Image360HistoricalDetail export { ViewerAnchor } from './components/ViewerAnchor/ViewerAnchor'; export { CameraController } from './components/CameraController/CameraController'; export { RevealToolbar } from './components/RevealToolbar/RevealToolbar'; +export { RevealKeepAlive } from './components/RevealKeepAlive/RevealKeepAlive'; // Hooks export { useReveal } from './components/RevealContainer/RevealContext'; diff --git a/react-components/stories/RevealKeepAlive.stories.tsx b/react-components/stories/RevealKeepAlive.stories.tsx new file mode 100644 index 00000000000..72222781563 --- /dev/null +++ b/react-components/stories/RevealKeepAlive.stories.tsx @@ -0,0 +1,105 @@ +/*! + * Copyright 2023 Cognite AS + */ +import type { Meta, StoryObj } from '@storybook/react'; +import { + CameraController, + type FdmAssetMappingsConfig, + Reveal3DResources, + RevealContainer, + RevealKeepAlive +} from '../src'; +import { Color, Matrix4, Vector3 } from 'three'; +import { createSdkByUrlToken } from './utilities/createSdkByUrlToken'; +import { useState, type ReactElement } from 'react'; + +const meta = { + title: 'Example/RevealKeepAlive', + component: RevealKeepAlive, + tags: ['autodocs'] +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const sdk = createSdkByUrlToken(); +const fdmAssetMappingConfig: FdmAssetMappingsConfig = { + source: { + space: 'fdm-3d-test-savelii', + version: '1', + type: 'view', + externalId: 'CDF_3D_Connection_Data' + }, + assetFdmSpace: 'bark-corporation', + global3dSpace: 'hf_3d_global_data' +}; + +export const Main: Story = { + render: () => +}; + +const KeepAliveMockScenario = (): ReactElement => { + const [isKeepAliveMounted, setIsKeepAliveMounted] = useState(true); + const [isRevealContainerMounted, setIsRevealContainerMounted] = useState(true); + const [isResourcesMounted, setIsResourcesMounted] = useState(true); + return ( + <> + + + + {isKeepAliveMounted && ( + + {isRevealContainerMounted && ( + + {isResourcesMounted && ( + + )} + + + )} + + )} + + ); +}; diff --git a/react-components/stories/ViewerAnchor.stories.tsx b/react-components/stories/ViewerAnchor.stories.tsx index 1f94e52549a..1fce09831be 100644 --- a/react-components/stories/ViewerAnchor.stories.tsx +++ b/react-components/stories/ViewerAnchor.stories.tsx @@ -7,14 +7,12 @@ import { Color, Vector3 } from 'three'; import { CameraController, ViewerAnchor } from '../src/'; import { createSdkByUrlToken } from './utilities/createSdkByUrlToken'; import styled from 'styled-components'; +import { DefaultFdmConfig } from './utilities/fdmConfig'; const meta = { title: 'Example/ViewerAnchor', component: Reveal3DResources, - tags: ['autodocs'], - argTypes: { - styling: {} - } + tags: ['autodocs'] } satisfies Meta; export default meta; @@ -29,9 +27,10 @@ export const Main: Story = { modelId: 1791160622840317, revisionId: 498427137020189 } - ] + ], + fdmAssetMappingConfig: DefaultFdmConfig }, - render: ({ resources, styling, fdmAssetMappingConfig }) => { + render: ({ resources, fdmAssetMappingConfig }) => { const position = new Vector3(25, 0, -25); const position2 = new Vector3(); const SuppressedDiv = withSuppressRevealEvents(styled.div``); @@ -46,11 +45,7 @@ export const Main: Story = { placement: 'topRight' } }}> - +
=9" "@cognite/reveal": 4.4.0 react: ">=18" + react-dom: ">=18" styled-components: ">=5" languageName: unknown linkType: soft @@ -5262,6 +5264,15 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^18.2.7": + version: 18.2.7 + resolution: "@types/react-dom@npm:18.2.7" + dependencies: + "@types/react": "*" + checksum: e02ea908289a7ad26053308248d2b87f6aeafd73d0e2de2a3d435947bcea0422599016ffd1c3e38ff36c42f5e1c87c7417f05b0a157e48649e4a02f21727d54f + languageName: node + linkType: hard + "@types/react-is@npm:^16.7.1 || ^17.0.0": version: 17.0.4 resolution: "@types/react-is@npm:17.0.4"