diff --git a/react-components/src/components/RevealToolbar/LayersButton.tsx b/react-components/src/components/RevealToolbar/LayersButton.tsx
new file mode 100644
index 00000000000..8245db5bac9
--- /dev/null
+++ b/react-components/src/components/RevealToolbar/LayersButton.tsx
@@ -0,0 +1,30 @@
+/*!
+ * Copyright 2023 Cognite AS
+ */
+
+import { type ReactElement, useState } from 'react';
+import { Button, Dropdown } from '@cognite/cogs.js';
+import { LayersContainer } from './LayersContainer/LayersContainer';
+
+export const LayersButton = (): ReactElement => {
+ const [layersEnabled, setLayersEnabled] = useState(false);
+ const showLayers = (): void => {
+ setLayersEnabled(!layersEnabled);
+ };
+
+ return (
+ }
+ visible={layersEnabled}
+ placement="auto">
+
+
+ );
+};
diff --git a/react-components/src/components/RevealToolbar/LayersContainer/CadModelLayersContainer.tsx b/react-components/src/components/RevealToolbar/LayersContainer/CadModelLayersContainer.tsx
new file mode 100644
index 00000000000..aabd4eda4d6
--- /dev/null
+++ b/react-components/src/components/RevealToolbar/LayersContainer/CadModelLayersContainer.tsx
@@ -0,0 +1,101 @@
+/*!
+ * Copyright 2023 Cognite AS
+ */
+
+import React, { type ReactElement, useState } from 'react';
+import { useReveal } from '../../RevealContainer/RevealContext';
+import { type CogniteCadModel } from '@cognite/reveal';
+import { Checkbox, Flex, Menu } from '@cognite/cogs.js';
+import { StyledChipCount, StyledLabel, StyledSubMenu } from './elements';
+import { use3DModelName } from '../../../hooks/use3DModelName';
+import uniqueId from 'lodash/uniqueId';
+
+export const CadModelLayersContainer = (): ReactElement => {
+ const viewer = useReveal();
+ const cadModels = viewer.models.filter((model) => model.type === 'cad');
+ const cadModelIds = cadModels.map((model) => model.modelId);
+
+ const modelName = use3DModelName(cadModelIds);
+
+ const [selectedCadModels, setSelectedCadModels] = useState<
+ Array<{ model: CogniteCadModel; isToggled: boolean; name: string }>
+ >(
+ cadModels.map((model, index) => ({
+ model: model as CogniteCadModel,
+ isToggled: (model as CogniteCadModel).visible,
+ name: modelName?.data?.[index] ?? 'No model name'
+ }))
+ );
+
+ const [allCadModelVisible, setAllCadModelVisible] = useState(true);
+ const [indeterminate, setIndeterminate] = useState(false);
+
+ const count = selectedCadModels.length.toString();
+
+ const handleCadModelVisibility = (model: CogniteCadModel): void => {
+ selectedCadModels.map((data) => {
+ if (data.model === model) {
+ return {
+ ...data,
+ isToggled: !data.isToggled
+ };
+ } else {
+ return data;
+ }
+ });
+ model.visible = !model.visible;
+ viewer.requestRedraw();
+ setSelectedCadModels([...selectedCadModels]);
+ setIndeterminate(selectedCadModels.some((data) => !data.isToggled));
+ setAllCadModelVisible(!selectedCadModels.every((data) => !data.isToggled));
+ };
+
+ const handleAllCadModelsVisibility = (visible: boolean): void => {
+ selectedCadModels.forEach((data) => {
+ data.isToggled = visible;
+ data.model.visible = visible;
+ });
+ viewer.requestRedraw();
+ setAllCadModelVisible(visible);
+ setIndeterminate(false);
+ setSelectedCadModels([...selectedCadModels]);
+ };
+
+ const cadModelContent = (): React.JSX.Element => {
+ return (
+
+ {selectedCadModels.map((data) => (
+ void }) => {
+ e.stopPropagation();
+ handleCadModelVisibility(data.model);
+ }
+ }}>
+ {data.name}
+
+ ))}
+
+ );
+ };
+
+ return (
+
+
+ {
+ e.stopPropagation();
+ handleAllCadModelsVisibility(c as boolean);
+ }}
+ />
+ CAD models
+
+
+
+ );
+};
diff --git a/react-components/src/components/RevealToolbar/LayersContainer/Image360LayersContainer.tsx b/react-components/src/components/RevealToolbar/LayersContainer/Image360LayersContainer.tsx
new file mode 100644
index 00000000000..07372d5f904
--- /dev/null
+++ b/react-components/src/components/RevealToolbar/LayersContainer/Image360LayersContainer.tsx
@@ -0,0 +1,92 @@
+/*!
+ * Copyright 2023 Cognite AS
+ */
+
+import React, { type ReactElement, useState } from 'react';
+import { useReveal } from '../../RevealContainer/RevealContext';
+import { Checkbox, Flex, Menu } from '@cognite/cogs.js';
+import { StyledChipCount, StyledLabel, StyledSubMenu } from './elements';
+import { type Image360Collection } from '@cognite/reveal';
+import uniqueId from 'lodash/uniqueId';
+
+export const Image360CollectionLayerContainer = (): ReactElement => {
+ const viewer = useReveal();
+ const image360Collection = viewer.get360ImageCollections();
+
+ const [selectedImage360Collection, setSelectedImage360Collection] = useState<
+ Array<{ image360: Image360Collection; isToggled: boolean }>
+ >(
+ image360Collection.map((image360Collection) => ({
+ image360: image360Collection,
+ isToggled: true
+ }))
+ );
+
+ const [all360ImagesVisible, setAll360ImagesVisible] = useState(true);
+ const [indeterminate, setIndeterminate] = useState(false);
+
+ const count = image360Collection.length.toString();
+
+ const handle360ImagesVisibility = (image360: Image360Collection): void => {
+ selectedImage360Collection.map((data) => {
+ if (data.image360 === image360) {
+ data.isToggled = !data.isToggled;
+ image360.setIconsVisibility(data.isToggled);
+ }
+ return data;
+ });
+ viewer.requestRedraw();
+ setSelectedImage360Collection([...selectedImage360Collection]);
+ setIndeterminate(selectedImage360Collection.some((data) => !data.isToggled));
+ setAll360ImagesVisible(!selectedImage360Collection.every((data) => !data.isToggled));
+ };
+
+ const handleAll360ImagesVisibility = (visible: boolean): void => {
+ [...selectedImage360Collection].forEach((data) => {
+ data.isToggled = visible;
+ data.image360.setIconsVisibility(data.isToggled);
+ });
+ viewer.requestRedraw();
+ setAll360ImagesVisible(visible);
+ setIndeterminate(false);
+ setSelectedImage360Collection([...selectedImage360Collection]);
+ };
+
+ const image360Content = (): React.JSX.Element => {
+ return (
+
+ {selectedImage360Collection.map((data) => (
+ void }) => {
+ e.stopPropagation();
+ handle360ImagesVisibility(data.image360);
+ }
+ }}>
+ {data.image360.label}
+
+ ))}
+
+ );
+ };
+
+ return (
+
+
+ {
+ e.stopPropagation();
+ handleAll360ImagesVisibility(c as boolean);
+ }}
+ />
+ 360 images
+
+
+
+ );
+};
diff --git a/react-components/src/components/RevealToolbar/LayersContainer/LayersContainer.tsx b/react-components/src/components/RevealToolbar/LayersContainer/LayersContainer.tsx
new file mode 100644
index 00000000000..adc64c3b94f
--- /dev/null
+++ b/react-components/src/components/RevealToolbar/LayersContainer/LayersContainer.tsx
@@ -0,0 +1,34 @@
+/*!
+ * Copyright 2023 Cognite AS
+ */
+
+import { type ReactElement } from 'react';
+
+import { Menu } from '@cognite/cogs.js';
+import styled from 'styled-components';
+
+import { CadModelLayersContainer } from './CadModelLayersContainer';
+import { PointCloudLayersContainer } from './PointCloudLayersContainer';
+import { Image360CollectionLayerContainer } from './Image360LayersContainer';
+
+export const LayersContainer = (): ReactElement => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const Container = styled.div`
+ position: relative;
+ top: 40px;
+`;
+
+const StyledMenu = styled(Menu)`
+ padding: 6px;
+ width: 214px;
+`;
diff --git a/react-components/src/components/RevealToolbar/LayersContainer/PointCloudLayersContainer.tsx b/react-components/src/components/RevealToolbar/LayersContainer/PointCloudLayersContainer.tsx
new file mode 100644
index 00000000000..c8c573861c5
--- /dev/null
+++ b/react-components/src/components/RevealToolbar/LayersContainer/PointCloudLayersContainer.tsx
@@ -0,0 +1,97 @@
+/*!
+ * Copyright 2023 Cognite AS
+ */
+
+import React, { type ReactElement, useState } from 'react';
+
+import { useReveal } from '../../RevealContainer/RevealContext';
+import { Checkbox, Flex, Menu } from '@cognite/cogs.js';
+import { StyledChipCount, StyledLabel, StyledSubMenu } from './elements';
+import { type CognitePointCloudModel } from '@cognite/reveal';
+import { use3DModelName } from '../../../hooks/use3DModelName';
+import uniqueId from 'lodash/uniqueId';
+
+export const PointCloudLayersContainer = (): ReactElement => {
+ const viewer = useReveal();
+ const pointCloudModels = viewer.models.filter((model) => model.type === 'pointcloud');
+ const pointCloudModelIds = pointCloudModels.map((model) => model.modelId);
+
+ const modelName = use3DModelName(pointCloudModelIds);
+
+ const [selectedPointCloudModels, setSelectedPointCloudModels] = useState<
+ Array<{ model: CognitePointCloudModel; isToggled: boolean; name: string }>
+ >(
+ pointCloudModels.map((model, index) => ({
+ model: model as CognitePointCloudModel,
+ isToggled: (model as CognitePointCloudModel).getDefaultPointCloudAppearance().visible ?? true,
+ name: modelName?.data?.[index] ?? 'No model name'
+ }))
+ );
+
+ const [allPointCloudModelVisible, setAllPointCloudModelVisible] = useState(true);
+ const [indeterminate, setIndeterminate] = useState(false);
+
+ const count = pointCloudModels.length.toString();
+
+ const handlePointCloudVisibility = (model: CognitePointCloudModel): void => {
+ selectedPointCloudModels.map((data) => {
+ if (data.model === model) {
+ data.isToggled = !data.isToggled;
+ model.setDefaultPointCloudAppearance({ visible: data.isToggled });
+ }
+ return data;
+ });
+ viewer.requestRedraw();
+ setSelectedPointCloudModels([...selectedPointCloudModels]);
+ setIndeterminate(selectedPointCloudModels.some((data) => !data.isToggled));
+ setAllPointCloudModelVisible(!selectedPointCloudModels.every((data) => !data.isToggled));
+ };
+
+ const handleAllPointCloudModelsVisibility = (visible: boolean): void => {
+ selectedPointCloudModels.forEach((data) => {
+ data.isToggled = visible;
+ data.model.setDefaultPointCloudAppearance({ visible });
+ });
+ viewer.requestRedraw();
+ setAllPointCloudModelVisible(visible);
+ setSelectedPointCloudModels([...selectedPointCloudModels]);
+ };
+
+ const pointCloudModelContent = (): React.JSX.Element => {
+ return (
+
+ {selectedPointCloudModels.map((data) => (
+ void }) => {
+ e.stopPropagation();
+ handlePointCloudVisibility(data.model);
+ }
+ }}>
+ {data.name}
+
+ ))}
+
+ );
+ };
+
+ return (
+
+
+ {
+ e.stopPropagation();
+ handleAllPointCloudModelsVisibility(c as boolean);
+ }}
+ />
+ Point clouds
+
+
+
+ );
+};
diff --git a/react-components/src/components/RevealToolbar/LayersContainer/elements.ts b/react-components/src/components/RevealToolbar/LayersContainer/elements.ts
new file mode 100644
index 00000000000..2b1de15235b
--- /dev/null
+++ b/react-components/src/components/RevealToolbar/LayersContainer/elements.ts
@@ -0,0 +1,41 @@
+/*!
+ * Copyright 2023 Cognite AS
+ */
+
+import { Checkbox, Chip, Menu } from '@cognite/cogs.js';
+import styled from 'styled-components';
+
+export const StyledCheckbox = styled(Checkbox)`
+ margin-left: 10px;
+`;
+
+export const StyledMenu = styled(Menu)`
+ padding: 6px;
+`;
+
+export const StyledSubMenu = styled(Menu)`
+ box-shadow: none;
+ padding: 8px;
+`;
+
+export const StyledChipCount = styled(Chip)`
+ && {
+ border-radius: 2px;
+ width: fit-content;
+ height: 20px;
+ max-height: 20px;
+ min-height: 20px;
+ min-width: 20px;
+ padding: 4px;
+ }
+`;
+
+export const StyledLabel = styled.div`
+ /* Font */
+ font-family: 'Inter';
+ font-style: normal;
+ font-weight: 500;
+ font-size: 16px;
+ line-height: 20px;
+ color: #000000;
+`;
diff --git a/react-components/src/components/RevealToolbar/RevealToolbar.tsx b/react-components/src/components/RevealToolbar/RevealToolbar.tsx
index 7e0a3e52992..c17b35a63ad 100644
--- a/react-components/src/components/RevealToolbar/RevealToolbar.tsx
+++ b/react-components/src/components/RevealToolbar/RevealToolbar.tsx
@@ -5,6 +5,7 @@
import { type ReactElement, type JSX } from 'react';
import { Button, ToolBar, type ToolBarProps } from '@cognite/cogs.js';
import { FitModelsButton } from './FitModelsButton';
+import { LayersButton } from './LayersButton';
import { SlicerButton } from './SlicerButton';
const defaultStyle: ToolBarProps = {
@@ -17,9 +18,7 @@ const defaultStyle: ToolBarProps = {
const defaultContent = (
<>
-
-
-
+
diff --git a/react-components/src/hooks/use3DModelName.tsx b/react-components/src/hooks/use3DModelName.tsx
new file mode 100644
index 00000000000..d41b4d56073
--- /dev/null
+++ b/react-components/src/hooks/use3DModelName.tsx
@@ -0,0 +1,34 @@
+/*!
+ * Copyright 2023 Cognite AS
+ */
+
+import { type QueryFunction, useQuery, type UseQueryResult } from '@tanstack/react-query';
+import { useSDK } from '../components/RevealContainer/SDKProvider';
+
+export const use3DModelName = (ids: number[]): UseQueryResult => {
+ const sdk = useSDK();
+
+ const queryFunction: QueryFunction = async () => {
+ const modelNamePromises = await Promise.allSettled(
+ ids.map(async (id) => {
+ const model = await sdk.models3D.retrieve(id);
+ return model.name;
+ })
+ );
+
+ const modelResolvedNames: string[] = [];
+ modelNamePromises.forEach((modelNamePromise) => {
+ if (modelNamePromise.status === 'fulfilled') {
+ modelResolvedNames.push(modelNamePromise.value);
+ } else if (modelNamePromise.status === 'rejected') {
+ console.error('Error while retriving Model Name', modelNamePromise.reason);
+ }
+ });
+
+ return modelResolvedNames;
+ };
+
+ const queryResult = useQuery(['cdf', '3d', 'model', ids], queryFunction);
+
+ return queryResult;
+};
diff --git a/react-components/src/index.ts b/react-components/src/index.ts
index 4da6c7a598a..a64b9376a7c 100644
--- a/react-components/src/index.ts
+++ b/react-components/src/index.ts
@@ -29,7 +29,9 @@ export type {
AddReveal3DModelOptions
} from './components/Reveal3DResources/types';
export { RevealToolbar } from './components/RevealToolbar/RevealToolbar';
+export { LayersButton } from './components/RevealToolbar/LayersButton';
export { SlicerButton } from './components/RevealToolbar/SlicerButton';
export { FitModelsButton } from './components/RevealToolbar/FitModelsButton';
export { useFdmAssetMappings } from './hooks/useFdmAssetMappings';
export { type FdmAssetMappingsConfig } from './hooks/types';
+export { use3DModelName } from './hooks/use3DModelName';
diff --git a/react-components/stories/LayersContainer.stories.tsx b/react-components/stories/LayersContainer.stories.tsx
new file mode 100644
index 00000000000..4c881f1fb4f
--- /dev/null
+++ b/react-components/stories/LayersContainer.stories.tsx
@@ -0,0 +1,56 @@
+/*!
+ * Copyright 2023 Cognite AS
+ */
+
+import type { Meta, StoryObj } from '@storybook/react';
+import {
+ CadModelContainer,
+ Image360CollectionContainer,
+ PointCloudContainer,
+ RevealContainer,
+ RevealToolbar
+} from '../src';
+import { CogniteClient } from '@cognite/sdk';
+import { Color, Matrix4 } from 'three';
+
+const meta = {
+ title: 'Example/Toolbar/LayersContainer',
+ component: CadModelContainer,
+ tags: ['autodocs']
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const token = new URLSearchParams(window.location.search).get('token') ?? '';
+const sdk = new CogniteClient({
+ appId: 'reveal.example',
+ baseUrl: 'https://greenfield.cognitedata.com',
+ project: '3d-test',
+ getToken: async () => await Promise.resolve(token)
+});
+
+export const Main: Story = {
+ args: {
+ addModelOptions: {
+ modelId: 1791160622840317,
+ revisionId: 498427137020189
+ },
+ transform: new Matrix4().makeTranslation(0, 10, 0)
+ },
+ render: ({ addModelOptions, transform }) => (
+
+
+
+
+
+
+
+ )
+};