diff --git a/README.md b/README.md
index c878379c8ec..c0556afc139 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ that can be explored in the browser.
### Building on Macbook M1
-Building Reveal on Macbook M1 migth require some special care.
+Building Reveal on Macbook M1 might require some special care.
If you experience issues during the `yarn`-stage in `viewer/`, e.g.
```
diff --git a/examples/src/pages/Viewer.tsx b/examples/src/pages/Viewer.tsx
index 8ce60c90795..fd59fa94505 100644
--- a/examples/src/pages/Viewer.tsx
+++ b/examples/src/pages/Viewer.tsx
@@ -6,7 +6,6 @@ import Stats from 'stats.js';
import { useEffect, useRef } from 'react';
import { CanvasWrapper } from '../components/styled';
import * as THREE from 'three';
-import { CogniteClient } from '@cognite/sdk';
import dat from 'dat.gui';
import {
Cognite3DViewer,
@@ -29,7 +28,6 @@ import { CameraUI } from '../utils/CameraUI';
import { PointCloudUi } from '../utils/PointCloudUi';
import { ModelUi } from '../utils/ModelUi';
import { NodeTransformUI } from '../utils/NodeTransformUI';
-import { createSDKFromEnvironment, createSDKFromToken } from '../utils/example-helpers';
import { PointCloudClassificationFilterUI } from '../utils/PointCloudClassificationFilterUI';
import { PointCloudObjectStylingUI } from '../utils/PointCloudObjectStylingUI';
import { CustomCameraManager } from '../utils/CustomCameraManager';
@@ -38,6 +36,7 @@ import { Image360UI } from '../utils/Image360UI';
import { Image360StylingUI } from '../utils/Image360StylingUI';
import { LoadGltfUi } from '../utils/LoadGltfUi';
import { createFunnyButton } from '../utils/PageVariationUtils';
+import { getCogniteClient } from '../utils/example-helpers';
window.THREE = THREE;
(window as any).reveal = reveal;
@@ -51,7 +50,7 @@ export function Viewer() {
// Check in order to avoid double initialization of everything, especially dat.gui.
// See https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects for why its called twice.
if (!canvasWrapperRef.current) {
- return () => {};
+ return;
}
const gui = new dat.GUI({ width: Math.min(500, 0.8 * window.innerWidth) });
@@ -76,18 +75,7 @@ export function Viewer() {
window.history.pushState({}, '', url.toString());
}
- let client: CogniteClient;
- if (project && overrideToken) {
- client = createSDKFromToken('reveal.example.example', project, overrideToken);
- } else if (project && environment) {
- client = await createSDKFromEnvironment('reveal.example.example', project, environment);
- } else {
- client = new CogniteClient({
- appId: 'reveal.example.example',
- project: 'dummy',
- getToken: async () => 'dummy'
- });
- }
+ let client = await getCogniteClient({ project, environment, overrideToken });
const edlEnabled = (urlParams.get('edl') ?? 'true') === 'true';
const progress = (itemsLoaded: number, itemsRequested: number, itemsCulled: number) => {
@@ -502,5 +490,6 @@ export function Viewer() {
viewer?.dispose();
};
}, []);
+
return ;
}
diff --git a/examples/src/utils/example-helpers.ts b/examples/src/utils/example-helpers.ts
index 89a15795b0b..c232c8bed1f 100644
--- a/examples/src/utils/example-helpers.ts
+++ b/examples/src/utils/example-helpers.ts
@@ -203,3 +203,27 @@ export async function createSDKFromEnvironment(
await client.authenticate();
return client;
}
+
+export const getCogniteClient = async ({
+ project,
+ environment,
+ overrideToken
+}: {
+ project: string | null;
+ environment: string | null;
+ overrideToken: string | null;
+}): Promise => {
+ if (project !== null && overrideToken !== null) {
+ return createSDKFromToken('reveal.example.example', project, overrideToken);
+ }
+
+ if (project !== null && environment !== null) {
+ return await createSDKFromEnvironment('reveal.example.example', project, environment);
+ }
+
+ return new CogniteClient({
+ appId: 'reveal.example.example',
+ project: 'dummy',
+ getToken: async () => 'dummy'
+ });
+};
diff --git a/react-components/package.json b/react-components/package.json
index 9c3e826b7c9..4ecb75faf42 100644
--- a/react-components/package.json
+++ b/react-components/package.json
@@ -1,6 +1,6 @@
{
"name": "@cognite/reveal-react-components",
- "version": "0.10.0",
+ "version": "0.12.0",
"exports": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
@@ -35,12 +35,12 @@
"@cognite/cogs.js": "^9.17.0",
"@cognite/reveal": "4.4.0",
"@cognite/sdk": "^8.2.0",
- "@storybook/addon-essentials": "7.2.3",
- "@storybook/addon-interactions": "7.2.3",
- "@storybook/addon-links": "7.2.3",
- "@storybook/blocks": "7.2.3",
- "@storybook/react": "7.2.3",
- "@storybook/react-webpack5": "7.2.3",
+ "@storybook/addon-essentials": "7.3.1",
+ "@storybook/addon-interactions": "7.3.1",
+ "@storybook/addon-links": "7.3.1",
+ "@storybook/blocks": "7.3.1",
+ "@storybook/react": "7.3.1",
+ "@storybook/react-webpack5": "7.3.1",
"@storybook/testing-library": "0.2.0",
"@tanstack/react-query-devtools": "^4.29.19",
"@types/lodash": "^4.14.190",
@@ -50,7 +50,7 @@
"@types/three": "0.155.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"eslint": "^8.0.1",
- "eslint-config-prettier": "^8.8.0",
+ "eslint-config-prettier": "^9.0.0",
"eslint-config-standard-with-typescript": "latest",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.25.2",
@@ -64,7 +64,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-router-dom": "^6.15.0",
- "storybook": "7.2.3",
+ "storybook": "7.3.1",
"style-loader": "^3.3.3",
"styled-components": "5.3.11",
"three": "0.155.0",
@@ -82,4 +82,4 @@
"@tanstack/react-query": "^4.29.19",
"lodash": "^4.17.21"
}
-}
+}
\ No newline at end of file
diff --git a/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx b/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx
index 21177fdf543..b327d685d18 100644
--- a/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx
+++ b/react-components/src/components/Image360CollectionContainer/Image360CollectionContainer.tsx
@@ -38,7 +38,11 @@ export function Image360CollectionContainer({
return collection;
}
- return await viewer.add360ImageSet('events', { site_id: siteId });
+ return await viewer.add360ImageSet(
+ 'events',
+ { site_id: siteId },
+ { preMultipliedRotation: false }
+ );
}
}
diff --git a/react-components/src/components/Image360Details/Image360Details.tsx b/react-components/src/components/Image360Details/Image360Details.tsx
new file mode 100644
index 00000000000..e26bd7edd33
--- /dev/null
+++ b/react-components/src/components/Image360Details/Image360Details.tsx
@@ -0,0 +1,89 @@
+/*!
+ * Copyright 2023 Cognite AS
+ */
+
+import { useState, type ReactElement, useCallback, useEffect } from 'react';
+import styled from 'styled-components';
+import { Image360HistoricalDetails } from '../Image360HistoricalDetails/Image360HistoricalDetails';
+import { useReveal } from '../..';
+import { type Image360 } from '@cognite/reveal';
+import { Button } from '@cognite/cogs.js';
+
+export function Image360Details(): ReactElement {
+ const viewer = useReveal();
+ const [enteredEntity, setEnteredEntity] = useState();
+ const [is360HistoricalPanelExpanded, setIs360HistoricalPanelExpanded] = useState(false);
+ const handleExpand = useCallback((isExpanded: boolean) => {
+ setIs360HistoricalPanelExpanded(isExpanded);
+ }, []);
+
+ const clearEnteredImage360 = (): void => {
+ setEnteredEntity(undefined);
+ };
+
+ const exitImage360Image = (): void => {
+ viewer.exit360Image();
+ };
+
+ const collections = viewer.get360ImageCollections();
+
+ useEffect(() => {
+ collections.forEach((collection) => {
+ collection.on('image360Entered', setEnteredEntity);
+ collection.on('image360Exited', clearEnteredImage360);
+ });
+ return () => {
+ collections.forEach((collection) => {
+ collection.off('image360Entered', setEnteredEntity);
+ collection.off('image360Exited', clearEnteredImage360);
+ });
+ };
+ }, [viewer, collections]);
+
+ return (
+ <>
+ {enteredEntity !== undefined && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ >
+ );
+}
+
+const StyledExitButton = styled(Button)`
+ border-radius: 8px;
+`;
+
+const ExitButtonContainer = styled.div`
+ position: absolute;
+ right: 20px;
+ top: 20px;
+ background-color: #ffffff;
+ height: 36px;
+ width: 36px;
+ border-radius: 8px;
+ outline: none;
+`;
+
+const Image360HistoricalPanel = styled.div<{ isExpanded: boolean }>`
+ position: absolute;
+ bottom: ${({ isExpanded }) => (isExpanded ? '0px' : '40px')};
+ display: flex;
+ flex-direction: column;
+ height: fit-content;
+ width: fit-content;
+ max-width: 100%;
+ min-width: fill-available;
+ transition: transform 0.25s ease-in-out;
+ transform: ${({ isExpanded }) => (isExpanded ? 'translateY(0)' : 'translateY(100%)')};
+`;
diff --git a/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts b/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts
new file mode 100644
index 00000000000..079911708af
--- /dev/null
+++ b/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts
@@ -0,0 +1,283 @@
+/*!
+ * Copyright 2023 Cognite AS
+ */
+
+import { type Node3D, type CogniteClient } from '@cognite/sdk';
+import { type EdgeItem, type FdmSDK } from '../../utilities/FdmSDK';
+import { RevisionFdmNodeCache } from './RevisionFdmNodeCache';
+import {
+ type FdmEdgeWithNode,
+ type Fdm3dNodeData,
+ type FdmCadEdge,
+ type RevisionKey,
+ type RevisionTreeIndex,
+ type FdmKey,
+ type FdmId,
+ type RevisionId,
+ type NodeId,
+ type ModelNodeIdKey,
+ type ModelId
+} from './types';
+import {
+ type InModel3dEdgeProperties,
+ SYSTEM_3D_EDGE_SOURCE,
+ SYSTEM_SPACE_3D_SCHEMA
+} from '../../utilities/globalDataModels';
+
+import { partition } from 'lodash';
+
+import assert from 'assert';
+import { fetchNodesForNodeIds } from './requests';
+
+export type ModelRevisionKey = `${number}-${number}`;
+export type ModelRevisionToEdgeMap = Map;
+
+export class FdmNodeCache {
+ private readonly _revisionNodeCaches = new Map();
+
+ private readonly _cdfClient: CogniteClient;
+ private readonly _fdmClient: FdmSDK;
+
+ private readonly _completeRevisions = new Set();
+
+ public constructor(cdfClient: CogniteClient, fdmClient: FdmSDK) {
+ this._cdfClient = cdfClient;
+ this._fdmClient = fdmClient;
+ }
+
+ public async getAllMappingExternalIds(
+ modelRevisionIds: Array<{ modelId: number; revisionId: number }>
+ ): Promise {
+ const [cachedRevisionIds, nonCachedRevisionIds] = partition(modelRevisionIds, (ids) => {
+ const key = createRevisionKey(ids.modelId, ids.revisionId);
+ return this._completeRevisions.has(key);
+ });
+
+ const cachedEdges = cachedRevisionIds.map((id) => this.getCachedEdgesForRevision(id));
+
+ const revisionToEdgesMap = await this.getRevisionToEdgesMap(nonCachedRevisionIds);
+
+ this.writeRevisionDataToCache(revisionToEdgesMap);
+
+ cachedEdges.forEach(([revisionKey, edges]) => {
+ revisionToEdgesMap.set(revisionKey, edges);
+ });
+
+ return revisionToEdgesMap;
+ }
+
+ private getCachedEdgesForRevision(id: {
+ modelId: number;
+ revisionId: number;
+ }): [RevisionKey, FdmEdgeWithNode[]] {
+ const revisionCache = this.getOrCreateRevisionCache(id.modelId, id.revisionId);
+ const revisionKey = createRevisionKey(id.modelId, id.revisionId);
+ const cachedRevisionEdges = revisionCache.getAllEdges();
+ return [revisionKey, cachedRevisionEdges];
+ }
+
+ private writeRevisionDataToCache(modelMap: Map): void {
+ for (const [revisionKey, data] of modelMap.entries()) {
+ const [modelId, revisionId] = revisionKeyToIds(revisionKey);
+ const revisionCache = this.getOrCreateRevisionCache(modelId, revisionId);
+
+ data.forEach((edgeAndNode) => {
+ revisionCache.insertTreeIndexMappings(edgeAndNode.node.treeIndex, edgeAndNode);
+ });
+
+ this._completeRevisions.add(revisionKey);
+ }
+ }
+
+ private async getRevisionToEdgesMap(
+ modelRevisionIds: Array<{ modelId: number; revisionId: number }>
+ ): Promise