Skip to content

Commit

Permalink
feat: Info card overlay (#3521)
Browse files Browse the repository at this point in the history
* feat: initial info card overlay

* feat: Support multiple ViewerAnchors

---------

Co-authored-by: Christopher J. Tannum <[email protected]>
  • Loading branch information
haakonflatval-cognite and christjt authored Jul 31, 2023
1 parent 4f0feca commit 1ceeb6b
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { type Color } from 'three';
import { ModelsLoadingStateContext } from '../Reveal3DResources/ModelsLoadingContext';
import { SDKProvider } from './SDKProvider';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { AuxillaryDivProvider } from '../ViewerAnchor/AuxillaryDivProvider';

type RevealContainerProps = {
color?: Color;
Expand Down Expand Up @@ -47,9 +48,11 @@ export function RevealContainer({
return (
<SDKProvider sdk={sdk}>
<QueryClientProvider client={queryClient}>
<div style={{ width: '100%', height: '100%' }} ref={revealDomElementRef}>
{mountChildren()}
</div>
<AuxillaryDivProvider>
<div style={{ width: '100%', height: '100%' }} ref={revealDomElementRef}>
{mountChildren()}
</div>
</AuxillaryDivProvider>
{uiElements}
</QueryClientProvider>
</SDKProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*!
* Copyright 2023 Cognite AS
*/

import { createContext, useContext, useState, useCallback, type ReactElement } from 'react';

export const AuxillaryDivProvider = ({ children }: { children: ReactElement }): ReactElement => {
const [elements, setElements] = useState<ReactElement[]>([]);

// Maintain a local copy of the elements, needed for properly supporting multiple
// `addElement` calls between rerenders
let cachedElements = elements;

const addElement = useCallback(
(element: ReactElement) => {
const newElementList = [...cachedElements, element];

setElements(newElementList);
cachedElements = newElementList;
},
[elements, setElements]
);

const removeElement = useCallback(
(element: ReactElement) => {
const newElementList = cachedElements.filter((e) => e !== element);

setElements(newElementList);
cachedElements = newElementList;
},
[elements, setElements]
);

return (
<>
<AuxillaryDivContext.Provider value={{ addElement, removeElement }}>
<div>{elements}</div>
{children}
</AuxillaryDivContext.Provider>
</>
);
};

type AuxillaryContextData = {
addElement: (element: ReactElement) => void;
removeElement: (element: ReactElement) => void;
};

const AuxillaryDivContext = createContext<AuxillaryContextData | null>(null);

export const useAuxillaryDivContext = (): AuxillaryContextData => {
const auxContext = useContext(AuxillaryDivContext);
if (auxContext === null) {
throw new Error('useAuxillaryDivContext must be used inside AuxillaryDivContext');
}

return auxContext;
};
64 changes: 64 additions & 0 deletions react-components/src/components/ViewerAnchor/ViewerAnchor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*!
* Copyright 2023 Cognite AS
*/

import { useEffect, useRef, type ReactElement, type RefObject } from 'react';
import { type Vector3 } from 'three';

import { useReveal } from '../RevealContainer/RevealContext';

import { HtmlOverlayTool } from '@cognite/reveal/tools';
import { useAuxillaryDivContext } from './AuxillaryDivProvider';

export type ViewerAnchorElementMapping = {
ref: RefObject<HTMLElement>;
position: Vector3;
};

export type ViewerAnchorProps = {
position: Vector3;
children: ReactElement;
uniqueKey: string;
};

export const ViewerAnchor = ({
position,
children,
uniqueKey
}: ViewerAnchorProps): ReactElement => {
const viewer = useReveal();

const htmlTool = useRef<HtmlOverlayTool>(new HtmlOverlayTool(viewer));

const auxContext = useAuxillaryDivContext();

const htmlRef = useRef<HTMLDivElement>(null);
const element = (
<div key={uniqueKey} ref={htmlRef} style={{ position: 'absolute' }}>
{children}
</div>
);

useEffect(() => {
auxContext.addElement(element);
return () => {
auxContext.removeElement(element);
};
}, []);

useEffect(() => {
if (htmlRef.current === null) {
return;
}

const elementRef = htmlRef.current;

htmlTool.current.add(elementRef, position);

return () => {
htmlTool.current.remove(elementRef);
};
}, [auxContext, children, htmlRef.current]);

return <></>;
};
1 change: 1 addition & 0 deletions react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export {
type Reveal3DResourcesStyling,
type FdmAssetStylingGroup
} from './components/Reveal3DResources/Reveal3DResources';
export { ViewerAnchor } from './components/ViewerAnchor/ViewerAnchor';
export { CameraController } from './components/CameraController/CameraController';
export type {
AddImageCollection360Options,
Expand Down
89 changes: 60 additions & 29 deletions react-components/stories/Reveal3DResources.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
*/
import type { Meta, StoryObj } from '@storybook/react';
import { Reveal3DResources, RevealContainer } from '../src';
import { Color, Matrix4 } from 'three';
import { CameraController } from '../src/';
import { Color, Matrix4, Vector3 } from 'three';
import { CameraController, ViewerAnchor } from '../src/';
import { createSdkByUrlToken } from './utilities/createSdkByUrlToken';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

Expand Down Expand Up @@ -123,31 +123,62 @@ export const Main: Story = {
assetFdmSpace: 'bark-corporation'
}
},
render: ({ resources, styling, fdmAssetMappingConfig }) => (
<RevealContainer
sdk={sdk}
color={new Color(0x4a4a4a)}
uiElements={<ReactQueryDevtools initialIsOpen={false} />}
viewerOptions={{
loadingIndicatorStyle: {
opacity: 1,
placement: 'topRight'
}
}}>
<Reveal3DResources
resources={resources}
styling={styling}
fdmAssetMappingConfig={fdmAssetMappingConfig}
/>
<CameraController
initialFitCamera={{
to: 'allModels'
}}
cameraControlsOptions={{
changeCameraTargetOnClick: true,
mouseWheelAction: 'zoomToCursor'
}}
/>
</RevealContainer>
)
render: ({ resources, styling, fdmAssetMappingConfig }) => {
const position = new Vector3(50, 30, 50);
const position2 = new Vector3(0, 0, 0);

return (
<RevealContainer
sdk={sdk}
color={new Color(0x4a4a4a)}
uiElements={<ReactQueryDevtools initialIsOpen={false} />}
viewerOptions={{
loadingIndicatorStyle: {
opacity: 1,
placement: 'topRight'
}
}}>
<Reveal3DResources
resources={resources}
styling={styling}
fdmAssetMappingConfig={fdmAssetMappingConfig}
/>
<ViewerAnchor position={position} uniqueKey="key2">
<p
style={{
backgroundColor: 'turquoise',
borderColor: 'black',
borderWidth: '10px',
borderStyle: 'solid',
maxWidth: '300px',
transform: 'translate(-50%, calc(-100% - 50px))'
}}>
This label is stuck at position {position.toArray().join(',')}
</p>
</ViewerAnchor>
<ViewerAnchor position={position2} uniqueKey="key1">
<p
style={{
backgroundColor: 'red',
borderColor: 'black',
borderWidth: '10px',
borderStyle: 'solid',
maxWidth: '300px',
transform: 'translate(0px, 0px)'
}}>
This label is stuck at position {position2.toArray().join(',')}
</p>
</ViewerAnchor>
<CameraController
initialFitCamera={{
to: 'allModels'
}}
cameraControlsOptions={{
changeCameraTargetOnClick: true,
mouseWheelAction: 'zoomToCursor'
}}
/>
</RevealContainer>
);
}
};
103 changes: 103 additions & 0 deletions react-components/stories/ViewerAnchor.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*!
* Copyright 2023 Cognite AS
*/
import type { Meta, StoryObj } from '@storybook/react';
import { Reveal3DResources, RevealContainer } from '../src';
import { Color, Matrix4, Vector3 } from 'three';
import { CameraController, ViewerAnchor } from '../src/';
import { createSdkByUrlToken } from './utilities/createSdkByUrlToken';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const meta = {
title: 'Example/ViewerAnchor',
component: Reveal3DResources,
tags: ['autodocs'],
argTypes: {
styling: {}
}
} satisfies Meta<typeof Reveal3DResources>;

export default meta;
type Story = StoryObj<typeof meta>;

const sdk = createSdkByUrlToken();

export const Main: Story = {
args: {
resources: [
{
modelId: 2551525377383868,
revisionId: 2143672450453400,
transform: new Matrix4().makeTranslation(-340, -480, 80)
}
],
styling: {},
fdmAssetMappingConfig: {
source: {
space: 'hf_3d_schema',
version: '1',
type: 'view',
externalId: 'cdf_3d_connection_data'
},
assetFdmSpace: 'hf_customer_a'
}
},
render: ({ resources, styling, fdmAssetMappingConfig }) => {
const position = new Vector3(50, 30, 50);
const position2 = new Vector3(0, 0, 0);

return (
<RevealContainer
sdk={sdk}
color={new Color(0x4a4a4a)}
uiElements={<ReactQueryDevtools initialIsOpen={false} />}
viewerOptions={{
loadingIndicatorStyle: {
opacity: 1,
placement: 'topRight'
}
}}>
<Reveal3DResources
resources={resources}
styling={styling}
fdmAssetMappingConfig={fdmAssetMappingConfig}
/>
<ViewerAnchor position={position} uniqueKey="key2">
<p
style={{
backgroundColor: 'turquoise',
padding: '10px',
borderRadius: '10px',
borderStyle: 'solid',
maxWidth: '300px',
transform: 'translate(-50%, calc(-100% - 50px))'
}}>
This label is stuck at position {position.toArray().join(',')}
</p>
</ViewerAnchor>
<ViewerAnchor position={position2} uniqueKey="key1">
<p
style={{
backgroundColor: 'red',
padding: '10px',
borderRadius: '10px',
borderStyle: 'solid',
maxWidth: '300px',
transform: 'translate(0px, 0px)'
}}>
This label is stuck at position {position2.toArray().join(',')}
</p>
</ViewerAnchor>
<CameraController
initialFitCamera={{
to: 'allModels'
}}
cameraControlsOptions={{
changeCameraTargetOnClick: true,
mouseWheelAction: 'zoomToCursor'
}}
/>
</RevealContainer>
);
}
};

0 comments on commit 1ceeb6b

Please sign in to comment.