From ebc8ca717ec4f0c8d5fbe3f2f3513aaaff01aa71 Mon Sep 17 00:00:00 2001
From: Sebastian Pape <pape@vr.rwth-aachen.de>
Date: Wed, 26 Jul 2023 16:57:06 +0200
Subject: [PATCH] Adding Settings Menu and use it to switch on and off stuff in
 the scene/visualization

---
 src/Desktop.tsx                         |  9 +--
 src/app/MenuBar.tsx                     | 22 ++-----
 src/common/PopoverListButton.tsx        |  8 +--
 src/common/store.ts                     | 27 ++++++++
 src/features/app/AppSettingsPopover.tsx | 84 +++++++++++++++++++++++++
 src/features/viewport/SkyBox.tsx        | 21 ++-----
 src/features/viewport/TF2.tsx           | 29 +++++----
 src/features/viewport/Viewport.tsx      | 12 +++-
 src/features/viewport/Viewport2D.tsx    |  3 +-
 src/features/viewport/Water.tsx         | 34 +++++-----
 10 files changed, 170 insertions(+), 79 deletions(-)
 create mode 100644 src/features/app/AppSettingsPopover.tsx

diff --git a/src/Desktop.tsx b/src/Desktop.tsx
index 7840428..9c34de0 100644
--- a/src/Desktop.tsx
+++ b/src/Desktop.tsx
@@ -23,7 +23,7 @@ const lightTheme = createTheme({
 
 function Desktop() {
   const updateMessageRates = useStore(state => state.updateMessageRates);
-  const [darkMode, setDarkMode] = useState(true);
+  const darkMode = useStore(state => state.appSettings.darkTheme);
 
   useEffect(
     () => {
@@ -34,7 +34,7 @@ function Desktop() {
   );
 
   return (
-    <ThemeProvider theme={darkMode ? darkTheme: lightTheme}>
+    <ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
       <Box
         component="div"
         sx={{
@@ -44,10 +44,7 @@ function Desktop() {
           flexDirection: 'column',
         }}
       >
-        <AppBar
-          darkMode={darkMode}
-          setDarkMode={setDarkMode}
-        />
+        <AppBar />
         <Split
           initialPrimarySize='20%'
         >
diff --git a/src/app/MenuBar.tsx b/src/app/MenuBar.tsx
index ebcde04..da3af59 100644
--- a/src/app/MenuBar.tsx
+++ b/src/app/MenuBar.tsx
@@ -4,10 +4,10 @@ import Grid2 from '@mui/material/Unstable_Grid2';
 import CameraMenu from '../features/camera/CameraMenu';
 import ConnectionMenu from '../features/connections/ConnectionsMenu';
 import { Link } from 'react-router-dom';
+import { useStore } from '../common/store';
+import AppSettingsPopover from '../features/app/AppSettingsPopover';
 
 export interface AppBarProps {
-  darkMode: boolean,
-  setDarkMode: (darkMode: boolean) => void;
 }
 
 function AppBar(props: AppBarProps) {
@@ -16,7 +16,7 @@ function AppBar(props: AppBarProps) {
       <Grid2 container>
         <Grid2>
           <ConnectionMenu />
-          <CameraMenu />
+          {/* <CameraMenu /> */}
           <IconButton
             component={Link}
             to="/?vr"
@@ -29,20 +29,8 @@ function AppBar(props: AppBarProps) {
         <Grid2 xs>
         </Grid2>
         <Grid2>
-          <Grid2 container alignItems="center">
-            <Grid2>
-              <LightMode />
-            </Grid2>
-            <Grid2>
-              <Switch
-                checked={props.darkMode}
-                onChange={() => props.setDarkMode(!props.darkMode)}
-              />
-            </Grid2>
-            <Grid2>
-              <DarkMode />
-            </Grid2>
-          </Grid2>
+          <AppSettingsPopover />
+
         </Grid2>
       </Grid2>
     </MUIAppBar>
diff --git a/src/common/PopoverListButton.tsx b/src/common/PopoverListButton.tsx
index d4fc0ec..d3136d2 100644
--- a/src/common/PopoverListButton.tsx
+++ b/src/common/PopoverListButton.tsx
@@ -30,16 +30,16 @@ const PopoverListButton = (props: PopoverListButtonProps) => {
         id={id}
         open={open}
         anchorEl={buttonElement}
-        onClose={() => setButtonElement(null) }
+        onClose={() => setButtonElement(null)}
         anchorOrigin={{
           vertical: "bottom",
           horizontal: "left",
         }}
       >
         <List>
-        {
-          props.children
-        }
+          {
+            props.children
+          }
         </List>
       </Popover>
     </Fragment>
diff --git a/src/common/store.ts b/src/common/store.ts
index a57c241..9706319 100644
--- a/src/common/store.ts
+++ b/src/common/store.ts
@@ -37,12 +37,24 @@ export interface AreaOfInterest {
   audioRecordings: Blob[],
 }
 
+export interface AppSettings {
+  showTFDebugVis: boolean,
+  showTFDebugLabels: boolean,
+  darkTheme: boolean,
+  showSea: boolean,
+  oceanLevelOffset: number,
+  showSkybox: boolean,
+}
+
 export interface ConfigState {
   connections: {[uri: string]: RosbridgeConnectionState};
   ship: Config['ship'],
   transformTree: Config['transformTree'] | undefined,
   robots: {[name: string]: Robot};
   aois: {[id: string]: AreaOfInterest},
+  appSettings: AppSettings,
+
+  updateAppSettings: (newSettings: Partial<AppSettings>) => void;
 
   connect: (uri: string, bsonMode: boolean) => void;
   updateTopics: (uri: string) => void;
@@ -96,6 +108,14 @@ export const useStore = create<ConfigState>((set, get) => {
     connections: {},
     ship: undefined,
     transformTree: undefined,
+    appSettings: {
+      darkTheme: true,
+      showSea: true,
+      oceanLevelOffset: 0,
+      showSkybox: true,
+      showTFDebugLabels: true,
+      showTFDebugVis: true
+    },
     aois: {
       "0": {
         id: 0,
@@ -117,6 +137,13 @@ export const useStore = create<ConfigState>((set, get) => {
       },
     },
     
+    updateAppSettings: (newSettings: Partial<AppSettings>) => {
+      set(state => ({ appSettings: {
+        ...state.appSettings,
+        ...newSettings
+      }}));
+    },
+
     connect: (uri, bsonMode) => {
       if (!(uri in get().connections)) {
         console.log(`Connecting to ${uri}`);
diff --git a/src/features/app/AppSettingsPopover.tsx b/src/features/app/AppSettingsPopover.tsx
new file mode 100644
index 0000000..5e85535
--- /dev/null
+++ b/src/features/app/AppSettingsPopover.tsx
@@ -0,0 +1,84 @@
+import PopoverListButton from "../../common/PopoverListButton";
+import { DarkMode, Label, LightMode, Settings } from '@mui/icons-material';
+import Grid2 from "@mui/material/Unstable_Grid2/Grid2";
+import { useStore } from "../../common/store";
+import { FormControlLabel, ListItem, Slider, Switch, Typography } from "@mui/material";
+
+export interface AppSettingsPopoverProps {
+}
+
+const AppSettingsPopover = (props: AppSettingsPopoverProps) => {
+  const darkMode = useStore(state => state.appSettings.darkTheme);
+  const showSea = useStore(state => state.appSettings.showSea);
+  const showSkybox = useStore(state => state.appSettings.showSkybox);
+  const showTFTreeVis = useStore(state => state.appSettings.showTFDebugVis);
+  const showTFTreeLabels = useStore(state => state.appSettings.showTFDebugLabels);
+  const oceanLevelOffset = useStore(state => state.appSettings.oceanLevelOffset);
+  const updateSettings = useStore(state => state.updateAppSettings);
+
+  return (
+    <PopoverListButton title={"Settings"} icon={Settings}>
+      <ListItem button={false}>
+        <FormControlLabel control={
+          <Switch
+            checked={darkMode}
+            onChange={() => updateSettings({ darkTheme: !darkMode })}
+          />
+        } label={darkMode ? "Dark Mode" : "Light Mode"} />
+      </ListItem>
+
+      <ListItem button={false}>
+        <FormControlLabel control={
+          <Switch
+            checked={showSea}
+            onChange={() => updateSettings({ showSea: !showSea })}
+          />
+        } label="Show Ocean" />
+      </ListItem>
+
+      <ListItem button={false}>
+        <Slider
+          value={oceanLevelOffset}
+          onChange={(event, value) => updateSettings({ oceanLevelOffset: (value as number) })}
+          aria-labelledby="Ocean Level Offset"
+          max={5}
+          min={-5}
+          step={0.01}
+          valueLabelDisplay="auto"
+        />
+        <Typography gutterBottom noWrap>
+          Ocean Height Offset
+        </Typography>
+      </ListItem>
+
+      <ListItem button={false}>
+        <FormControlLabel control={
+          <Switch
+            checked={showSkybox}
+            onChange={() => updateSettings({ showSkybox: !showSkybox })}
+          />
+        } label="Show Skybox" />
+      </ListItem>
+
+      <ListItem button={false}>
+        <FormControlLabel control={
+          <Switch
+            checked={showTFTreeVis}
+            onChange={() => updateSettings({ showTFDebugVis: !showTFTreeVis })}
+          />
+        } label="Show TF-Tree Visualization" />
+      </ListItem>
+
+      <ListItem button={false}>
+        <FormControlLabel control={
+          <Switch
+            checked={showTFTreeLabels}
+            onChange={() => updateSettings({ showTFDebugLabels: !showTFTreeLabels })}
+          />
+        } label="Show TF-Tree Labels" />
+      </ListItem>
+    </PopoverListButton>
+  );
+};
+
+export default AppSettingsPopover;
diff --git a/src/features/viewport/SkyBox.tsx b/src/features/viewport/SkyBox.tsx
index d434347..c42cce5 100644
--- a/src/features/viewport/SkyBox.tsx
+++ b/src/features/viewport/SkyBox.tsx
@@ -1,30 +1,21 @@
+import { useCubeTexture } from '@react-three/drei';
 import { useThree } from '@react-three/fiber'
 import { useEffect } from 'react';
-import { CubeTextureLoader } from 'three'
 
 export interface SkyBoxProps {
   baseURL: string;
+  visible?: boolean;
 }
 
 function SkyBox(props: SkyBoxProps) {
   const { scene } = useThree();
-
+  let texture = useCubeTexture(["east.jpeg", "west.jpeg", "up.jpeg", "down.jpeg", "north.jpeg", "south.jpeg"], { path: props.baseURL });
   useEffect(
     () => {
-      const loader = new CubeTextureLoader();
-      // The CubeTextureLoader load method takes an array of urls representing all 6 sides of the cube.
-      const texture = loader.load([
-        `${props.baseURL}/east.jpeg`,
-        `${props.baseURL}/west.jpeg`,
-        `${props.baseURL}/up.jpeg`,
-        `${props.baseURL}/down.jpeg`,
-        `${props.baseURL}/north.jpeg`,
-        `${props.baseURL}/south.jpeg`,
-      ]);
-      // Set the scene background property to the resulting texture.
-      scene.background = texture;
+      if (!scene || !texture) return;
+      scene.background = props.visible ? texture : null;
     },
-    [scene, props.baseURL]
+    [scene, texture, props.visible]
   );
 
   return null;
diff --git a/src/features/viewport/TF2.tsx b/src/features/viewport/TF2.tsx
index 65378e7..4942879 100644
--- a/src/features/viewport/TF2.tsx
+++ b/src/features/viewport/TF2.tsx
@@ -1,7 +1,7 @@
 import { Props, useThree } from '@react-three/fiber'
 import { PropsWithChildren, useEffect, useRef } from 'react';
 import { Group, Matrix4, Mesh, MeshBasicMaterial, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three'
-import { useConnection } from '../../common/store';
+import { useConnection, useStore } from '../../common/store';
 import { Config as GeneralConfig } from '../../schemas/Config.schema';
 import tf2_msgs from '../../common/ROS/tf2_msgs';
 import geometry_msgs from '../../common/ROS/geometry_msgs';
@@ -46,8 +46,6 @@ export type yaml_anchor = {
 };
 
 export interface TF2Props {
-    config: GeneralConfig["transformTree"] | undefined;
-    debugVisualization?: boolean;
     yaml?: string[];
     attachPrintGraphFunction?: boolean;
 }
@@ -68,9 +66,12 @@ function TF2(props: TF2Props) {
     const { scene } = useThree();
     let root = useRef<Object3D>(new Object3D());
     let knownObjects = useRef(new Map<string, Object3D>());
+    let debugGeometry = useRef<Object3D[]>([]);
     const debug_geometry = useRef<SphereGeometry>(new SphereGeometry(0.1, 16, 16));
     const debug_material = useRef<MeshBasicMaterial>(new MeshBasicMaterial({ color: 0xff0000 }));
-    const connection = useConnection(props.config?.rosbridge.uri);
+    const config = useStore(state => state.transformTree);
+    const connection = useConnection(config?.rosbridge.uri);
+    let showDebugSpheres = useStore(state => state.appSettings.showTFDebugVis);
 
     useEffect(() => {
         if (!props.yaml) return;
@@ -109,8 +110,8 @@ function TF2(props: TF2Props) {
 
         // Debug
         const sphere = new Mesh(debug_geometry.current, debug_material.current);
-        sphere.name = prefix + "debugging_sphere";
-        sphere.visible = (props.debugVisualization) ? true : false;
+        sphere.visible = showDebugSpheres;
+        debugGeometry.current.push(sphere);
         Result.add(sphere);
 
         knownObjects.current.set(name, Result);
@@ -163,7 +164,7 @@ function TF2(props: TF2Props) {
                     let res = "\"" + node_name + "\" : {";
                     let first = true;
                     for (let i = 0; i < node.children.length; i++) {
-                        if (node.children[i].name === "" || node.children[i].name == prefix + "debugging_sphere") continue;
+                        if (node.children[i].name === "" || debugGeometry.current.indexOf(node) != -1) continue;
                         if (first) { first = false; } else { res += ","; }
                         res += recurse(node.children[i]);
                     }
@@ -178,16 +179,14 @@ function TF2(props: TF2Props) {
 
     // visualizations visible/invisible
     useEffect(() => {
-        root.current.traverse((node) => {
-            if (node.name == prefix + "debugging_sphere") {
-                node.visible = (props.debugVisualization || false);
-            }
-        });
-    }, [props.debugVisualization]);
+        for (let obj of debugGeometry.current) {
+            obj.visible = showDebugSpheres;
+        }
+    }, [showDebugSpheres]);
 
     // Subscribe/Unsubscribe to TF Messages
     useEffect(() => {
-        if (!connection?.rosbridge || !props.config) return;
+        if (!connection?.rosbridge || !config) return;
 
         let tf_topic = new Topic({ "ros": connection?.rosbridge, "name": "/tf", "messageType": 'tf2_msgs/TFMessage' });
         let tf_static_topic = new Topic({ "ros": connection?.rosbridge, "name": "/tf_static", "messageType": 'tf2_msgs/TFMessage' });
@@ -199,7 +198,7 @@ function TF2(props: TF2Props) {
             tf_topic.unsubscribe(newTFMessage);
             tf_static_topic.unsubscribe(newTFMessage);
         };
-    }, [connection?.rosbridge, props.config]);
+    }, [connection?.rosbridge, config]);
 
     return null;
 }
diff --git a/src/features/viewport/Viewport.tsx b/src/features/viewport/Viewport.tsx
index ebbf6d3..e155eac 100644
--- a/src/features/viewport/Viewport.tsx
+++ b/src/features/viewport/Viewport.tsx
@@ -3,7 +3,8 @@ import { Canvas } from '@react-three/fiber';
 import { MeshBasicMaterial, MeshLambertMaterial } from 'three';
 import For from '../../common/For';
 import { useStore } from '../../common/store';
-import Mesh from './Mesh';
+import Skybox from './SkyBox';
+import Water from './Water';
 import MeshLoader from './MeshLoader';
 import Robot from './Robot';
 import TF2, { Frame } from './TF2';
@@ -13,7 +14,9 @@ import FiducialVisualizer from './FiducialVisualizer';
 const Viewport = () => {
   const robots = useStore(state => state.robots);
   const ship = useStore(state => state.ship);
-  const transformtree = useStore(state => state.transformTree);
+  const showSkybox = useStore(state => state.appSettings.showSkybox);
+  const showOcean = useStore(state => state.appSettings.showSea);
+  const oceanLevelOffest = useStore(state => state.appSettings.oceanLevelOffset);
 
   return (
     <Canvas
@@ -30,6 +33,9 @@ const Viewport = () => {
       <ambientLight />
       <pointLight />
       <directionalLight />
+      <Skybox baseURL="/skyboxes/clouds/" visible={showSkybox} />
+      <Water waterNormalsTexture='waternormals.jpg' size={1000} heightOffset={oceanLevelOffest} visible={showOcean} />
+
       {/* {
         ship && ship.topics.mesh ?
           <Mesh
@@ -68,7 +74,7 @@ const Viewport = () => {
       </For>
       <FiducialVisualizer knownMarkers={[103, 104, 105, 109]} image_uri_prefix="/markers/aruco_DICT_5X5_250_Marker_" image_uri_postfix=".png" marker_size={[0.52, 0.52]} frame_prefix="fiducial_static_" />
       {/* <FiducialVisualizer knownMarkers={[103, 104, 105, 109]} image_uri_prefix="/markers/aruco_DICT_5X5_250_Marker_" image_uri_postfix=".png" marker_size={[0.52, 0.52]} frame_prefix="/mussol/fiducial_" color_tint='orange' /> */}
-      <TF2 config={transformtree} debugVisualization={false} yaml={["meshes/fiducial_tags.yaml"]} attachPrintGraphFunction />
+      <TF2 yaml={["meshes/fiducial_tags.yaml"]} attachPrintGraphFunction />
     </Canvas>
   );
 }
diff --git a/src/features/viewport/Viewport2D.tsx b/src/features/viewport/Viewport2D.tsx
index 281dd9e..0c58104 100644
--- a/src/features/viewport/Viewport2D.tsx
+++ b/src/features/viewport/Viewport2D.tsx
@@ -25,7 +25,6 @@ const Viewport2D = () => {
   const [cameraPosition, setCameraPosition] = useState<[number, number]>([0, 0]);
   const sideFactor = side === 'portside' ? -1 : 1;
   const canvas = useRef<HTMLCanvasElement>(null);
-  const transformtree = useStore(state => state.transformTree);
 
   // React uses passive events by default. So, we need to register the event
   // listener this way.
@@ -126,7 +125,7 @@ const Viewport2D = () => {
               />
           }
         </For>
-        <TF2 config={transformtree} debugVisualization={false} />
+        <TF2 />
       </Canvas>
       <Box
         component="div"
diff --git a/src/features/viewport/Water.tsx b/src/features/viewport/Water.tsx
index a7c56a8..eb69cdf 100644
--- a/src/features/viewport/Water.tsx
+++ b/src/features/viewport/Water.tsx
@@ -1,29 +1,29 @@
 import { useThree } from '@react-three/fiber'
-import { useEffect } from 'react';
-import { TextureLoader, PlaneGeometry, RepeatWrapping, Vector3 } from 'three'
+import { useEffect, useRef } from 'react';
+import { TextureLoader, PlaneGeometry, RepeatWrapping, Vector3, Group } from 'three'
 import { Water as ThreeJSWater } from "three/examples/jsm/objects/Water";
 
 export interface WaterProps {
   waterNormalsTexture?: string;
-  width?: number;
-  height?: number;
+  size?: number;
+  heightOffset?: number;
+  visible?: boolean;
 }
 
 function Water(props: WaterProps) {
   const { scene } = useThree();
-
-  let water: ThreeJSWater;
+  let water = useRef<ThreeJSWater | null>(null);
 
   useEffect(() => {
     const updateInterval = setInterval(() => {
-      water.material.uniforms[ 'time' ].value += 1.0 / 60.0 / 10;
-    }, 1/60*1000);
+      if (water.current) water.current.material.uniforms['time'].value += 1.0 / 60.0 / 10;
+    }, 1 / 60 * 1000);
 
-    const waterGeometry = new PlaneGeometry(props.width || 100, props.height || 100, 1, 1);
+    const waterGeometry = new PlaneGeometry(props.size || 100, props.size || 100, 1, 1);
     waterGeometry.computeVertexNormals();
 
-    water = new ThreeJSWater(
-      waterGeometry ,
+    water.current = new ThreeJSWater(
+      waterGeometry,
       {
         textureWidth: 512,
         textureHeight: 512,
@@ -38,17 +38,17 @@ function Water(props: WaterProps) {
         alpha: 0.1,
       }
     );
-    scene.add(water);
-    water.rotation.x = -Math.PI * 0.5;
-    water.position.setY(-5);
+    water.current.rotation.x = -Math.PI * 0.5;
+    water.current.position.setY(-5);
 
     return () => {
       clearInterval(updateInterval);
-      scene.remove(water);
     }
-  }, [props.width, props.height, props.waterNormalsTexture]);
+  }, [props.size, props.waterNormalsTexture]);
 
-  return null;
+  return <group position={[0, props.heightOffset || 0, 0]} visible={!!props.visible}>
+    {water.current ? <primitive object={water.current} /> : null}
+  </group>;
 }
 
 export default Water;
-- 
GitLab