Skip to content

Commit

Permalink
feat(react-components): new hook for getting scenes data from FDM (#4013
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Savokr authored Dec 12, 2023
1 parent 70aef06 commit a714e1b
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"KEYBOARD_RIGHT": "Right",
"KEYBOARD_UP": "Up",
"LAYERS_FILTER_TOOLTIP": "Filter 3D resource layers",
"SCENE_SELECT_HEADER": "Select 3D location",
"MEASUREMENTS_TOOLTIP": "Distance measuring tool",
"MOUSE_INSTRUCTIONS": "Click+drag",
"MOUSE_NAVIGATION_DESCRIPTION": "Click and drag to rotate, and pan the view. Use mouse wheel to zoom the view. Left click to select",
Expand Down
51 changes: 51 additions & 0 deletions react-components/src/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,54 @@ export type Reveal360AnnotationAssetData = {
asset: Asset;
assetAnnotationImage360Info: AssetAnnotationImage360Info;
};

export type Transformation3d = {
translationX: number;
translationY: number;
translationZ: number;
eulerRotationX: number;
eulerRotationY: number;
eulerRotationZ: number;
scaleX: number;
scaleY: number;
scaleZ: number;
};

export type SceneModelsProperties = Transformation3d & {
revisionId: number;
};

export type SceneConfigurationProperties = {
name: string;
cameraTranslationX: number;
cameraTranslationY: number;
cameraTranslationZ: number;
cameraEulerRotationX: number;
cameraEulerRotationY: number;
cameraEulerRotationZ: number;
};

export type SkyboxProperties = {
label: string;
isSpherical: boolean;
file: string;
};

export type GroundPlaneProperties = {
file: string;
label: string;
wrapping: string;
};

export type Cdf3dRevisionProperties = {
revisionId: number;
scaleX: number;
scaleY: number;
scaleZ: number;
translationX: number;
translationY: number;
translationZ: number;
eulerRotationX: number;
eulerRotationY: number;
eulerRotationZ: number;
};
159 changes: 159 additions & 0 deletions react-components/src/hooks/use3dScenes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*!
* Copyright 2023 Cognite AS
*/

import { type QueryFunction, useQuery, type UseQueryResult } from '@tanstack/react-query';
import { useSDK } from '../components/RevealContainer/SDKProvider';
import { type CogniteClient } from '@cognite/sdk';
import { useMemo } from 'react';
import { type EdgeItem, FdmSDK, type Query } from '../utilities/FdmSDK';
import { type AddReveal3DModelOptions } from '..';
import { type Cdf3dRevisionProperties } from './types';
import { Euler, Matrix4, Quaternion, Vector3 } from 'three';
import { CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal';

export const use3dScenes = (
userSdk?: CogniteClient
): UseQueryResult<Record<string, AddReveal3DModelOptions[]>> => {
const sdk = useSDK(userSdk);

const fdmSdk = useMemo(() => new FdmSDK(sdk), [sdk]);

const queryFunction: QueryFunction<Record<string, AddReveal3DModelOptions[]>> = async () => {
const scenesQuery = createGetScenesQuery();

try {
const scenesQueryResult = await fdmSdk.queryNodesAndEdges(scenesQuery);

const scenesMap: Record<string, AddReveal3DModelOptions[]> =
scenesQueryResult.items.sceneModels.reduce(
(acc, item) => {
const edge = item as EdgeItem;

const { externalId } = edge.startNode;

const properties = Object.values(
Object.values(edge.properties)[0] as Record<string, unknown>
)[0] as Cdf3dRevisionProperties;
const sceneModels = acc[externalId];

const newModelId = Number(edge.endNode.externalId);
const newModelRevisionId = Number(properties?.revisionId);

if (isNaN(newModelId) || isNaN(newModelRevisionId)) {
return acc;
}

const newModel = {
modelId: newModelId,
revisionId: newModelRevisionId,
transform: new Matrix4()
.compose(
new Vector3(
properties.translationX,
properties.translationY,
properties.translationZ
),
new Quaternion().setFromEuler(
new Euler(
properties.eulerRotationX,
properties.eulerRotationY,
properties.eulerRotationZ
)
),
new Vector3(properties.scaleX, properties.scaleY, properties.scaleZ)
)
.premultiply(CDF_TO_VIEWER_TRANSFORMATION)
};

if (sceneModels !== undefined) {
sceneModels.push(newModel);
} else {
acc[externalId] = [newModel];
}

return acc;
},
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
{} as Record<string, AddReveal3DModelOptions[]>
);

return scenesMap;
} catch (error) {
console.warn("Scene space doesn't exist or has no scenes with 3D models");
return {};
}
};

return useQuery<Record<string, AddReveal3DModelOptions[]>>(
['cdf', '3d', 'scenes'],
queryFunction
);
};

function createGetScenesQuery(limit: number = 100): Query {
return {
with: {
scenes: {
nodes: {
filter: {
hasData: [
{
type: 'view',
space: 'scene_space',
externalId: 'SceneConfiguration',
version: 'v4'
}
]
}
},
limit
},
sceneModels: {
edges: {
from: 'scenes',
maxDistance: 1,
direction: 'outwards',
filter: {
equals: {
property: ['edge', 'type'],
value: {
space: 'scene_space',
externalId: 'SceneConfiguration.cdf3dModels'
}
}
}
},
limit
}
},
select: {
scenes: {
sources: [
{
source: {
type: 'view',
space: 'scene_space',
externalId: 'SceneConfiguration',
version: 'v4'
},
properties: ['*']
}
]
},
sceneModels: {
sources: [
{
source: {
type: 'view',
space: 'scene_space',
externalId: 'Cdf3dRevisionProperties',
version: '2190c9b6f5cb82'
},
properties: ['*']
}
]
}
}
};
}
1 change: 1 addition & 0 deletions react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export {
type FdmNodeDataResult
} from './hooks/useClickedNode';
export { useCameraNavigation } from './hooks/useCameraNavigation';
export { use3dScenes } from './hooks/use3dScenes';
export { useMappedEdgesForRevisions } from './components/NodeCacheProvider/NodeCacheProvider';
export { useIsRevealInitialized } from './hooks/useIsRevealInitialized';
export { use3dNodeByExternalId } from './hooks/use3dNodeByExternalId';
Expand Down
1 change: 1 addition & 0 deletions react-components/src/utilities/FdmSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type NodeResultSetExpression = {
from?: string;
through?: ViewPropertyReference;
chainTo?: EdgeDirection;
direction?: 'outwards' | 'inwards';
};
};

Expand Down
118 changes: 118 additions & 0 deletions react-components/stories/ScenesSelector.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*!
* Copyright 2023 Cognite AS
*/

import type { Meta, StoryObj } from '@storybook/react';
import {
CadModelContainer,
RevealToolbar,
withSuppressRevealEvents,
withCameraStateUrlParam,
useGetCameraStateFromUrlParam,
useCameraNavigation,
Reveal3DResources,
type AddReveal3DModelOptions
} from '../src';
import { Color } from 'three';
import styled from 'styled-components';
import { Button, Dropdown, Menu, ToolBar } from '@cognite/cogs.js';
import { type ReactElement, useEffect, useState } from 'react';
import { signalStoryReadyForScreenshot } from './utilities/signalStoryReadyForScreenshot';
import { RevealStoryContainer } from './utilities/RevealStoryContainer';
import { getAddModelOptionsFromUrl } from './utilities/getAddModelOptionsFromUrl';
import { use3dScenes } from '../src';

const meta = {
title: 'Example/SceneSelector',
component: CadModelContainer,
tags: ['autodocs']
} satisfies Meta<typeof CadModelContainer>;

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

const MyCustomToolbar = styled(withSuppressRevealEvents(withCameraStateUrlParam(ToolBar)))`
position: absolute;
left: 20px;
top: 70px;
`;

const SceneButton = ({
setResources
}: {
setResources: (value: AddReveal3DModelOptions[]) => void;
}): ReactElement => {
const { data } = use3dScenes();
const [selectedScene, setSelectedScene] = useState(Object.keys(data ?? {})[0]);

useEffect(() => {
if (data === undefined) return;

setResources(data[selectedScene]);

return () => {
setResources([]);
};
}, [selectedScene]);

return (
<Dropdown
placement="right-start"
content={
<Menu>
<Menu.Header>Select 3D location</Menu.Header>
{Object.keys(data ?? {}).map((scene) => {
if (data === undefined) return <></>;

return (
<Menu.Item
key={scene}
toggled={selectedScene === scene}
onClick={() => {
setSelectedScene(scene);
}}>
{scene}
</Menu.Item>
);
})}
</Menu>
}>
<Button icon="World" type="ghost" />
</Dropdown>
);
};

export const Main: Story = {
args: {
addModelOptions: getAddModelOptionsFromUrl('/primitives')
},
render: () => {
const [resources, setResources] = useState<AddReveal3DModelOptions[]>([]);

return (
<RevealStoryContainer color={new Color(0x4a4a4a)}>
<FitToUrlCameraState />
<MyCustomToolbar>
<SceneButton setResources={setResources} />
<RevealToolbar.FitModelsButton />
<RevealToolbar.SlicerButton />
</MyCustomToolbar>
<Reveal3DResources resources={resources} />
</RevealStoryContainer>
);
}
};

function FitToUrlCameraState(): ReactElement {
const getCameraState = useGetCameraStateFromUrlParam();
const cameraNavigation = useCameraNavigation();

useEffect(() => {
signalStoryReadyForScreenshot();
const currentCameraState = getCameraState();
if (currentCameraState === undefined) return;
cameraNavigation.fitCameraToState(currentCameraState);
}, []);

return <></>;
}

0 comments on commit a714e1b

Please sign in to comment.