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