Skip to content

Commit

Permalink
feat(react-components): add threed resource container (#3451)
Browse files Browse the repository at this point in the history
* feat: add container for CAD model

* feat: add 360 image collection container

* fix: add 3d resource container

* feat: add CameraController component

* fix: add missing import

* feat: infer model type using Reveal's determineModelType

* fix: from QA

---------

Co-authored-by: cognite-bulldozer[bot] <51074376+cognite-bulldozer[bot]@users.noreply.github.com>
  • Loading branch information
christjt and cognite-bulldozer[bot] authored Jul 4, 2023
1 parent 578286e commit d2ca002
Show file tree
Hide file tree
Showing 19 changed files with 479 additions and 65 deletions.
5 changes: 2 additions & 3 deletions react-components/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const path = require('path');

module.exports = {
env: {
browser: true,
Expand All @@ -14,7 +12,8 @@ module.exports = {
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: [path.join(__dirname, 'tsconfig.json')]
project: './tsconfig.json',
tsconfigRootDir: __dirname
},
plugins: ['react', 'prettier', 'header'],
rules: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@ import { type Matrix4 } from 'three';
type Cognite3dModelProps = {
addModelOptions: AddModelOptions;
transform?: Matrix4;
onLoad?: () => void;
};

export default function CogniteCadModelContainer({
export function CadModelContainer({
addModelOptions,
transform
transform,
onLoad
}: Cognite3dModelProps): ReactElement {
const modelRef = useRef<CogniteCadModel>();
const viewer = useReveal();
const { modelId, revisionId } = addModelOptions;
const { modelId, revisionId, geometryFilter } = addModelOptions;

useEffect(() => {
addModel(modelId, revisionId, transform).catch(console.error);
addModel(modelId, revisionId, transform, onLoad).catch(console.error);
return removeModel;
}, [addModelOptions]);
}, [modelId, revisionId, geometryFilter]);

useEffect(() => {
if (modelRef.current === undefined || transform === undefined) return;
Expand All @@ -31,12 +33,18 @@ export default function CogniteCadModelContainer({

return <></>;

async function addModel(modelId: number, revisionId: number, transform?: Matrix4): Promise<void> {
async function addModel(
modelId: number,
revisionId: number,
transform?: Matrix4,
onLoad?: () => void
): Promise<void> {
const cadModel = await viewer.addCadModel({ modelId, revisionId });
if (transform !== undefined) {
cadModel.setModelTransformation(transform);
}
modelRef.current = cadModel;
onLoad?.();
}

function removeModel(): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*!
* Copyright 2023 Cognite AS
*/
import { type ReactElement, useEffect, useContext, useRef } from 'react';
import { useReveal } from '../RevealContainer/RevealContext';
import { ModelsLoadingStateContext } from '../Reveal3DResources/ModelsLoadingContext';
import { type CameraState } from '@cognite/reveal';

export type CameraControllerProps = {
initialFitCamera?: FittingStrategy;
};

type FittingStrategy =
| { to: 'cameraState'; state: CameraState }
| { to: 'allModels' }
| { to: 'none' };

export function CameraController({ initialFitCamera }: CameraControllerProps): ReactElement {
const initialCameraSet = useRef(false);
const viewer = useReveal();
const { modelsAdded } = useContext(ModelsLoadingStateContext);

const fittingStrategy: Required<FittingStrategy> = initialFitCamera ?? { to: 'allModels' };

useEffect(() => {
if (initialCameraSet.current) return;
if (fittingStrategy.to === 'none') {
initialCameraSet.current = true;
return;
}
if (fittingStrategy.to === 'cameraState') {
viewer.cameraManager.setCameraState(fittingStrategy.state);
initialCameraSet.current = true;
return;
}
if (fittingStrategy.to === 'allModels' && modelsAdded) {
viewer.fitCameraToModels(viewer.models, 0, true);
initialCameraSet.current = true;
}
}, [modelsAdded]);

return <></>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*!
* Copyright 2023 Cognite AS
*/
import { type ReactElement, useEffect, useRef } from 'react';
import { useReveal } from '../RevealContainer/RevealContext';
import { type Image360Collection } from '@cognite/reveal';

type Image360CollectionContainerProps = {
siteId: string;
onLoad?: () => void;
};

export function Image360CollectionContainer({
siteId,
onLoad
}: Image360CollectionContainerProps): ReactElement {
const modelRef = useRef<Image360Collection>();
const viewer = useReveal();

useEffect(() => {
add360Collection().catch(console.error);
return remove360Collection;
}, [siteId]);

return <></>;

async function add360Collection(): Promise<void> {
const image360Collection = await viewer.add360ImageSet('events', { site_id: siteId });
modelRef.current = image360Collection;
onLoad?.();
}

function remove360Collection(): void {
if (modelRef.current === undefined) return;
viewer.remove360ImageSet(modelRef.current);
modelRef.current = undefined;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*!
* Copyright 2023 Cognite AS
*/

import { type CognitePointCloudModel, type AddModelOptions } from '@cognite/reveal';
import { useEffect, useRef, type ReactElement } from 'react';
import { type Matrix4 } from 'three';
import { useReveal } from '../RevealContainer/RevealContext';

type Cognite3dModelProps = {
addModelOptions: AddModelOptions;
transform?: Matrix4;
onLoad?: () => void;
};

export function PointCloudContainer({
addModelOptions,
transform,
onLoad
}: Cognite3dModelProps): ReactElement {
const modelRef = useRef<CognitePointCloudModel>();
const viewer = useReveal();
const { modelId, revisionId } = addModelOptions;

useEffect(() => {
addModel(modelId, revisionId, transform).catch(console.error);
return removeModel;
}, [modelId, revisionId]);

useEffect(() => {
if (modelRef.current === undefined || transform === undefined) return;
modelRef.current.setModelTransformation(transform);
}, [transform]);

return <></>;

async function addModel(modelId: number, revisionId: number, transform?: Matrix4): Promise<void> {
const pointCloudModel = await viewer.addPointCloudModel({ modelId, revisionId });
if (transform !== undefined) {
pointCloudModel.setModelTransformation(transform);
}
modelRef.current = pointCloudModel;
onLoad?.();
}

function removeModel(): void {
if (modelRef.current === undefined || !viewer.models.includes(modelRef.current)) return;
viewer.removeModel(modelRef.current);
modelRef.current = undefined;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*!
* Copyright 2023 Cognite AS
*/
import { createContext } from 'react';

export type ModelsLoadingState = {
modelsAdded: boolean;
setModelsAdded: (value: boolean) => void;
};
export const ModelsLoadingStateContext = createContext<ModelsLoadingState>({
modelsAdded: false,
setModelsAdded: () => {}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*!
* Copyright 2023 Cognite AS
*/
import { useRef, type ReactElement, useContext, useState, useEffect } from 'react';
import { type Cognite3DViewer } from '@cognite/reveal';
import { ModelsLoadingStateContext } from './ModelsLoadingContext';
import { CadModelContainer } from '../CadModelContainer/CadModelContainer';
import { PointCloudContainer } from '../PointCloudContainer/PointCloudContainer';
import { Image360CollectionContainer } from '../Image360CollectionContainer/Image360CollectionContainer';
import { useReveal } from '../RevealContainer/RevealContext';
import {
type AddReveal3DModelOptions,
type AddImageCollection360Options,
type TypedReveal3DModel,
type AddResourceOptions
} from './types';

export type Reveal3DResourcesProps = {
resources: AddResourceOptions[];
};

export const Reveal3DResources = ({ resources }: Reveal3DResourcesProps): ReactElement => {
const [reveal3DModels, setReveal3DModels] = useState<TypedReveal3DModel[]>([]);
const { setModelsAdded } = useContext(ModelsLoadingStateContext);
const viewer = useReveal();
const numModelsLoaded = useRef(0);

useEffect(() => {
getTypedModels(resources, viewer).then(setReveal3DModels).catch(console.error);
}, [resources]);

const image360CollectionAddOptions = resources.filter(
(resource): resource is AddImageCollection360Options =>
(resource as AddImageCollection360Options).siteId !== undefined
);

const onModelLoaded = (): void => {
numModelsLoaded.current += 1;
if (numModelsLoaded.current === resources.length) {
setModelsAdded(true);
}
};

return (
<>
{reveal3DModels
.filter(({ type }) => type === 'cad')
.map((addModelOption, index) => {
return (
<CadModelContainer
key={`${addModelOption.modelId}/${addModelOption.revisionId}/${index}`}
addModelOptions={addModelOption}
transform={addModelOption.transform}
onLoad={onModelLoaded}
/>
);
})}
{reveal3DModels
.filter(({ type }) => type === 'pointcloud')
.map((addModelOption, index) => {
return (
<PointCloudContainer
key={`${addModelOption.modelId}/${addModelOption.revisionId}/${index}`}
addModelOptions={addModelOption}
transform={addModelOption.transform}
onLoad={onModelLoaded}
/>
);
})}
{image360CollectionAddOptions.map((addModelOption) => {
return (
<Image360CollectionContainer
key={`${addModelOption.siteId}`}
siteId={addModelOption.siteId}
onLoad={onModelLoaded}
/>
);
})}
</>
);
};

async function getTypedModels(
resources: AddResourceOptions[],
viewer: Cognite3DViewer
): Promise<TypedReveal3DModel[]> {
return await Promise.all(
resources
.filter(
(resource): resource is AddReveal3DModelOptions =>
(resource as AddReveal3DModelOptions).modelId !== undefined &&
(resource as AddReveal3DModelOptions).revisionId !== undefined
)
.map(async (addModelOptions) => {
const type = await viewer.determineModelType(
addModelOptions.modelId,
addModelOptions.revisionId
);
const typedModel: TypedReveal3DModel = { ...addModelOptions, type };
return typedModel;
})
);
}
15 changes: 15 additions & 0 deletions react-components/src/components/Reveal3DResources/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*!
* Copyright 2023 Cognite AS
*/

import { type AddModelOptions, type SupportedModelTypes } from '@cognite/reveal';
import { type Matrix4 } from 'three';

export type AddImageCollection360Options = {
siteId: string;
};

export type AddResourceOptions = AddReveal3DModelOptions | AddImageCollection360Options;

export type AddReveal3DModelOptions = AddModelOptions & { transform?: Matrix4 };
export type TypedReveal3DModel = AddReveal3DModelOptions & { type: SupportedModelTypes | '' };
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,15 @@ import { useEffect, useRef, type ReactNode, useState, type ReactElement } from '
import { Cognite3DViewer } from '@cognite/reveal';
import { RevealContext } from './RevealContext';
import { type Color } from 'three';
import { ModelsLoadingStateContext } from '../Reveal3DResources/ModelsLoadingContext';

type RevealContainerProps = {
color?: Color;
sdk: CogniteClient;
children?: ReactNode;
};

export default function RevealContainer({
children,
sdk,
color
}: RevealContainerProps): ReactElement {
export function RevealContainer({ children, sdk, color }: RevealContainerProps): ReactElement {
const [viewer, setViewer] = useState<Cognite3DViewer>();
const revealDomElementRef = useRef<HTMLDivElement>(null);

Expand All @@ -36,7 +33,9 @@ export default function RevealContainer({
if (viewer === undefined) return <></>;
return (
<>
<RevealContext.Provider value={viewer}>{children}</RevealContext.Provider>
<RevealContext.Provider value={viewer}>
<ModelsLoadingProvider>{children}</ModelsLoadingProvider>
</RevealContext.Provider>
</>
);
}
Expand All @@ -57,3 +56,13 @@ export default function RevealContainer({
setViewer(undefined);
}
}

function ModelsLoadingProvider({ children }: { children?: ReactNode }): ReactElement {
const [modelsLoading, setModelsLoading] = useState(false);
return (
<ModelsLoadingStateContext.Provider
value={{ modelsAdded: modelsLoading, setModelsAdded: setModelsLoading }}>
{children}
</ModelsLoadingStateContext.Provider>
);
}
7 changes: 0 additions & 7 deletions react-components/src/components/index.ts

This file was deleted.

Loading

0 comments on commit d2ca002

Please sign in to comment.