diff --git a/.env.development b/.env.development new file mode 100644 index 0000000000000000000000000000000000000000..b26185bf9239df12815974a74ccf5a29f7e32334 --- /dev/null +++ b/.env.development @@ -0,0 +1,3 @@ +HTTPS=true +SSL_CRT_FILE=development-certificate/bw2.crt +SSL_KEY_FILE=development-certificate/bw2.key diff --git a/.gitignore b/.gitignore index 4d29575de80483b005c29bfcac5061cd2f45313e..c4c8057fefd0b2ef044e26b8caf21878aa0934df 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# generated files +src/schemas diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..27742a7019c8f91492ccadee446fabb27e4bcc67 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "config-schemas"] + path = src/config-schemas + url = https://gitlab.georgiatech-metz.fr/soehrl/config-schemas.git +[submodule "development-certificate"] + path = development-certificate + url = ../development-certificate.git diff --git a/development-certificate b/development-certificate new file mode 160000 index 0000000000000000000000000000000000000000..988b7580a51672290bc64591cb9bcdf2c7108b44 --- /dev/null +++ b/development-certificate @@ -0,0 +1 @@ +Subproject commit 988b7580a51672290bc64591cb9bcdf2c7108b44 diff --git a/package.json b/package.json index e1a96e504926f2af49f02d71bb8af3bbb1c1bc82..7eef0e9f49256d2bd7c5618120adeb5d070cb62e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "shipt-it", + "name": "ship-it", "version": "0.1.0", "private": true, "dependencies": { @@ -22,6 +22,7 @@ "@types/react-router-dom": "^5.3.3", "@types/roslib": "^1.1.10", "@types/three": "^0.144.0", + "jsonschema": "^1.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.0.2", @@ -31,13 +32,26 @@ "roslib": "^1.3.0", "three": "^0.144.0", "typescript": "^4.4.2", - "web-vitals": "^2.1.0" + "url": "^0.11.0", + "web-vitals": "^2.1.0", + "zustand": "^4.1.4" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", + "start": "concurrently 'react-scripts start' 'yarn run watch'", + "build": "yarn run convert-schemas && react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "convert-schemas": "json2ts -i 'src/config-schemas/*' -o 'src/schemas/'", + "watch": "npm-watch" + }, + "watch": { + "convert-schemas": { + "patterns": [ + "src/config-schemas/" + ], + "extensions": "schema.json", + "delay": 100 + } }, "eslintConfig": { "extends": [ @@ -58,6 +72,9 @@ ] }, "devDependencies": { - "@types/react-swipeable-views": "^0.13.1" + "@types/react-swipeable-views": "^0.13.1", + "concurrently": "^7.5.0", + "json-schema-to-typescript": "^11.0.2", + "npm-watch": "^0.11.0" } } diff --git a/src/App.tsx b/src/App.tsx index 249096f36832188bbb82952c413ca588b39d7248..f02dce53cea79bfafd662b7bb9a4e85bbfb76dcc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,16 @@ import { useEffect, useState } from 'react' -import { useNavigate } from 'react-router-dom'; -import { useAppDispatch, useAppSelector } from './app/hooks'; import { createTheme } from '@mui/material'; import { Box, ThemeProvider } from '@mui/system'; -import { configUpdated } from './features/config/config-slice'; import AppBar from './app/MenuBar'; -import { useNonInitialEffect } from './hooks/useNonInitialEffect'; import Viewport from './features/viewport/Viewport'; import RobotList from './features/robots/RobotList'; import { Split } from '@geoffcox/react-splitter'; -import Viewport2D from './features/viewport/Viewport2D'; +import { validate } from 'jsonschema'; +import ConfigSchema from './config-schemas/Config.schema.json'; +import { Config } from './schemas/Config.schema'; +import RobotSchema from './config-schemas/Robot.schema.json'; +import { Robot } from './schemas/Robot.schema'; +import { useStore } from './common/store'; const darkTheme = createTheme({ palette: { @@ -24,28 +25,69 @@ const lightTheme = createTheme({ }); function App() { - const config = useAppSelector(state => state.config); - const dispatch = useAppDispatch(); - const navigate = useNavigate(); + // const config = useAppSelector(state => state.config); + const addRobot = useStore(state => state.addRobot); + const updateMessageRates = useStore(state => state.updateMessageRates); + // const navigate = useNavigate(); const [darkMode, setDarkMode] = useState(true); - useEffect(() => { - const queryParams = new URLSearchParams(window.location.search); - const configString = queryParams.get('config'); - if (configString) { - try { - const decodedConfig = atob(configString); - const config = JSON.parse(decodedConfig); - dispatch(configUpdated(config)); - } catch (error) { - console.error('Invalid config'); + useEffect( + () => { + let cancelled = false; + + const fetchConfig = async () => { + const configResponse = await fetch('http://localhost:8080/https://gitlab.georgiatech-metz.fr/soehrl/fake-data-config/raw/main/config.json'); + const json = await configResponse.json(); + validate(json, ConfigSchema, { throwAll: true }); + const config = json as Config; + + if (cancelled) { + return; + } + + for (const robot of config.robots) { + const robotResponse = await fetch(`http://localhost:8080/https://gitlab.georgiatech-metz.fr/soehrl/fake-data-config/raw/main/${robot}`); + const json = await robotResponse.json(); + validate(json, RobotSchema, { throwAll: true }); + const robotConfig = json as Robot; + if (!cancelled) { + addRobot(robotConfig); + } + } } - } - }, []); - useNonInitialEffect(() => { - navigate(`?config=${btoa(JSON.stringify(config))}`); - }, [config]); + fetchConfig().catch(console.error); + + return () => { cancelled = true }; + }, + [addRobot] + ); + + useEffect( + () => { + const interval = setInterval(() => updateMessageRates(1), 1000); + return () => clearInterval(interval); + }, + [updateMessageRates] + ); + + // useEffect(() => { + // const queryParams = new URLSearchParams(window.location.search); + // const configString = queryParams.get('config'); + // if (configString) { + // try { + // const decodedConfig = atob(configString); + // const config = JSON.parse(decodedConfig); + // dispatch(configUpdated(config)); + // } catch (error) { + // console.error('Invalid config'); + // } + // } + // }, []); + + // useNonInitialEffect(() => { + // navigate(`?config=${btoa(JSON.stringify(config))}`); + // }, [config]); return ( <ThemeProvider theme={darkMode ? darkTheme: lightTheme}> @@ -70,7 +112,9 @@ function App() { horizontal > <Viewport /> + {/* <Viewport2D /> + */} </Split> </Split> </Box> diff --git a/src/app/MenuBar.tsx b/src/app/MenuBar.tsx index a3e177504073f51cdfc150ed34e0e5b3278ddd72..7f02bf1ddd34c43fde41d81a84bf32fd9d73b7a7 100644 --- a/src/app/MenuBar.tsx +++ b/src/app/MenuBar.tsx @@ -1,9 +1,8 @@ -import { AppBar as MUIAppBar, IconButton, Switch } from '@mui/material'; -import { DarkMode, LightMode, SmartToy as RobotIcon } from '@mui/icons-material'; +import { AppBar as MUIAppBar, Switch } from '@mui/material'; +import { DarkMode, LightMode } from '@mui/icons-material'; import Grid2 from '@mui/material/Unstable_Grid2'; -import ConnectionsMenu from '../features/connections/ConnectonsMenu'; import CameraMenu from '../features/camera/CameraMenu'; -import RobotsMenu from '../features/robots/RobotMenu'; +import ConnectionMenu from '../features/connections/ConnectionsMenu'; export interface AppBarProps { darkMode: boolean, @@ -15,8 +14,7 @@ function AppBar(props: AppBarProps) { <MUIAppBar position="sticky"> <Grid2 container> <Grid2> - <ConnectionsMenu /> - <RobotsMenu /> + <ConnectionMenu /> <CameraMenu /> </Grid2> <Grid2 xs> diff --git a/src/app/store.ts b/src/app/store.ts index d3c1495151ae691cf6a9ed0fdcefcec05d29978c..3f2134a167cb523b00b380891b3635e3fec3e2b6 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -1,10 +1,12 @@ import { configureStore } from '@reduxjs/toolkit'; import configReducer from '../features/config/config-slice'; import cameraReducer from '../features/camera/camera-slice'; +import connectionsReducer from '../features/connections/rosbridge-connections-slice'; export const store = configureStore({ reducer: { config: configReducer, + connections: connectionsReducer, camera: cameraReducer, }, }); diff --git a/src/common/ROS/geometry_msgs.ts b/src/common/ROS/geometry_msgs.ts new file mode 100644 index 0000000000000000000000000000000000000000..0c79337a44c6af7877bb90b0e9008839ee8598d1 --- /dev/null +++ b/src/common/ROS/geometry_msgs.ts @@ -0,0 +1,44 @@ +import std_msgs from './std_msgs'; + +type geometry_msgs = { + Vector3: { + x: number + y: number + z: number + } + + Point: { + x: number + y: number + z: number + } + + Quaternion: { + x: number + y: number + z: number + w: number + } + + Transform: { + translation: geometry_msgs['Vector3'] + rotation: geometry_msgs['Quaternion'] + } + + TransformStamped: { + header: std_msgs['Header'] + transform: geometry_msgs['Transform'] + } + + Pose: { + position: geometry_msgs['Point'] + orientation: geometry_msgs['Quaternion'] + } + + PoseStamped: { + header: std_msgs['Header'] + pose: geometry_msgs['Pose'] + } +}; + +export default geometry_msgs; diff --git a/src/common/ROS/geometry_msgs.tsx b/src/common/ROS/geometry_msgs.tsx deleted file mode 100644 index f7d4420791384f09fc1513e35c559b14ec7fb8c0..0000000000000000000000000000000000000000 --- a/src/common/ROS/geometry_msgs.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Header } from "./std_msgs"; - -export interface Vector3 { - x: number; - y: number; - z: number; -} - -export interface Quaternion { - x: number; - y: number; - z: number; - w: number; -} - -export interface Transform { - translation: Vector3; - rotation: Quaternion; -} - -export interface TransformStamped { - header: Header; - child_frame_id: string; - transform: Transform; -} - -export interface tfMessage { - transforms: Transform[]; -} diff --git a/src/common/ROS/index.ts b/src/common/ROS/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d3d7b3c8596a1bfc9f85fb782888ae2ef6dc65b --- /dev/null +++ b/src/common/ROS/index.ts @@ -0,0 +1,11 @@ +import std_msgs from './std_msgs'; +import geometry_msgs from './geometry_msgs'; +import sensor_msgs from './sensor_msgs'; +import mesh_msgs from './mesh_msgs'; + +export type Messages = { + std_msgs: std_msgs; + geometry_msgs: geometry_msgs; + sensor_msgs: sensor_msgs; + mesh_msgs: mesh_msgs; +} diff --git a/src/common/ROS/mesh_msgs.ts b/src/common/ROS/mesh_msgs.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb558d9e72ed214f4e27167604933a2736e11bb1 --- /dev/null +++ b/src/common/ROS/mesh_msgs.ts @@ -0,0 +1,22 @@ +import geometry_msgs from "./geometry_msgs"; +import std_msgs from "./std_msgs"; + +type mesh_msgs = { + MeshTriangleIndices: { + vertex_indices: [number, number, number] + } + + MeshGeometry: { + vertices: geometry_msgs['Vector3'][] + vertex_normals: geometry_msgs['Vector3'][] + faces: mesh_msgs['MeshTriangleIndices'][] + } + + MeshGeometryStamped: { + header: std_msgs['Header'] + uuid: string + mesh_geometry: mesh_msgs['MeshGeometry'] + } +} + +export default mesh_msgs; diff --git a/src/common/ROS/sensor_msgs.ts b/src/common/ROS/sensor_msgs.ts new file mode 100644 index 0000000000000000000000000000000000000000..238c831bd4576c80598f665b18b26f8b459f3063 --- /dev/null +++ b/src/common/ROS/sensor_msgs.ts @@ -0,0 +1,100 @@ +import std_msgs from "./std_msgs"; + +enum PowerSupplyStatus { + Unknown = 0, + Charging = 1, + Discharging = 2, + NotCharging = 3, + Full = 4, +} + +enum PowerSupplyHealth { + Unknown = 0, + Good = 1, + Overheat = 2, + Dead = 3, + Overvoltage = 4, + UnspecFailure = 5, + Cold = 6, + WatchdogTimerExpire = 7, + SafetyTimerExpire = 8, +} + +enum PowerSupplyTechnology { + Unknown = 0, + NiMH = 1, + Lion = 2, + LiPo = 3, + LiFE = 4, + NiCd = 5, + LiMn = 6, +} + +enum PointFieldDataType { + Int8 = 1, + UInt8 = 2, + Int16 = 3, + UInt16 = 4, + Int32 = 5, + UInt32 = 6, + Float32 = 7, + Float64 = 8, +} + +type sensor_msgs = { + BatteryState: { + header: std_msgs['Header'] + voltage: number + temperature: number + current: number + charge: number + capacity: number + design_capacity: number + percentage: number + + power_supply_status: PowerSupplyStatus + power_supply_health: PowerSupplyHealth + power_supply_technology: PowerSupplyTechnology + present: boolean + + cell_voltage: number[] + cell_temperature: number[] + + location: string + serial_number: string + } + + PointField: { + name: string + offset: number + datatype: PointFieldDataType + count: number + } + + PointCloud2: { + width: number + height: number + fields: sensor_msgs['PointField'][] + is_bigendian: boolean + point_step: number + row_step: number + data: string + is_dense: boolean + } + + CompressedImage: { + header: std_msgs['Header'] + format: 'jpeg' | 'png' + data: string + } + + Image: { + width: number + height: number + encoding: 'rgb8' | 'mono8' | '16UC1' | 'bgr8' | string // There are more, but these are the supported ones + step: number + data: string + } +} + +export default sensor_msgs; diff --git a/src/common/ROS/std_msgs.ts b/src/common/ROS/std_msgs.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f413049b59badff82e1905e6c44c0de35ce7c0f --- /dev/null +++ b/src/common/ROS/std_msgs.ts @@ -0,0 +1,12 @@ +type std_msgs = { + Header: { + seq: number; + time: { + sec: number; + nsec: number; + }; + frame_id: string; + }; +} + +export default std_msgs; diff --git a/src/common/ROS/std_msgs.tsx b/src/common/ROS/std_msgs.tsx deleted file mode 100644 index da7d8f76c1cf6392a277145b89ff90c02f7880e8..0000000000000000000000000000000000000000 --- a/src/common/ROS/std_msgs.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export interface Header { -} diff --git a/src/common/store.ts b/src/common/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..9bd3779a6c72cf70a92de1c92d38b67a2c9e0f4a --- /dev/null +++ b/src/common/store.ts @@ -0,0 +1,340 @@ +import { useCallback, useEffect } from 'react'; +import { Ros, Topic } from 'roslib'; +import create from 'zustand'; +import { Robot } from '../schemas/Robot.schema'; +import { Messages } from "./ROS"; + +export interface ROSTopicState { + topic: Topic; + name: string; + type: string; + subscriberCount: number; + updateCallback: (value: any) => void; + value: any; + messageCount: number; + messageRate: number; +} + +export interface RosbridgeConnectionState { + uri: string; + bsonMode: boolean; + status: 'establishing connection' | 'connected' | 'disconnected'; + rosbridge: Ros; + topics: {[topic: string]: ROSTopicState | number}; +} + +export interface ConfigState { + connections: {[uri: string]: RosbridgeConnectionState}; + robots: {[name: string]: Robot}; + + connect: (uri: string, bsonMode: boolean) => void; + updateTopics: (uri: string) => void; + + addRobot: (robot: Robot) => void; + + subscribe: (uri: string, topic: string) => void; + unsubscribe: (uri: string, topic: string) => void; + updateMessageRates: (elapsedTime: number) => void; +} + +export const useStore = create<ConfigState>((set, get) => { + + const createSubscriptionHandler = (uri: string, topic: string) => { + return (value: any) => { + // console.info(`Updating topic ${topic} of ${uri} to ${JSON.stringify(value)}`); + set(state => { + const topicState = state.connections[uri].topics[topic]; + if (typeof topicState === 'number') { + console.error(`Recevied a message for a topic not subscribed to!`); + return {}; + // } else { + // console.info(`Updating topic ${topic} of ${uri} to ${JSON.stringify(value)}`); + } + + return { + connections: { + ...state.connections, + [uri]: { + ...state.connections[uri], + topics: { + ...state.connections[uri].topics, + [topic]: { + ...topicState, + messageCount: topicState.messageCount + 1, + value, + } + } + } + } + } + }); + } + }; + + return { + robots: {}, + connections: {}, + + connect: (uri, bsonMode) => { + if (!(uri in get().connections)) { + console.log(`Connecting to ${uri}`); + const rosbridge = new Ros({ url: uri }); + + set(state => ({ + connections: { + ...state.connections, + [uri]: { + uri, + bsonMode, + status: 'establishing connection', + topics: {}, + rosbridge, + } + } + })); + + rosbridge.on('connection', _ => { + set(state => ({ + connections: { + ...state.connections, + [uri]: { + ...state.connections[uri], + status: 'connected', + } + } + })); + }); + + rosbridge.on('close', _ => { + set(state => ({ + connections: { + ...state.connections, + [uri]: { + ...state.connections[uri], + status: 'disconnected', + } + } + })); + }); + + get().updateTopics(uri); + } + }, + + updateTopics: (uri: string) => { + const connection = get().connections[uri]; + if (!connection) { + console.error(`No connection to ${uri} exists`); + return; + } + + connection.rosbridge.getTopics(({ topics, types }) => { + if (topics.length !== types.length) { + console.error(`Invalid response from rosbridge`); + return; + } + + set(state => { + const oldTopics = state.connections[uri].topics; + const connectionTopics: {[topic: string]: ROSTopicState} = {}; + + for (let i = 0; i < topics.length; ++i) { + const oldTopic = oldTopics[topics[i]]; + if (typeof oldTopic === 'number' || typeof oldTopic === 'undefined') { + const handler = createSubscriptionHandler(uri, topics[i]); + const topicState = { + name: topics[i], + type: types[i], + value: null, + subscriberCount: oldTopic || 0, + topic: new Topic({ + ros: connection.rosbridge, + name: topics[i], + messageType: types[i], + }), + updateCallback: handler, + messageCount: 0, + messageRate: 0, + }; + console.log(`Discovered ${topicState.name} on ${uri} (${oldTopic})`); + if (oldTopic > 0) { + console.log(`Subscribe to ${topicState.name} on ${uri}`); + topicState.topic.subscribe(handler); + } + connectionTopics[topics[i]] = topicState; + } else { + console.warn(`Not implemented yet! ${topics[i]}`); + } + } + + return { + connections: { + ...state.connections, + [uri]: { + ...state.connections[uri], + topics: connectionTopics, + } + } + }; + }); + }); + }, + + addRobot: (robot: Robot) => { + if (!(robot.name in get().robots)) { + if (!(robot.rosbridge.uri in get().connections)) { + get().connect(robot.rosbridge.uri, robot.rosbridge.bsonMode || false); + } + set(state => ({ robots: { + ...state.robots, + [robot.name]: structuredClone(robot), + }})); + } + }, + + subscribe: (uri: string, topicName: string) => { + const connection = get().connections[uri]; + if (!connection) { + console.error(`Connection with uri ${uri} not found`); + return; + } + + const topic = connection.topics[topicName]; + if (!topic) { + console.info(`Registered ${topicName} on ${uri} for subscription`); + set(state => ({ + connections: { + ...state.connections, + [uri]: { + ...state.connections[uri], + topics: { + ...state.connections[uri].topics, + [topicName]: 1, + } + } + } + })); + } else if (typeof topic === 'number') { + set(state => ({ + connections: { + ...state.connections, + [uri]: { + ...state.connections[uri], + topics: { + ...state.connections[uri].topics, + [topicName]: topic + 1, + } + } + } + })); + } else { + if (topic.subscriberCount === 0) { + console.log(`Subscribe to ${topicName} on ${uri}`); + topic.topic.subscribe(topic.updateCallback); + } + topic.subscriberCount++; + console.log(`Increased subscribers of ${topicName} on ${uri} to ${topic.subscriberCount}`); + + set(state => ({ + connections: { + [uri]: { + ...state.connections[uri], + topics: { + ...state.connections[uri].topics, + [topicName]: topic, + } + } + } + })); + } + }, + + unsubscribe: (uri: string, topicName: string) => { + const connection = get().connections[uri]; + if (!connection) { + console.error(`Connection with uri ${uri} not found`); + return; + } + + const topic = connection.topics[topicName]; + + if (typeof topic !== 'object') { + console.error(`Trying to unsubscribe from a topic not subscribed to!`); + return; + } + + topic.subscriberCount--; + console.log(`Decreased subscribers of ${topicName} on ${uri} to ${topic.subscriberCount}`); + if (topic.subscriberCount === 0) { + console.log(`Unsubscribe from ${topicName} on ${uri}`); + topic.topic.unsubscribe(topic.updateCallback); + } + set(state => ({ + connections: { + [uri]: { + ...state.connections[uri], + topics: { + ...state.connections[uri].topics, + [topicName]: topic, + } + } + } + })); + }, + + updateMessageRates: (elapsedTime: number) => { + set(state => { + for (const connection of Object.values(state.connections)) { + for (const topic of Object.values(connection.topics)) { + if (typeof topic === 'object') { + if (topic.messageCount > 0) { + console.log(`Topic ${topic.name} received ${topic.messageCount} messages in the last ${elapsedTime} seconds`); + } + topic.messageRate = topic.messageCount / elapsedTime; + topic.messageCount = 0; + } + } + } + return { + connections: { ...state.connections } + } + }); + } + } +}); + +export function useConnection(uri?: string) { + return useStore( + useCallback( + state => uri ? state.connections[uri] : undefined, + [uri] + ) + ); +} + +export function useTopic< + Package extends string & keyof Messages, + Message extends string & keyof Messages[Package] +>(uri?: string, topic?: string, _?: `${Package}/${Message}`) { + const subscribe = useStore(useCallback(state => state.subscribe, [])); + const unsubscribe = useStore(useCallback(state => state.unsubscribe, [])); + + useEffect( + () => { + if (uri && topic) { + subscribe(uri, topic); + + return () => unsubscribe(uri, topic); + } + }, + [subscribe, unsubscribe, uri, topic] + ); + + + const connectionTopic = useStore(useCallback(state => uri && topic ? state.connections[uri]?.topics[topic]: undefined, [uri, topic])); + if (typeof connectionTopic === 'number' || typeof connectionTopic === 'undefined') { + return undefined; + } else { + return connectionTopic.value as Messages[Package][Message] | undefined; + } +} diff --git a/src/config-schemas b/src/config-schemas new file mode 160000 index 0000000000000000000000000000000000000000..d0bde3353b2f5d718d2ff0562d5ddcca6fe98987 --- /dev/null +++ b/src/config-schemas @@ -0,0 +1 @@ +Subproject commit d0bde3353b2f5d718d2ff0562d5ddcca6fe98987 diff --git a/src/features/config/config-hooks.ts b/src/features/config/config-hooks.ts new file mode 100644 index 0000000000000000000000000000000000000000..09ca0da71ab50f4da495d7ea8c09c0d9378cb825 --- /dev/null +++ b/src/features/config/config-hooks.ts @@ -0,0 +1,5 @@ +import { useAppSelector } from "../../app/hooks"; + +export function useRobot(name: string) { + return useAppSelector(state => state.config.robots.find(robot => robot.name === name)); +} diff --git a/src/features/config/config-slice.ts b/src/features/config/config-slice.ts index 6f25008f4a22859e70a2a8f3f60674e4cb537551..a97cc4f4d7d1663b15fedb770d5b4fce64f1bbbe 100644 --- a/src/features/config/config-slice.ts +++ b/src/features/config/config-slice.ts @@ -1,57 +1,35 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - -export interface ConnectionConfig { - url: string; -} - -export type RobotType = "Crawler" | "Airborne Drone" | "Underwater Drone"; - -export interface RobotConfig { - connection: string; - prefix: string; - poseTopic?: string; - type: RobotType; -} +import { Robot } from '../../schemas/Robot.schema'; interface ConfigState { - connections: {[key: string]: ConnectionConfig}; - robots: {[key: string]: RobotConfig}; - shipMesh?: string; + robots: Robot[]; } const initialState: ConfigState = { - connections: {}, - robots: {}, + robots: [], } const configSlice = createSlice({ name: 'config', initialState, reducers: { - configUpdated(_state, action: PayloadAction<ConfigState>) { + updateConfig(_state, action: PayloadAction<ConfigState>) { return action.payload }, - connectionAdded(state, action: PayloadAction<{name: string, config: ConnectionConfig}>) { - if (!(action.payload.name in state.connections)) { - state.connections[action.payload.name] = action.payload.config; + addRobot(state, action: PayloadAction<Robot>) { + const robotToAdd = action.payload; + if (!state.robots.find(robot => robot.name === robotToAdd.name)) { + state.robots.push(robotToAdd); } }, - connectionRemoved(state, action: PayloadAction<string>) { - delete state.connections[action.payload]; - }, - robotAdded(state, action: PayloadAction<{name: string, config: RobotConfig}>) { - if (!(action.payload.name in state.robots)) { - state.robots[action.payload.name] = action.payload.config; + removeRobot(state, action: PayloadAction<string>) { + const robotIndex = state.robots.findIndex(robot => robot.name === action.payload); + if (robotIndex !== -1) { + state.robots.splice(robotIndex, 1); } - }, - robotRemoved(state, action: PayloadAction<string>) { - delete state.robots[action.payload]; - }, - shipMeshSet(state, action: PayloadAction<string|undefined>) { - state.shipMesh = action.payload; - }, + } } }); -export const { connectionAdded, connectionRemoved, configUpdated, robotAdded, robotRemoved, shipMeshSet } = configSlice.actions; +export const { addRobot, removeRobot, updateConfig } = configSlice.actions; export default configSlice.reducer; diff --git a/src/features/connections/ConnectionInfoDialog.tsx b/src/features/connections/ConnectionInfoDialog.tsx index b0eb03f95d6635dd7d4bc6e2b9632d681fabb772..58f74cf2a16d2f3f955dc828d0baf6b3cba235f0 100644 --- a/src/features/connections/ConnectionInfoDialog.tsx +++ b/src/features/connections/ConnectionInfoDialog.tsx @@ -3,14 +3,31 @@ import { Accordion, AccordionDetails, AccordionSummary, CircularProgress, Link, import { useEffect, useState } from 'react'; import { Param, Ros } from 'roslib'; import For from '../../common/For'; -import { Topic, useConnection, useTopic } from './RosbridgeConnections'; import Show from '../../common/Show'; import Dialog from '../../common/Dialog'; +import { ROSTopicState, useConnection, useTopic } from '../../common/store'; + +interface TopicMessageProps { + uri: string; + topic: ROSTopicState; +} + +const TopicMessage = (props: TopicMessageProps) => { + const message = useTopic(props.uri, props.topic.name); + + return ( + <pre + style={{ margin: 0 }} + > + { JSON.stringify(message, undefined, 4) } + </pre> + ); +}; interface TopicInfoProps { - topic: Topic; + uri: string; + topic: ROSTopicState; rosDistro?: string|null; - ros: Ros, searchQuery?: string, } @@ -19,9 +36,8 @@ const TopicInfo = (props: TopicInfoProps) => { const [pkg, message] = props.topic.type.split("/"); return `http://docs.ros.org/en/${props.rosDistro || "melodic"}/api/${pkg}/html/msg/${message}.html`; }; - - const [shouldSubscribe, setShouldSubscribe] = useState(false); - const message = useTopic(props.ros, props.topic.name, props.topic.type, shouldSubscribe); + + const [showMessage, setShowMessage] = useState(false); return ( <Show @@ -32,7 +48,7 @@ const TopicInfo = (props: TopicInfoProps) => { } > <Accordion - onChange={(_, expanded) => setShouldSubscribe(expanded)} + onChange={(_, expanded) => setShowMessage(expanded)} > <AccordionSummary expandIcon={<ExpandMore />} @@ -46,10 +62,12 @@ const TopicInfo = (props: TopicInfoProps) => { > { props.topic.type } </Link> +   + ({props.topic.messageRate}Hz, {props.topic.subscriberCount} Subscriber) </AccordionSummary> <AccordionDetails> - <Show when={message} fallback={<CircularProgress />}> - <pre style={{ margin: 0 }}>{ JSON.stringify(message, undefined, 2) }</pre> + <Show when={showMessage}> + <TopicMessage uri={props.uri} topic={props.topic} /> </Show> </AccordionDetails> </Accordion> @@ -58,48 +76,48 @@ const TopicInfo = (props: TopicInfoProps) => { } export interface ConnectionInfoDialogProps { - connection: string; - open?: boolean; - close: () => void; + connection?: string; + onClose: () => void; } const ConnectionInfoDialog = (props: ConnectionInfoDialogProps) => { const connection = useConnection(props.connection); - const [rosdistro, setRosdistro] = useState<string|null>(null); - const [topics, setTopics] = useState<undefined|null|Topic[]>(undefined); - const [topicSearchQuery, setTopicSearchQuery] = useState<string|undefined>(); + // const [rosdistro, setRosdistro] = useState<string|null>(null); + // const [topics, setTopics] = useState<undefined|null|Topic[]>(undefined); + // const [topicSearchQuery, setTopicSearchQuery] = useState<string|undefined>(); - useEffect(() => { - if (props.open && connection) { - const rosdistro = new Param({ ros: connection, name: '/rosdistro' }); - rosdistro.get(rosdistro => setRosdistro(rosdistro)); + // useEffect(() => { + // if (props.open && connection) { + // const rosdistro = new Param({ ros: connection, name: '/rosdistro' }); + // rosdistro.get(rosdistro => setRosdistro(rosdistro)); - connection.getTopics( - topics => { - const topicsArray: Topic[] = []; - for (let i = 0; i < topics.topics.length; ++i) { - topicsArray.push({ - name: topics.topics[i], - type: topics.types[i], - }); - } - setTopics(topicsArray); - }, - () => { - setTopics(null); - } - ); - } - }, [props.open, connection]); + // connection.getTopics( + // topics => { + // const topicsArray: Topic[] = []; + // for (let i = 0; i < topics.topics.length; ++i) { + // topicsArray.push({ + // name: topics.topics[i], + // type: topics.types[i], + // }); + // } + // setTopics(topicsArray); + // }, + // () => { + // setTopics(null); + // } + // ); + // } + // }, [props.open, connection]); return ( <Dialog - open={props.open || false} + open={!!props.connection} title={props.connection} - close={props.close} + close={props.onClose} width="80%" height="80%" > + {/* <h2>ROS distribution</h2> <Show when={rosdistro} @@ -125,26 +143,29 @@ const ConnectionInfoDialog = (props: ConnectionInfoDialogProps) => { }} /> </h2> - <Show - when={topics} - fallback={ typeof topics === 'undefined' ? <CircularProgress /> : <p>An error occured while querying the topics.</p>} - > + */} { - topics => - <For each={topics}> - { - topic => - <TopicInfo - key={`${props.connection}${topic.name}`} - topic={topic} - rosDistro={rosdistro} - ros={connection} - searchQuery={topicSearchQuery} - /> - } - </For> + connection ? + Object + .keys(connection.topics) + .map(name => ({name, topic: connection.topics[name]})) + .map(({name, topic}) => + typeof topic === 'number' ? + <p + key={`${props.connection}${name}`} + > + {`${name}: ${topic}`} + </p> + : + <TopicInfo + key={`${props.connection}${name}`} + uri={props.connection || ""} + topic={topic} + /> + ) + : + null } - </Show> </Dialog> ); }; diff --git a/src/features/connections/ConnectionsMenu.tsx b/src/features/connections/ConnectionsMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..021bc0bb5b17fb26701ed9c57e380578f94465c9 --- /dev/null +++ b/src/features/connections/ConnectionsMenu.tsx @@ -0,0 +1,53 @@ + +import PopoverListButton from "../../common/PopoverListButton"; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { RadioButtonChecked, RadioButtonUnchecked, Power, PowerOff } from '@mui/icons-material'; +import { List, ListItem, ListItemButton, ListItemIcon, ListItemText } from "@mui/material"; +import Show from "../../common/Show"; +import { useStore } from '../../common/store'; +import { useState } from "react"; +import ConnectionInfoDialog from "./ConnectionInfoDialog"; + + +const ConnectionMenu = () => { + const connections = useStore(state => state.connections); + const [selectedConnection, selectConnection] = useState<string>(); + + return ( + <> + <ConnectionInfoDialog + connection={selectedConnection} + onClose={() => { selectConnection(undefined) }} + /> + <PopoverListButton + icon={ Power } + title="Connections" + > + { + Object.keys(connections).map(uri => connections[uri]).map(connection => + <ListItem + key={connection.uri} + > + <ListItemButton + onClick={() => selectConnection(connection.uri)} + > + <ListItemIcon> + { + connection.status === 'disconnected' ? + <PowerOff color="error" /> : + <Power color={connection.status === 'connected' ? "success" : "warning"} /> + } + </ListItemIcon> + <ListItemText> + { connection.uri } + </ListItemText> + </ListItemButton> + </ListItem> + ) + } + </PopoverListButton> + </> + ); +}; + +export default ConnectionMenu; diff --git a/src/features/connections/ConnectonsMenu.tsx b/src/features/connections/ConnectonsMenu.tsx deleted file mode 100644 index 4790794713c2c2e294861fee5834aca422901fb0..0000000000000000000000000000000000000000 --- a/src/features/connections/ConnectonsMenu.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Divider, IconButton, ListItem, ListItemButton } from "@mui/material"; -import PopoverListButton from "../../common/PopoverListButton"; -import { Power as ConnectionIcon, Delete } from '@mui/icons-material'; -import NewConnectionDialog from "./NewConnectionDialog"; -import { Fragment, useEffect, useState } from "react"; -import { useAppDispatch, useAppSelector } from '../../app/hooks'; -import For from "../../common/For"; -import Show from "../../common/Show"; -import { ConnectionConfig, connectionRemoved } from "../config/config-slice"; -import { useConnection } from "./RosbridgeConnections"; -import ConnectionInfoDialog from "./ConnectionInfoDialog"; - -interface ConnectionsMenuEntryProps { - name: string; - config: ConnectionConfig; -} - -const ConnectionsMenuEntry = (props: ConnectionsMenuEntryProps) => { - const dispatch = useAppDispatch(); - const connection = useConnection(props.name); - const connectionConfig = useAppSelector(state => state.config.connections[props.name]); - const [connected, setConnected] = useState(connection ? connection.isConnected : false); - const [connectionInfoOpen, setConnectionInfoOpen] = useState(false); - - useEffect(() => { - const connectListener = () => setConnected(true); - const disconnectListener = () => setConnected(false); - if (connection) { - connection.addListener('connection', connectListener); - connection.addListener('close', disconnectListener); - } - - return () => { - if (connection) { - connection.removeListener('connection', connectListener); - connection.removeListener('close', disconnectListener); - } - } - }, [connection]); - - return ( - <ListItem> - <IconButton - onClick={() => connected ? connection.close() : connection.connect(connectionConfig.url)} - color={connected ? "success" : connection ? "error" : "warning"} - disabled={!connection} - > - <ConnectionIcon /> - </IconButton> - <ListItemButton onClick={() => setConnectionInfoOpen(true)}> - {props.name} - </ListItemButton> - <IconButton - onClick={() => dispatch(connectionRemoved(props.name))} - > - <Delete /> - </IconButton> - <ConnectionInfoDialog - connection={props.name} - open={connectionInfoOpen} - close={() => setConnectionInfoOpen(false)} - /> - </ListItem> - ); -}; - -const ConnectionsMenu = () => { - const connections = useAppSelector(state => state.config.connections); - const [newConnection, setNewConnection] = useState(false); - - return ( - <Fragment> - <NewConnectionDialog - open={newConnection} - close={() => setNewConnection(false)} - /> - <PopoverListButton - icon={ConnectionIcon} - title="Connections" - > - <For each={Object.keys(connections).map(name => ({ name, config: connections[name] }))}> - { - connection => <ConnectionsMenuEntry key={connection.name} {...connection} /> - } - </For> - <Show when={Object.keys(connections).length > 0}> - <Divider /> - </Show> - <ListItem> - <ListItemButton onClick={() => setNewConnection(true)}>Add Connection</ListItemButton> - </ListItem> - </PopoverListButton> - </Fragment> - ); -}; - -export default ConnectionsMenu; diff --git a/src/features/connections/NewConnectionDialog.tsx b/src/features/connections/NewConnectionDialog.tsx deleted file mode 100644 index fd1e6e07627a8df28ab9c3b976f34a1c24f60eb3..0000000000000000000000000000000000000000 --- a/src/features/connections/NewConnectionDialog.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import Button from '@mui/material/Button'; -import FormControl from '@mui/material/FormControl'; -import TextField from '@mui/material/TextField'; -import { useState } from 'react'; -import Dialog from '../../common/Dialog'; -import { useAppSelector, useAppDispatch } from '../../app/hooks'; -import { connectionAdded } from '../config/config-slice'; - -export interface NewConnectionDialogProps { - open?: boolean; - close: () => void; -} - -const NewConnectionDialog = (props: NewConnectionDialogProps) => { - const connections = useAppSelector(state => state.config.connections); - const dispatch = useAppDispatch(); - const [name, setName] = useState("New Connection"); - const [url, setURL] = useState("ws://localhost:9090"); - - return ( - <Dialog - open={props.open || false} - title="Connection Details" - close={props.close} - > - <FormControl> - <TextField - label="Name" - variant="standard" - value={name} - error={name in connections} - onChange={event => setName(event.target.value)} - /> - <TextField - label="URL" - variant="standard" - value={url} - onChange={event => setURL(event.target.value)} - /> - <Button - variant="text" - disabled={name in connections} - onClick={() => { - dispatch(connectionAdded({ name, config: { url } })); - props.close(); - }} - > - Connect - </Button> - </FormControl> - </Dialog> - ); -}; - -export default NewConnectionDialog; diff --git a/src/features/connections/RosbridgeConnections.tsx b/src/features/connections/RosbridgeConnections.tsx deleted file mode 100644 index 42be54fbd7ca48267df8459492fd43ccaf7b9cae..0000000000000000000000000000000000000000 --- a/src/features/connections/RosbridgeConnections.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { createContext, PropsWithChildren, useContext, useEffect, useState } from "react"; -import { Message, Ros, Topic as ROSLibTopic } from "roslib"; -import { useAppSelector } from '../../app/hooks'; - -export interface Topic { - name: string; - type: string; -} - -export type RosbridgeConnectionMap = {[key: string]: Ros}; -const RosbridgeConnectionsContext = createContext<RosbridgeConnectionMap>({}); - -const RosbridgeConnections = (props: PropsWithChildren) => { - const connectionConfigs = useAppSelector(state => state.config.connections); - const [connections, setConnections] = useState<RosbridgeConnectionMap>({}); - - useEffect(() => { - const newConnections: RosbridgeConnectionMap = {}; - for (const connectionName of Object.keys(connectionConfigs)) { - if (connectionName in connections) { - // Check if URL changed - newConnections[connectionName] = connections[connectionName]; - } else { - newConnections[connectionName] = new Ros({ url: connectionConfigs[connectionName].url }); - } - } - for (const oldConnection of Object.keys(connections)) { - if (!(oldConnection in newConnections)) { - connections[oldConnection].close(); - } - } - setConnections(newConnections); - }, [connectionConfigs]); - - return ( - <RosbridgeConnectionsContext.Provider value={connections}> - { - props.children - } - </RosbridgeConnectionsContext.Provider> - ); -} - -const useConnection = (name: string) => { - const connections = useContext(RosbridgeConnectionsContext); - return connections[name] || null; -}; - -export const useTopics = (connectionName: string) => { - const connection = useConnection(connectionName); - const [topics, setTopics] = useState<undefined|null|Topic[]>(undefined); - - useEffect(() => { - if (connection) { - connection.getTopics( - topics => { - const topicsArray: Topic[] = []; - for (let i = 0; i < topics.topics.length; ++i) { - topicsArray.push({ - name: topics.topics[i], - type: topics.types[i], - }); - } - setTopics(topicsArray); - }, - () => { - setTopics(null); - } - ); - } - }, [connection]); - - return topics; -} - -export const useTopicsOfType = (connectionName: string, types: string|string[]) => { - const connection = useConnection(connectionName); - const [topics, setTopics] = useState<undefined|null|Topic[]>(undefined); - - if (typeof types === 'string') { - types = [types]; - } - - useEffect(() => { - if (connection) { - connection.getTopics( - topics => { - const topicsArray: Topic[] = []; - for (let i = 0; i < topics.topics.length; ++i) { - if (types.indexOf(topics.types[i]) !== -1) { - topicsArray.push({ - name: topics.topics[i], - type: topics.types[i], - }); - } - } - setTopics(topicsArray); - }, - () => { - setTopics(null); - } - ); - } - }, [connection]); - - return topics; -} - -export function useTopicFromConnection<T = Message>(connection: string, topicName: string, topicType: string, subscribe?: boolean) { - const ros = useConnection(connection); - const [topic, setTopic] = useState<ROSLibTopic<T>|null>(null); - const [message, setMessage] = useState<T|undefined>(); - - useEffect(() => { - if (ros) { - setTopic(new ROSLibTopic<T>({ ros, name: topicName, messageType: topicType })); - } - return () => setTopic(null); - }, [ros, topicName, topicType]); - useEffect(() => { - if (topic && (typeof subscribe == 'undefined' || subscribe)) { - const theTopic = topic; - theTopic.subscribe(setMessage); - return () => theTopic.unsubscribe(); - } - }, [topic, subscribe]); - - return message; -} - -export function useTopic<T = Message>(ros: Ros, topicName: string, topicType: string, subscribe?: boolean) { - const [topic, setTopic] = useState<ROSLibTopic<T>|null>(null); - const [message, setMessage] = useState<T|undefined>(); - - useEffect(() => { - if (ros) { - setTopic(new ROSLibTopic<T>({ ros, name: topicName, messageType: topicType })); - } - return () => setTopic(null); - }, [ros, topicName, topicType]); - useEffect(() => { - if (topic && (typeof subscribe == 'undefined' || subscribe)) { - const theTopic = topic; - theTopic.subscribe(setMessage); - return () => theTopic.unsubscribe(); - } - }, [topic, subscribe]); - - return message; -} - -export default RosbridgeConnections; -export { RosbridgeConnectionsContext, useConnection }; diff --git a/src/features/connections/connections-selector.ts b/src/features/connections/connections-selector.ts deleted file mode 100644 index 6d62bdab5ee98317ccc00734e3b9764d467a6677..0000000000000000000000000000000000000000 --- a/src/features/connections/connections-selector.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from '../../app/store'; - -// const selectConnection = createSelector<RootState>((state: RootState) => state.config.connections); -// const selectWebSocketConnectiosn = (state: RootState) => -// const selectConnections = (index: number) => createSelector( -// (state: RootState) => state.config.connections[index] -// ); -// const selectConnections = (state: RootState) => state.config.connections; -// const selectConnection = createSelector(selectConnections, (connections, connectionId: string) => connections[index]); -// const selectWebSocketConnection = (index: number) => createSelector(selectConnection(index), connection => new WebSocket(connection.url)); - -// export { selectConnections, selectConnection, selectWebSocketConnection }; diff --git a/src/features/connections/rosbridge-connections-middleware.ts b/src/features/connections/rosbridge-connections-middleware.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fcfe6105e783e7b6194bec2a403b6f59d03039a --- /dev/null +++ b/src/features/connections/rosbridge-connections-middleware.ts @@ -0,0 +1,16 @@ +import { Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; +import { RootState } from '../../app/store'; +import { connect } from './rosbridge-connections-slice'; + +const connectionsMiddleware: Middleware< + {}, + RootState +> = store => { + + return next => action => { + if (connect.match(action)) { + } + }; +} + +export default connectionsMiddleware; diff --git a/src/features/connections/rosbridge-connections-slice.ts b/src/features/connections/rosbridge-connections-slice.ts new file mode 100644 index 0000000000000000000000000000000000000000..e0c4bac36749ffb024d23db1b75aa5b058dee0f9 --- /dev/null +++ b/src/features/connections/rosbridge-connections-slice.ts @@ -0,0 +1,51 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface ROSTopicState { + name: string; + type: string; + value: any; + subscriberCount: number; +} + +interface RosbridgeConnectionState { + uri: string; + bsonMode: boolean; + status: 'establishing connection' | 'connected' | 'disconnected'; + topics: ROSTopicState[]; +} + +export type RosbridgeConnectionsState = RosbridgeConnectionState[]; + +const initialState: RosbridgeConnectionsState = []; + +const rosbridgeConnectionsSlice = createSlice({ + name: 'rosbridgeConnection', + initialState, + reducers: { + connect(state, action: PayloadAction<{ uri: string, bsonMode: boolean}>) { + const { uri, bsonMode } = action.payload; + if (!state.find(connection => connection.uri === uri)) { + state.push({ + uri, + bsonMode, + status: 'establishing connection', + topics: [], + }); + } + }, + disconnect(state, action: PayloadAction<string>) { + const uri = action.payload; + const index = state.findIndex(connection => connection.uri === uri) + if (index !== -1) { + state.splice(index, 1); + } + }, + subscribe(state, action: PayloadAction<{ uri: string, topic: string }>) { + }, + unsubscribe(state, action: PayloadAction<{ uri: string, topic: string }>) { + }, + } +}); + +export const { connect, disconnect, subscribe, unsubscribe } = rosbridgeConnectionsSlice.actions; +export default rosbridgeConnectionsSlice.reducer; diff --git a/src/features/images/CompressedImage.tsx b/src/features/images/CompressedImage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..83ebaf3f1c34dbed8fbee0ed3266e7b0733cab28 --- /dev/null +++ b/src/features/images/CompressedImage.tsx @@ -0,0 +1,30 @@ +import { CSSProperties } from "react"; +import Show from "../../common/Show"; +import { useTopic } from "../../common/store"; + +export interface CompressedROSImageProps { + connection: string; + topic: string; + style?: CSSProperties; +} + +function CompressedROSImage(props: CompressedROSImageProps) { + const compressedImage = useTopic(props.connection, props.topic, 'sensor_msgs/CompressedImage'); + // Todo: use direct dom manipulation here! + + return ( + <Show when={compressedImage} fallback={<>Loading {props.topic}...</>}> + { + compressedImage => + <img + style={props.style} + src={`data:image/${compressedImage.format};base64, ${compressedImage.data}`} + title={props.topic} + alt={props.topic} + /> + } + </Show> + ); +}; + +export default CompressedROSImage; diff --git a/src/features/images/Image.tsx b/src/features/images/Image.tsx new file mode 100644 index 0000000000000000000000000000000000000000..619f8c5fa71cbb96eed70125d6a8655098a14954 --- /dev/null +++ b/src/features/images/Image.tsx @@ -0,0 +1,98 @@ +import { CSSProperties } from "react"; + +interface RawROSImageProps { + connectionName: string; + topic: string; + style?: CSSProperties; +} + +// function RawROSImage(props: RawROSImageProps) { +// const ros = useConnection(props.connectionName); +// const image = useTopic<Image>(ros, props.topic, 'sensor_msgs/Image'); +// const [context, setContext] = useState<CanvasRenderingContext2D|null>(null); + +// let imageData: ImageData; + +// useEffect(() => { +// if (image && context) { +// if (!imageData || (imageData.width !== image.width || imageData.height !== image.height)) { +// imageData = context.createImageData(image.width, image.height); +// } +// const data = atob(image.data); +// switch (image.encoding) { +// case "mono8": +// for (let y = 0; y < image.height; ++y) { +// for (let x = 0; x < image.width; ++x) { +// const destinationBaseIndex = (y * image.width + x) * 4; +// const sourceBaseIndex = y * image.step + x; + +// for (let i = 0; i < 3; ++i) { +// imageData.data[destinationBaseIndex + i] = data.charCodeAt(sourceBaseIndex); +// } +// imageData.data[destinationBaseIndex + 3] = 255; +// } +// } +// break; + +// case "rgb8": +// for (let y = 0; y < image.height; ++y) { +// for (let x = 0; x < image.width; ++x) { +// const destinationBaseIndex = (y * image.width + x) * 4; +// const sourceBaseIndex = y * image.step + x * 3; + +// for (let i = 0; i < 3; ++i) { +// imageData.data[destinationBaseIndex + i] = data.charCodeAt(sourceBaseIndex + i); +// } +// imageData.data[destinationBaseIndex + 3] = 255; +// } +// } +// break; + +// case "bgr8": +// for (let y = 0; y < image.height; ++y) { +// for (let x = 0; x < image.width; ++x) { +// const destinationBaseIndex = (y * image.width + x) * 4; +// const sourceBaseIndex = y * image.step + x * 3; + +// for (let i = 0; i < 3; ++i) { +// imageData.data[destinationBaseIndex + i] = data.charCodeAt(sourceBaseIndex + 2 - i); +// } +// imageData.data[destinationBaseIndex + 3] = 255; +// } +// } +// break; + +// case "16UC1": +// for (let y = 0; y < image.height; ++y) { +// for (let x = 0; x < image.width; ++x) { +// const destinationBaseIndex = (y * image.width + x) * 4; +// const sourceBaseIndex = y * image.step + x * 2; +// const p0 = data.charCodeAt(sourceBaseIndex + 0); +// const p1 = data.charCodeAt(sourceBaseIndex + 1); +// const value = (p0 << 0 | p1 << 8) / Math.pow(2, 16) * 256; + +// for (let i = 0; i < 3; ++i) { +// imageData.data[destinationBaseIndex + i] = value; +// } +// imageData.data[destinationBaseIndex + 3] = 255; +// } +// } +// break; + +// default: +// console.error(`Unsupported encoding: ${image.encoding}`); +// } +// context.putImageData(imageData, 0, 0); +// } +// }, [image, context]); + +// return ( +// <canvas +// ref={canvas => setContext(canvas?.getContext("2d") || null)} +// width={image?.width} +// height={image?.height} +// title={props.topic} +// style={props.style} +// /> +// ); +// }; diff --git a/src/features/images/ImageCarousel.tsx b/src/features/images/ImageCarousel.tsx index 02f1b34a962dacfff36dda086042b49efea4a3df..b3888fc9df6f494571f54bb880c417136ebb761e 100644 --- a/src/features/images/ImageCarousel.tsx +++ b/src/features/images/ImageCarousel.tsx @@ -1,159 +1,27 @@ import { KeyboardArrowLeft, KeyboardArrowRight } from '@mui/icons-material'; import { Box, Button, MobileStepper, Paper, SxProps, Theme } from '@mui/material'; import { CSSProperties, PropsWithoutRef, useEffect, useState } from 'react'; -import SwipeableViews from 'react-swipeable-views'; -import { useConnection, useTopic, useTopicsOfType } from '../connections/RosbridgeConnections'; import Show from '../../common/Show'; +import CompressedROSImage from './CompressedImage'; -interface Image { - width: number; - height: number; - encoding: "rgb8" | "mono8" | "16UC1" | "bgr8"; - step: number; - data: string; -} - -interface CompressedImage { - format: "jpeg" | "png"; - data: string; -} - -interface RawROSImageProps { - connectionName: string; +export interface ImageCarouselImage { topic: string; - style?: CSSProperties; + name: string; } -function RawROSImage(props: RawROSImageProps) { - const ros = useConnection(props.connectionName); - const image = useTopic<Image>(ros, props.topic, 'sensor_msgs/Image'); - const [context, setContext] = useState<CanvasRenderingContext2D|null>(null); - - let imageData: ImageData; - - useEffect(() => { - if (image && context) { - if (!imageData || (imageData.width !== image.width || imageData.height !== image.height)) { - imageData = context.createImageData(image.width, image.height); - } - const data = atob(image.data); - switch (image.encoding) { - case "mono8": - for (let y = 0; y < image.height; ++y) { - for (let x = 0; x < image.width; ++x) { - const destinationBaseIndex = (y * image.width + x) * 4; - const sourceBaseIndex = y * image.step + x; - - for (let i = 0; i < 3; ++i) { - imageData.data[destinationBaseIndex + i] = data.charCodeAt(sourceBaseIndex); - } - imageData.data[destinationBaseIndex + 3] = 255; - } - } - break; - - case "rgb8": - for (let y = 0; y < image.height; ++y) { - for (let x = 0; x < image.width; ++x) { - const destinationBaseIndex = (y * image.width + x) * 4; - const sourceBaseIndex = y * image.step + x * 3; - - for (let i = 0; i < 3; ++i) { - imageData.data[destinationBaseIndex + i] = data.charCodeAt(sourceBaseIndex + i); - } - imageData.data[destinationBaseIndex + 3] = 255; - } - } - break; - - case "bgr8": - for (let y = 0; y < image.height; ++y) { - for (let x = 0; x < image.width; ++x) { - const destinationBaseIndex = (y * image.width + x) * 4; - const sourceBaseIndex = y * image.step + x * 3; - - for (let i = 0; i < 3; ++i) { - imageData.data[destinationBaseIndex + i] = data.charCodeAt(sourceBaseIndex + 2 - i); - } - imageData.data[destinationBaseIndex + 3] = 255; - } - } - break; - - case "16UC1": - for (let y = 0; y < image.height; ++y) { - for (let x = 0; x < image.width; ++x) { - const destinationBaseIndex = (y * image.width + x) * 4; - const sourceBaseIndex = y * image.step + x * 2; - const p0 = data.charCodeAt(sourceBaseIndex + 0); - const p1 = data.charCodeAt(sourceBaseIndex + 1); - const value = (p0 << 0 | p1 << 8) / Math.pow(2, 16) * 256; - - for (let i = 0; i < 3; ++i) { - imageData.data[destinationBaseIndex + i] = value; - } - imageData.data[destinationBaseIndex + 3] = 255; - } - } - break; - - default: - console.error(`Unsupported encoding: ${image.encoding}`); - } - context.putImageData(imageData, 0, 0); - } - }, [image, context]); - - return ( - <canvas - ref={canvas => setContext(canvas?.getContext("2d") || null)} - width={image?.width} - height={image?.height} - title={props.topic} - style={props.style} - /> - ); -}; - -export interface CompressedROSImageProps { - connection: string; - topic: string; - style?: CSSProperties; -} - -function CompressedROSImage(props: RawROSImageProps) { - const ros = useConnection(props.connectionName); - const image = useTopic<CompressedImage>(ros, props.topic, 'sensor_msgs/CompressedImage'); - - return ( - <Show when={image} fallback={<>Loading {props.topic}...</>}> - { - compressedImage => - <img - style={props.style} - src={`data:image/${compressedImage.format};base64, ${compressedImage.data}`} - title={props.topic} - /> - } - </Show> - ); -}; - export interface ImageCarouselProps { - connectionName: string; - prefix?: string; + connection: string; + images: ImageCarouselImage[]; sx?: SxProps<Theme>; } export default function ImageCarousel(props: PropsWithoutRef<ImageCarouselProps>) { - const imageTopics = useTopicsOfType(props.connectionName, ['sensor_msgs/Image', 'sensor_msgs/CompressedImage']) || []; - const topics = props.prefix ? imageTopics.filter(topic => topic.name.startsWith(props.prefix as string)) : imageTopics; const [activeStep, setActiveStep] = useState(0); - if (topics.length === 0) { + if (props.images.length === 0) { return null; } - const topic = topics[activeStep]; + const image = props.images[activeStep]; return ( <Box @@ -167,44 +35,31 @@ export default function ImageCarousel(props: PropsWithoutRef<ImageCarouselProps> <Box component="div" style={{ - position: "relative", flexGrow: 1, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', }} > - { - topic.type === 'sensor_msgs/Image' ? - <RawROSImage - key={topic.name} - connectionName={props.connectionName} - topic={topic.name} - style={{ - position: "absolute", - top: "50%", - left: "50%", - transform: "translateX(-50%) translateY(-50%)", - maxWidth: "100%", - maxHeight: "100%", - }} - /> - : - <CompressedROSImage - key={topic.name} - connectionName={props.connectionName} - topic={topic.name} - style={{ - position: "absolute", - top: "50%", - left: "50%", - transform: "translateX(-50%) translateY(-50%)", - maxWidth: "100%", - maxHeight: "100%", - }} - /> - } + <CompressedROSImage + key={image.topic} + connection={props.connection} + topic={image.topic} + style={{ + maxWidth: "100%", + }} + /> + <Box + component="span" + style={{ + }} + > + {image.name} + </Box> </Box> <MobileStepper variant="dots" - steps={topics.length} + steps={props.images.length} position="static" activeStep={activeStep} sx={{ @@ -217,7 +72,7 @@ export default function ImageCarousel(props: PropsWithoutRef<ImageCarouselProps> setActiveStep(step => step + 1); event.stopPropagation(); }} - disabled={activeStep === topics.length - 1} + disabled={activeStep === props.images.length - 1} > <KeyboardArrowRight /> </Button> diff --git a/src/features/robots/BatteryIcon.tsx b/src/features/robots/BatteryIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..355096906699cb6e98e9631973844b9d20a9865a --- /dev/null +++ b/src/features/robots/BatteryIcon.tsx @@ -0,0 +1,65 @@ +import BatteryUnknownIcon from '@mui/icons-material/BatteryUnknown'; +import Battery20Icon from '@mui/icons-material/Battery20'; +import Battery30Icon from '@mui/icons-material/Battery30'; +import Battery50Icon from '@mui/icons-material/Battery50'; +import Battery60Icon from '@mui/icons-material/Battery60'; +import Battery80Icon from '@mui/icons-material/Battery80'; +import Battery90Icon from '@mui/icons-material/Battery90'; +import BatteryFullIcon from '@mui/icons-material/BatteryFull'; +import BatteryCharging20Icon from '@mui/icons-material/BatteryCharging20'; +import BatteryCharging30Icon from '@mui/icons-material/BatteryCharging30'; +import BatteryCharging50Icon from '@mui/icons-material/BatteryCharging50'; +import BatteryCharging60Icon from '@mui/icons-material/BatteryCharging60'; +import BatteryCharging80Icon from '@mui/icons-material/BatteryCharging80'; +import BatteryCharging90Icon from '@mui/icons-material/BatteryCharging90'; +import BatteryChargingFullIcon from '@mui/icons-material/BatteryChargingFull'; +import BatteryAlertIcon from '@mui/icons-material/BatteryAlert'; + +interface BatteryIconProps { + percentage?: number; + isCharging?: boolean; +} + +export default function BatteryIcon(props: BatteryIconProps) { + if (typeof props.percentage === 'undefined') { + return <BatteryUnknownIcon color="error" />; + } + + if (props.percentage < 0) { + return <BatteryAlertIcon color="error" />; + } else if (props.percentage < 0.1) { + return props.isCharging ? + <BatteryCharging20Icon color="warning" /> : + <BatteryAlertIcon color="warning" />; + } else if (props.percentage < 0.25) { + return props.isCharging ? + <BatteryCharging20Icon color="success" /> : + <Battery20Icon />; + } else if (props.percentage < 0.4) { + return props.isCharging ? + <BatteryCharging30Icon color="success" /> : + <Battery30Icon />; + } else if (props.percentage < 0.55) { + return props.isCharging ? + <BatteryCharging50Icon color="success" /> : + <Battery50Icon />; + } else if (props.percentage < 0.7) { + return props.isCharging ? + <BatteryCharging60Icon color="success" /> : + <Battery60Icon />; + } else if (props.percentage < 0.85) { + return props.isCharging ? + <BatteryCharging80Icon color="success" /> : + <Battery80Icon />; + } else if (props.percentage < 0.95) { + return props.isCharging ? + <BatteryCharging90Icon color="success" /> : + <Battery90Icon />; + } else if (props.percentage <= 1.0) { + return props.isCharging ? + <BatteryChargingFullIcon color="success" /> : + <BatteryFullIcon />; + } else { //props.percentage > 1 + return <BatteryAlertIcon color="error" />; + } +} diff --git a/src/features/robots/RobotDetailsDialog.tsx b/src/features/robots/RobotDetailsDialog.tsx index 831514d52afa60c43807b04948cefbda62ad2cee..18d25acb0f8fc20f280b785e21ef6e06f2337ac8 100644 --- a/src/features/robots/RobotDetailsDialog.tsx +++ b/src/features/robots/RobotDetailsDialog.tsx @@ -1,58 +1,20 @@ import Dialog from '../../common/Dialog'; -import { useAppSelector } from '../../app/hooks'; -import ImageCarousel from '../images/ImageCarousel'; import { Split } from '@geoffcox/react-splitter'; -import { Box, Button, TextField, useTheme } from '@mui/material'; +import { Box, Button, useTheme } from '@mui/material'; import Viewport from '../viewport/Viewport'; -import { useConnection, useTopic, useTopicFromConnection } from '../connections/RosbridgeConnections'; -import RobotDialog from './RobotDialog'; import { useState } from 'react'; - -enum DiagnosticStatusLevel { - OK = 0, - WARN = 1, - ERROR = 2, - STALE = 3, -} +import { useRobot } from '../config/config-hooks'; +import { useTopic } from '../../common/store'; export interface RobotDetailsDialogProps { robot: string; close: () => void; - message?: string; - messageLevel?: DiagnosticStatusLevel; - batteryPercentage?: number; - clearMessage?: () => void; -} - -interface Pose { - position: {x: number, y: number, z: number }; - orientation: {w: number, x: number, y: number, z: number } -} - -interface PoseStamped { - pose: Pose; } const RobotDetailsDialog = (props: RobotDetailsDialogProps) => { - const robot = useAppSelector(state => state.config.robots[props.robot]); - const ros = useConnection(robot.connection); - const poseStamped = useTopic<PoseStamped>(ros, robot.poseTopic || "", 'geometry_msgs/PoseStamped'); + const robot = useRobot(props.robot); + const poseStamped = useTopic(robot?.rosbridge.uri, robot?.topics.pose, 'geometry_msgs/PoseStamped'); const [showEditDialog, setShowEditDialog] = useState(false); - const theme = useTheme(); - - let color = 'transparent'; - let messageTitle = ''; - - if (props.messageLevel === DiagnosticStatusLevel.OK) { - color = theme.palette.info.main; - messageTitle = 'Info'; - } else if (props.messageLevel === DiagnosticStatusLevel.WARN) { - color = theme.palette.warning.main; - messageTitle = 'Warning'; - } else if (props.messageLevel === DiagnosticStatusLevel.ERROR) { - color = theme.palette.error.main; - messageTitle = 'Error'; - } return ( <Dialog @@ -97,25 +59,6 @@ const RobotDetailsDialog = (props: RobotDetailsDialogProps) => { { Math.round((poseStamped?.pose?.orientation.z || 0) * 100) / 100 } </Box> - <Box - component="div" - sx={{ - color, - visibility: props.message ? 'visible' : 'hidden', - }} - > - <h2>{ messageTitle }</h2> - { props.message } - <Box - component="div" - > - <Button - onClick={props.clearMessage} - > - Clear Message - </Button> - </Box> - </Box> <Box component="div" sx={{ @@ -164,11 +107,6 @@ const RobotDetailsDialog = (props: RobotDetailsDialogProps) => { > Edit </Button> - <RobotDialog - open={showEditDialog} - close={() => setShowEditDialog(false)} - edit={props.robot} - /> </Box> <Split @@ -176,13 +114,13 @@ const RobotDetailsDialog = (props: RobotDetailsDialogProps) => { horizontal > <Viewport /> - <ImageCarousel + {/*<ImageCarousel connectionName={robot.connection} prefix={robot.prefix} sx={{ height: "100%", }} - /> + />*/} </Split> </Split> </Dialog> diff --git a/src/features/robots/RobotDialog.tsx b/src/features/robots/RobotDialog.tsx deleted file mode 100644 index dc7a08b6c2d28ef0f261f1bd9147cc54daf52a7f..0000000000000000000000000000000000000000 --- a/src/features/robots/RobotDialog.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { MenuItem } from '@mui/material'; -import Button from '@mui/material/Button'; -import FormControl from '@mui/material/FormControl'; -import TextField from '@mui/material/TextField'; -import { useEffect, useState } from 'react'; -import { useAppDispatch, useAppSelector } from '../../app/hooks'; -import { robotAdded, robotRemoved, RobotType } from '../config/config-slice'; -import { Topic, useTopics, useTopicsOfType } from '../connections/RosbridgeConnections'; -import Dialog from '../../common/Dialog'; - -export interface RobotDialogProps { - open?: boolean; - edit?: string; - close: () => void; -} - -function extractPrefixes(topics: Topic[]) { - const prefixes = ["/"]; - for (const topic of topics) { - for (let separatorPosition = topic.name.indexOf('/'); - separatorPosition !== -1; - separatorPosition = topic.name.indexOf('/', separatorPosition + 1)) { - const prefix = topic.name.substring(0, separatorPosition + 1); - if (prefixes.indexOf(prefix) === -1) { - prefixes.push(prefix); - } - } - } - return prefixes; -} - -const RobotDialog = (props: RobotDialogProps) => { - const connections = useAppSelector(state => state.config.connections); - const robots = useAppSelector(state => state.config.robots); - const connectionNames = Object.keys(connections); - const dispatch = useAppDispatch(); - const [name, setName] = useState("New Robot"); - const [connection, setConnection] = useState(""); - const [prefix, setPrefix] = useState("/"); - const topics = useTopics(connection); - const prefixes = topics ? extractPrefixes(topics) : ["/"]; - const [type, setType] = useState<RobotType>("Crawler"); - - const poseTopics = useTopicsOfType(connection, ['geometry_msgs/PoseStamped']); - const [poseTopic, setPoseTopic] = useState<string>(); - - useEffect(() => { - if (!props.open) { - return; - } - - if (props.edit) { - const config = robots[props.edit]; - setName(props.edit); - setConnection(config.connection); - setPrefix(config.prefix); - setPoseTopic(config.poseTopic); - setType(config.type); - } else { - let name = "New Robot"; - for (let i = 2; name in robots; ++i) { - name = `New Robot ${i}`; - } - setName(name); - if (Object.keys(connections).length > 0) { - setConnection(Object.keys(connections)[0]); - } - setPrefix('/'); - setType("Crawler"); - } - }, [props.open, props.edit, robots, connections]); - - return ( - <Dialog - open={props.open || false} - title={props.edit ? `Edit Robot` : "Add New Robot"} - close={props.close} - > - <FormControl size="small" sx={{ m: 1 }}> - <TextField - label="Name" - variant="standard" - value={name} - error={name in robots && props.edit !== name} - onChange={event => setName(event.target.value)} - /> - <TextField - label="Type" - variant="standard" - value={type} - onChange={event => setType(event.target.value as RobotType)} - select - > - <MenuItem value="Crawler">Crawler</MenuItem> - <MenuItem value="Airborne Drone">Airborne Drone</MenuItem> - <MenuItem value="Underwater Drone">Underwater Drone</MenuItem> - </TextField> - <TextField - label="Connection" - variant="standard" - value={connection} - onChange={event => setConnection(event.target.value)} - select - > - <MenuItem - key={null} - value={undefined} - > - None - </MenuItem> - { - connectionNames.map(connectionName => - <MenuItem - key={connectionName} - value={connectionName} - > - {connectionName} - </MenuItem> - ) - } - </TextField> - <TextField - label="Prefix" - variant="standard" - value={prefix} - onChange={event => setPrefix(event.target.value)} - select - > - <MenuItem - key={null} - value={undefined} - > - None - </MenuItem> - { - prefixes.map(prefix => - <MenuItem - key={prefix} - value={prefix} - > - {prefix} - </MenuItem> - ) - } - </TextField> - <TextField - label="Pose" - variant="standard" - value={poseTopic} - onChange={event => setPoseTopic(event.target.value)} - select - > - <MenuItem - key={null} - value={undefined} - > - None - </MenuItem> - { - poseTopics?.map(topic => - <MenuItem - key={topic.name} - value={topic.name} - > - {topic.name} - </MenuItem> - ) - } - </TextField> - <Button - variant="text" - disabled={name in connections} - onClick={() => { - if (props.edit) { - dispatch(robotRemoved(props.edit)); - } - dispatch(robotAdded({ name, config: { prefix, connection, poseTopic, type } })); - props.close(); - }} - > - { - props.edit ? 'Save' : 'Add' - } - </Button> - </FormControl> - </Dialog> - ); -}; - -export default RobotDialog; diff --git a/src/features/robots/RobotList.tsx b/src/features/robots/RobotList.tsx index 0cca38f417e5ec45ab22538ddba5193bc2c0240a..50c5992b16b1225628503a6b6c14c596e5b3e0ba 100644 --- a/src/features/robots/RobotList.tsx +++ b/src/features/robots/RobotList.tsx @@ -1,136 +1,30 @@ -import { PropsWithoutRef, useEffect, useState } from 'react'; -import { RobotConfig } from '../config/config-slice'; +import { PropsWithoutRef, useCallback, useState } from 'react'; import { useAppSelector } from '../../app/hooks'; +import { Robot as RobotConfig } from '../../schemas/Robot.schema'; import For from '../../common/For'; import { Box } from '@mui/system'; -import { Button, Collapse, IconButton, List, ListItem, ListItemButton, Paper, useTheme } from '@mui/material'; +import { Button, IconButton, MobileStepper, Paper, useTheme } from '@mui/material'; import { Close as CloseIcon } from '@mui/icons-material'; import Show from '../../common/Show'; -import ImageCarousel from '../images/ImageCarousel'; -import RobotDialog from './RobotDialog'; import RobotDetailsDialog from './RobotDetailsDialog'; -import BatteryUnknownIcon from '@mui/icons-material/BatteryUnknown'; -import Battery20Icon from '@mui/icons-material/Battery20'; -import Battery30Icon from '@mui/icons-material/Battery30'; -import Battery50Icon from '@mui/icons-material/Battery50'; -import Battery60Icon from '@mui/icons-material/Battery60'; -import Battery80Icon from '@mui/icons-material/Battery80'; -import Battery90Icon from '@mui/icons-material/Battery90'; -import BatteryFullIcon from '@mui/icons-material/BatteryFull'; -import BatteryCharging20Icon from '@mui/icons-material/BatteryCharging20'; -import BatteryCharging30Icon from '@mui/icons-material/BatteryCharging30'; -import BatteryCharging50Icon from '@mui/icons-material/BatteryCharging50'; -import BatteryCharging60Icon from '@mui/icons-material/BatteryCharging60'; -import BatteryCharging80Icon from '@mui/icons-material/BatteryCharging80'; -import BatteryCharging90Icon from '@mui/icons-material/BatteryCharging90'; -import BatteryChargingFullIcon from '@mui/icons-material/BatteryChargingFull'; -import BatteryAlertIcon from '@mui/icons-material/BatteryAlert'; import InfoIcon from '@mui/icons-material/Info'; -import SettingsIcon from '@mui/icons-material/Settings'; -import { useTopic, useTopicFromConnection } from '../connections/RosbridgeConnections'; - -interface BatteryIconProps { - percentage?: number; - isCharging?: boolean; -} - -function BatteryIcon(props: BatteryIconProps) { - if (typeof props.percentage === 'undefined') { - return <BatteryUnknownIcon color="error" />; - } - - if (props.percentage < 0) { - return <BatteryAlertIcon color="error" />; - } else if (props.percentage < 0.1) { - return props.isCharging ? - <BatteryCharging20Icon color="warning" /> : - <BatteryAlertIcon color="warning" />; - } else if (props.percentage < 0.25) { - return props.isCharging ? - <BatteryCharging20Icon color="success" /> : - <Battery20Icon />; - } else if (props.percentage < 0.4) { - return props.isCharging ? - <BatteryCharging30Icon color="success" /> : - <Battery30Icon />; - } else if (props.percentage < 0.55) { - return props.isCharging ? - <BatteryCharging50Icon color="success" /> : - <Battery50Icon />; - } else if (props.percentage < 0.7) { - return props.isCharging ? - <BatteryCharging60Icon color="success" /> : - <Battery60Icon />; - } else if (props.percentage < 0.85) { - return props.isCharging ? - <BatteryCharging80Icon color="success" /> : - <Battery80Icon />; - } else if (props.percentage < 0.95) { - return props.isCharging ? - <BatteryCharging90Icon color="success" /> : - <Battery90Icon />; - } else if (props.percentage <= 1.0) { - return props.isCharging ? - <BatteryChargingFullIcon color="success" /> : - <BatteryFullIcon />; - } else { //props.percentage > 1 - return <BatteryAlertIcon color="error" />; - } -} +import BatteryIcon from './BatteryIcon'; +import { useStore, useTopic } from '../../common/store'; +import CompressedROSImage from '../images/CompressedImage'; +import ImageCarousel from '../images/ImageCarousel'; interface RobotItemProps { - name: string; - config: RobotConfig; -} - -interface BatteryState { - percentage: number; -} - -enum DiagnosticStatusLevel { - OK = 0, - WARN = 1, - ERROR = 2, - STALE = 3, -} - -interface DiagnosticStatus { - level: DiagnosticStatusLevel; - name: string; - message: string; + robot: RobotConfig; } function RobotItem(props: PropsWithoutRef<RobotItemProps>) { const [showDetailView, setShowDetailView] = useState(false); - const [isEditing, setIsEditing] = useState(false); const theme = useTheme(); - const batteryState = useTopicFromConnection<BatteryState>( - props.config.connection, - `${props.config.prefix}/battery`, - 'sensor_msgs/BatteryState' - ); - const rosMessage = useTopicFromConnection<DiagnosticStatus>( - props.config.connection, - `${props.config.prefix}/status`, - 'diagnostic_msgs/DiagnosticStatus' - ); - const [message, setMessage] = useState<DiagnosticStatus>(); - useEffect( - () => setMessage(rosMessage), - [rosMessage] - ); + const batteryState = useTopic(props.robot.rosbridge.uri, props.robot.topics.batteryState, 'sensor_msgs/BatteryState'); let borderColor = 'transparent'; - if (message?.level === DiagnosticStatusLevel.OK) { - borderColor = theme.palette.info.main; - } else if (message?.level === DiagnosticStatusLevel.WARN) { - borderColor = theme.palette.warning.main; - } else if (message?.level === DiagnosticStatusLevel.ERROR) { - borderColor = theme.palette.error.main; - } - return ( <Box component="div" @@ -144,19 +38,10 @@ function RobotItem(props: PropsWithoutRef<RobotItemProps>) { > <Show when={showDetailView}> <RobotDetailsDialog - robot={props.name} + robot={props.robot.name} close={() => setShowDetailView(false)} - batteryPercentage={batteryState?.percentage} - message={message?.message} - messageLevel={message?.level} - clearMessage={() => setMessage(undefined)} /> </Show> - <RobotDialog - open={isEditing} - close={() => setIsEditing(false)} - edit={props.name} - /> <Box component="div" sx={{ @@ -178,7 +63,7 @@ function RobotItem(props: PropsWithoutRef<RobotItemProps>) { flexGrow: 1, }} > - <h3>{props.name}</h3> + <h3>{props.robot.name}</h3> </Box> <IconButton> <BatteryIcon percentage={batteryState?.percentage} /> @@ -188,11 +73,6 @@ function RobotItem(props: PropsWithoutRef<RobotItemProps>) { > <InfoIcon /> </IconButton> - <IconButton - onClick={() => setIsEditing(true)} - > - <SettingsIcon /> - </IconButton> </Box> <Box component="div" @@ -200,13 +80,15 @@ function RobotItem(props: PropsWithoutRef<RobotItemProps>) { position: 'relative', }} > - <ImageCarousel - connectionName={props.config.connection} - prefix={props.config.prefix} - sx={{ - height: "200px", - }} - /> + <Show when={props.robot.topics.images}> + { + images => + <ImageCarousel + connection={props.robot.rosbridge.uri} + images={images} + /> + } + </Show> </Box> </Box> </Box> @@ -250,12 +132,6 @@ function RobotDetailView(props: PropsWithoutRef<RobotDetailViewProps>) { <CloseIcon /> </Button> </Box> - <ImageCarousel - connectionName={props.config.connection} - prefix={props.config.prefix} - sx={{ - }} - /> </Paper> ); } @@ -264,9 +140,7 @@ export interface RobotListProps { } export default function RobotList(props: PropsWithoutRef<RobotListProps>) { - const robots = useAppSelector(state => state.config.robots); - const [selectedRobot, selectRobot] = useState<string>(); - const [showDetails, setShowDetails] = useState(false); + const robots = useStore(useCallback(state => state.robots, [])); return ( <Paper @@ -294,32 +168,16 @@ export default function RobotList(props: PropsWithoutRef<RobotListProps>) { justifyContent: "center", }} > - <For each={Object.keys(robots).map(name => ({ name, config: robots[name] }))}> - { - robot => - <RobotItem - key={robot.name} - {...robot} - /> - } - </For> + { + Object.keys(robots).map(name => robots[name]).map(robot => + <RobotItem + key={robot.name} + robot={robot} + /> + ) + } </Box> </Box> - <Collapse - in={showDetails} - onTransitionEnd={() => selectRobot(showDetails ? selectedRobot : undefined)} - > - <Show when={selectedRobot}> - { - selectedRobot => - <RobotDetailView - name={selectedRobot} - config={robots[selectedRobot]} - onClose={() => setShowDetails(false)} - /> - } - </Show> - </Collapse> </Paper> ); } diff --git a/src/features/robots/RobotMenu.tsx b/src/features/robots/RobotMenu.tsx deleted file mode 100644 index a82393b45b76bfee82c9f3010e6ff46c8c7fb297..0000000000000000000000000000000000000000 --- a/src/features/robots/RobotMenu.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Divider, IconButton, ListItem, ListItemButton } from "@mui/material"; -import PopoverListButton from "../../common/PopoverListButton"; -import { SmartToy as RobotIcon, Delete } from '@mui/icons-material'; -import { Fragment, useState } from "react"; -import { useAppDispatch, useAppSelector } from '../../app/hooks'; -import For from "../../common/For"; -import Show from "../../common/Show"; -import { RobotConfig, robotRemoved } from "../config/config-slice"; -import RobotDialog from "./RobotDialog"; - -interface RobotsMenuEntryProps { - name: string; - config: RobotConfig; -} - -const RobotsMenuEntry = (props: RobotsMenuEntryProps) => { - const dispatch = useAppDispatch(); - // const connection = useRobot(props.name); - // const connectionConfig = useAppSelector(state => state.config.connections[props.name]); - // const [connected, setConnected] = useState(connection ? connection.isConnected : false); - const [connectionInfoOpen, setRobotInfoOpen] = useState(false); - - // useEffect(() => { - // const connectListener = () => setConnected(true); - // const disconnectListener = () => setConnected(false); - // if (connection) { - // connection.addListener('connection', connectListener); - // connection.addListener('close', disconnectListener); - // } - - // return () => { - // if (connection) { - // connection.removeListener('connection', connectListener); - // connection.removeListener('close', disconnectListener); - // } - // } - // }, [connection]); - - return ( - <ListItem> - <ListItemButton onClick={() => setRobotInfoOpen(true)}> - {props.name} - </ListItemButton> - <IconButton - onClick={() => dispatch(robotRemoved(props.name))} - > - <Delete /> - </IconButton> - </ListItem> - ); -}; - -const RobotsMenu = () => { - const robots = useAppSelector(state => state.config.robots); - const [newRobot, setNewRobot] = useState(false); - - return ( - <Fragment> - <RobotDialog - open={newRobot} - close={() => setNewRobot(false)} - /> - <PopoverListButton - icon={RobotIcon} - title="Robots" - > - <For each={Object.keys(robots).map(name => ({ name, config: robots[name] }))}> - { - robot => <RobotsMenuEntry key={robot.name} {...robot} /> - } - </For> - <Show when={Object.keys(robots).length > 0}> - <Divider /> - </Show> - <ListItem> - <ListItemButton onClick={() => setNewRobot(true)}>Add Robot</ListItemButton> - </ListItem> - </PopoverListButton> - </Fragment> - ); -}; - -export default RobotsMenu; diff --git a/src/features/viewport/Mesh.tsx b/src/features/viewport/Mesh.tsx index 2e3885b0bca0739cec2d0e599c2ef66c0039ad6e..efea1404b3eda0c6631a2ecfadc3f0f63c3ffb06 100644 --- a/src/features/viewport/Mesh.tsx +++ b/src/features/viewport/Mesh.tsx @@ -1,57 +1,17 @@ import { EventHandlers } from "@react-three/fiber/dist/declarations/src/core/events"; import { useEffect, useState } from "react"; -import { Ros, Vector3 } from "roslib"; import { BufferGeometry, DoubleSide, Float32BufferAttribute, Uint32BufferAttribute } from "three"; -import { useTopic } from "../connections/RosbridgeConnections"; - -export interface MeshTriangleIndices { - vertex_indices: [number, number, number]; -} - -export interface MeshGeometry { - vertices: Vector3[]; - vertex_normals: Vector3[]; - faces: MeshTriangleIndices[]; -} - -export interface MeshGeometryStamped { - uuid: string; - mesh_geometry: MeshGeometry; -} +import { useTopic } from "../../common/store"; export interface MeshProps extends EventHandlers { - ros: Ros; + rosbridgeURI: string; topic: string; onGeometryUpdate?: (geometry?: BufferGeometry) => void; } -// const quad = { -// vertices: new Float32Array([ -// -1, 1, 0, -// 1, 1, 0, -// -1, -1, 0, -// 1, -1, 0, -// ]), -// indices: new Uint32Array([ -// 0, 1, 2, -// 1, 2, 3, -// ]), -// } - -// const triangle = { -// vertices: new Float32Array([ -// -1, -1, 0, -// 0, 1, 0, -// 1, -1, 0, -// ]), -// indices: new Uint32Array([ -// 0, 1, 2, -// ]), -// } - const Mesh = (props: MeshProps) => { - const mesh = useTopic<MeshGeometryStamped>(props.ros, props.topic, 'mesh_msgs/MeshGeometryStamped', true); - const [geometry, setGeometry] = useState<BufferGeometry|undefined>(); + const mesh = useTopic(props.rosbridgeURI, props.topic, 'mesh_msgs/MeshGeometryStamped'); + const [geometry, setGeometry] = useState<BufferGeometry>(); useEffect(() => { if (mesh) { @@ -85,19 +45,6 @@ const Mesh = (props: MeshProps) => { } }, [mesh]); -// useEffect(() => { -// let i = 0; -// const x = setInterval(() => { -// const geometry = new BufferGeometry(); -// geometry.setIndex(new Uint32BufferAttribute(i % 2 == 0 ? quad.indices : triangle.indices, 1)); -// geometry.setAttribute('position', new Float32BufferAttribute(i % 2 == 0 ? quad.vertices : triangle.vertices, 3)); -// geometry.computeVertexNormals(); -// setGeometry(geometry); -// ++i; -// }, 1000); -// return () => clearInterval(x); -// }, []); - return ( <mesh onClick={props.onClick} diff --git a/src/features/viewport/PointCloud.tsx b/src/features/viewport/PointCloud.tsx index e80c6b3b49926f0edd7f674813e3bae92e10087f..bd4043cb7d6cff85cc91c81288431b4154aee3e8 100644 --- a/src/features/viewport/PointCloud.tsx +++ b/src/features/viewport/PointCloud.tsx @@ -1,65 +1,87 @@ - import { useEffect, useState } from "react"; -import { Ros } from "roslib"; -import { Color } from "three"; -import For from "../../common/For"; -import { useTopic } from "../connections/RosbridgeConnections"; - +import { BufferGeometry, Float32BufferAttribute } from "three"; +import { useTopic } from "../../common/store"; -export interface MeshGeometryStamped { - // uuid: string; - // mesh_geometry: MeshGeometry; - width: number; - data: string; -} -export interface PointCloudProps { - ros: Ros; +export interface PointCloud2Props { + rosbridgeURI: string; topic: string; } -interface P { - x: number; - y: number; - z: number; - intensity: number; -} - -const PointCloud = (props: PointCloudProps) => { - const pointCloud = useTopic<MeshGeometryStamped>(props.ros, props.topic, 'sensor_msgs/PointCloud2', true); - const [points, setPoints] = useState<P[]>([]); +const PointCloud2 = (props: PointCloud2Props) => { + const cloud = useTopic(props.rosbridgeURI, props.topic, 'sensor_msgs/PointCloud2'); + const [geometry, setGeometry] = useState<BufferGeometry|undefined>(); + const [offset, setOffset] = useState<[number, number, number]>([0, 0, 0]); useEffect(() => { - // console.log(pointCloud?.width); - // console.log(pointCloud?.data); - if (pointCloud) { - const x = Uint8Array.from(atob(pointCloud.data), c => c.charCodeAt(0)); - const floats = new Float32Array(x.buffer); - const ps: P[] = []; - for (let i = 0; i < pointCloud.width; ++i) { - ps.push({ - x: floats[i * 4 + 0], - y: floats[i * 4 + 1], - z: floats[i * 4 + 2], - intensity: floats[i * 4 + 3], - }); + if (cloud) { + const t0 = performance.now(); + const uint8Array = Uint8Array.from(atob(cloud.data), c => c.charCodeAt(0)); + const t1 = performance.now(); + console.log(`size: ${uint8Array.length / 1000}kb`); + + const bufferView = new DataView(uint8Array.buffer); + const geometry = new BufferGeometry(); + const positions = new Float32Array(cloud.width * cloud.height * 3); + const colors = new Float32Array(cloud.width * cloud.height * 3); + + const xField = cloud.fields.find(field => field.name === 'x'); + const yField = cloud.fields.find(field => field.name === 'y'); + const zField = cloud.fields.find(field => field.name === 'z'); + const colorField = cloud.fields.find(field => field.name === 'rgba'); + + if (!xField || !yField || !zField || !colorField) { + return; + } + + console.log(`Start processing ${cloud.width * cloud.height} points`); + const offset: [number, number, number] = [0, 0, 0]; + const t2 = performance.now(); + + for (let y = 0; y < cloud.height; ++y) { + for (let x = 0; x < cloud.width; ++x) { + const byteOffset = cloud.row_step * y + x * cloud.point_step; + const pointOffset = y * cloud.width + x; + + positions[pointOffset * 3 + 0] = -bufferView.getFloat32(byteOffset + xField.offset, !cloud.is_bigendian); + positions[pointOffset * 3 + 2] = bufferView.getFloat32(byteOffset + yField.offset, !cloud.is_bigendian); + positions[pointOffset * 3 + 1] = bufferView.getFloat32(byteOffset + zField.offset, !cloud.is_bigendian); + + const color = bufferView.getUint32(byteOffset + colorField.offset, !cloud.is_bigendian); + colors[pointOffset * 3 + 0] = ((color >> 16) & 0xff) / 255.0; + colors[pointOffset * 3 + 1] = ((color >> 8) & 0xff) / 255.0; + colors[pointOffset * 3 + 2] = ((color >> 0) & 0xff) / 255.0; + + for (let i = 0; i < 3; ++i) { + offset[i] = (pointOffset / (pointOffset + 1)) * offset[i] - positions[pointOffset * 3 + i] / (pointOffset + 1); + } + } } - setPoints(ps); - // console.log(floats); + setOffset(offset); + const t3 = performance.now(); + + geometry.setAttribute('position', new Float32BufferAttribute(positions, 3)); + geometry.setAttribute('color', new Float32BufferAttribute(colors, 3)); + setGeometry(geometry); + + console.log(`Finished processing ${cloud.width * cloud.height} points`); + console.log(`${t1 - t0} ms`); + console.log(`${t2 - t1} ms`); + console.log(`${t3 - t2} ms`); + + return () => geometry.dispose(); + } else { + setGeometry(undefined); } - }, [pointCloud]); + }, [cloud]); return ( - <For each={points}> - { - (p, i) => - <mesh position={[-p.x, p.z, p.y]} key={i}> - <sphereGeometry args={[0.02]}> - </sphereGeometry> - <meshStandardMaterial color={new Color(p.intensity / 50, 0, 0)} /> - </mesh> - } - </For> + <points + geometry={geometry} + position={offset} + > + <pointsMaterial size={0.01} vertexColors /> + </points> ); } -export default PointCloud; +export default PointCloud2; diff --git a/src/features/viewport/PointCloud2.tsx b/src/features/viewport/PointCloud2.tsx deleted file mode 100644 index 3a792db9b5bde2b4c7ceffd25f682809f6bc12a1..0000000000000000000000000000000000000000 --- a/src/features/viewport/PointCloud2.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useEffect, useState } from "react"; -import { Ros } from "roslib"; -import { BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute } from "three"; -import { useTopic } from "../connections/RosbridgeConnections"; - -export interface PointFieldMessage { - name: string; - offset: number; - datatype: number; - count: number; -} - -export interface PointCloud2Message { - width: number; - height: number; - fields: PointFieldMessage[]; - is_bigendian: boolean; - point_step: number; - row_step: number; - data: string; - is_dense: boolean; -} - -export interface PointCloud2Props { - ros: Ros; - topic: string; - onGeometryUpdate?: (geometry?: BufferGeometry) => void; -} - -const PointCloud2 = (props: PointCloud2Props) => { - const cloud = useTopic<PointCloud2Message>(props.ros, props.topic, 'sensor_msgs/PointCloud2', true); - const [geometry, setGeometry] = useState<BufferGeometry|undefined>(); - const [offset, setOffset] = useState<[number, number, number]>([0, 0, 0]); - - useEffect(() => { - if (cloud) { - const t0 = performance.now(); - const uint8Array = Uint8Array.from(atob(cloud.data), c => c.charCodeAt(0)); - const t1 = performance.now(); - console.log(`size: ${uint8Array.length / 1000}kb`); - - const bufferView = new DataView(uint8Array.buffer); - const geometry = new BufferGeometry(); - const positions = new Float32Array(cloud.width * cloud.height * 3); - const colors = new Float32Array(cloud.width * cloud.height * 3); - - const xField = cloud.fields.find(field => field.name === 'x'); - const yField = cloud.fields.find(field => field.name === 'y'); - const zField = cloud.fields.find(field => field.name === 'z'); - const colorField = cloud.fields.find(field => field.name === 'rgba'); - - if (!xField || !yField || !zField || !colorField) { - return; - } - - console.log(`Start processing ${cloud.width * cloud.height} points`); - const offset: [number, number, number] = [0, 0, 0]; - const t2 = performance.now(); - - for (let y = 0; y < cloud.height; ++y) { - for (let x = 0; x < cloud.width; ++x) { - const byteOffset = cloud.row_step * y + x * cloud.point_step; - const pointOffset = y * cloud.width + x; - - positions[pointOffset * 3 + 0] = -bufferView.getFloat32(byteOffset + xField.offset, !cloud.is_bigendian); - positions[pointOffset * 3 + 2] = bufferView.getFloat32(byteOffset + yField.offset, !cloud.is_bigendian); - positions[pointOffset * 3 + 1] = bufferView.getFloat32(byteOffset + zField.offset, !cloud.is_bigendian); - - const color = bufferView.getUint32(byteOffset + colorField.offset, !cloud.is_bigendian); - colors[pointOffset * 3 + 0] = ((color >> 16) & 0xff) / 255.0; - colors[pointOffset * 3 + 1] = ((color >> 8) & 0xff) / 255.0; - colors[pointOffset * 3 + 2] = ((color >> 0) & 0xff) / 255.0; - - for (let i = 0; i < 3; ++i) { - offset[i] = (pointOffset / (pointOffset + 1)) * offset[i] - positions[pointOffset * 3 + i] / (pointOffset + 1); - } - } - } - setOffset(offset); - const t3 = performance.now(); - - geometry.setAttribute('position', new Float32BufferAttribute(positions, 3)); - geometry.setAttribute('color', new Float32BufferAttribute(colors, 3)); - setGeometry(geometry); - - console.log(`Finished processing ${cloud.width * cloud.height} points`); - console.log(`${t1 - t0} ms`); - console.log(`${t2 - t1} ms`); - console.log(`${t3 - t2} ms`); - } else { - // setGeometry(undefined); - // if (props.onGeometryUpdate) { - // props.onGeometryUpdate(undefined); - // } - } - }, [cloud]); - -// useEffect(() => { -// let i = 0; -// const x = setInterval(() => { -// const geometry = new BufferGeometry(); -// geometry.setIndex(new Uint32BufferAttribute(i % 2 == 0 ? quad.indices : triangle.indices, 1)); -// geometry.setAttribute('position', new Float32BufferAttribute(i % 2 == 0 ? quad.vertices : triangle.vertices, 3)); -// geometry.computeVertexNormals(); -// setGeometry(geometry); -// ++i; -// }, 1000); -// return () => clearInterval(x); -// }, []); - - return ( - <points - geometry={geometry} - position={offset} - > - <pointsMaterial size={0.01} vertexColors /> - </points> - ); -} - -export default PointCloud2; diff --git a/src/features/viewport/Robot.tsx b/src/features/viewport/Robot.tsx index 687b68ffbecaae91a0f4e53794479cb14cf49aee..31cb9a97d361708305c1f9af331885aaa32af5dd 100644 --- a/src/features/viewport/Robot.tsx +++ b/src/features/viewport/Robot.tsx @@ -1,37 +1,24 @@ -import { useLoader } from '@react-three/fiber'; import { PropsWithoutRef } from 'react'; import { Euler, Quaternion } from 'three'; -import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'; -import { RobotConfig } from '../config/config-slice'; -import { RosbridgeConnectionMap, useTopic } from '../connections/RosbridgeConnections'; +import { useTopic } from '../../common/store'; +import { Robot as RobotConfig } from '../../schemas/Robot.schema'; export interface RobotProps { - name: string; config: RobotConfig; - connections: RosbridgeConnectionMap; } -interface Pose { - position: {x: number, y: number, z: number }; - orientation: {w: number, x: number, y: number, z: number } -} +export default function Robot(props: PropsWithoutRef<RobotProps>) { + // const model = useLoader(OBJLoader, 'crawler/Altiscan2t.obj'); + const poseStamped = useTopic(props.config.rosbridge.uri, props.config.topics.pose, 'geometry_msgs/PoseStamped'); -interface PoseStamped { - pose: Pose; -} + // If we haven't received a pose yet: do not render the robot. + if (!poseStamped) { + return null; + } -export default function Robot(props: PropsWithoutRef<RobotProps>) { - const ros = props.connections[props.config.connection]; - const model = useLoader(OBJLoader, 'crawler/Altiscan2t.obj'); - const poseStamped = useTopic<PoseStamped>(ros, props.config.poseTopic || "", 'geometry_msgs/PoseStamped'); - const position: [number, number, number] = [-(poseStamped?.pose.position.x || 0), poseStamped?.pose.position.z || 0, poseStamped?.pose.position.y || 0]; - const orientation: [number, number, number, number] = [poseStamped?.pose.orientation.x || 0, poseStamped?.pose.orientation.y || 0, poseStamped?.pose.orientation.z || 0, poseStamped?.pose.orientation.w || 1]; - const sd: [number, number, number, number] = [ - -orientation[0], - orientation[2], - orientation[1], - orientation[3], - ]; + // TODO: refactor this! + const position: [number, number, number] = [-poseStamped.pose.position.x, poseStamped.pose.position.z, poseStamped.pose.position.y]; + const orientation: [number, number, number, number] = [-poseStamped.pose.orientation.x, poseStamped.pose.orientation.z, poseStamped.pose.orientation.y, poseStamped.pose.orientation.w]; const q = new Quaternion(); q.setFromEuler(new Euler(0, -Math.PI / 2, Math.PI), true); @@ -39,7 +26,7 @@ export default function Robot(props: PropsWithoutRef<RobotProps>) { return ( <group position={position} - quaternion={sd} + quaternion={orientation} scale={[1, 1, 1]} > <group diff --git a/src/features/viewport/Viewport.tsx b/src/features/viewport/Viewport.tsx index d793a47eb8ea649d28db72c123af82b05edb7ad6..624b389342f3f5dbe94218f07cdb8e34ad26e0c7 100644 --- a/src/features/viewport/Viewport.tsx +++ b/src/features/viewport/Viewport.tsx @@ -1,64 +1,31 @@ import { Canvas } from '@react-three/fiber'; -import { Controllers, Hands, XR } from '@react-three/xr'; -import SkyBox from './SkyBox'; -import Water from './Water'; -import { OrbitControls, FirstPersonControls } from '../camera/Controls'; -import Show from '../../common/Show'; -import { useContext, useState } from 'react'; -import { useAppSelector } from '../../app/hooks'; +import { useState } from 'react'; import For from '../../common/For'; +import { useStore } from '../../common/store'; import Robot from './Robot'; -import { RosbridgeConnectionsContext } from '../connections//RosbridgeConnections'; -import Mesh from './Mesh'; -import PointCloud2 from './PointCloud2'; const Viewport = () => { - const robots = useAppSelector(state => state.config.robots); - const cameraControls = useAppSelector(state => state.camera.controls); + // const robots = useAppSelector(state => state.config.robots); + const robots = useStore(state => state.robots); + // const cameraControls = useAppSelector(state => state.camera.controls); const [isInXr, setIsInXr] = useState(false); - const connections = useContext(RosbridgeConnectionsContext); return ( - <> - <Canvas - tabIndex={0} - style={{ - }} - > - <Show when={!isInXr}> - <Show when={cameraControls === 'orbit'}> - <OrbitControls /> - </Show> - <Show when={cameraControls === 'first-person'}> - <FirstPersonControls /> - </Show> - <></> - </Show> - <XR - onSessionStart={() => setIsInXr(true)} - onSessionEnd={() => setIsInXr(false)} - > - <Controllers /> - <Hands /> - <SkyBox baseURL="skyboxes/clouds" /> - <Water width={1000} height={1000} waterNormalsTexture="waternormals.jpg" /> - <ambientLight /> - <pointLight position={[10, 10, 10]} /> - <PointCloud2 ros={connections['New Connection']} topic="/crawler/leica_pts_parser/cloud" /> - <For each={Object.keys(robots)}> - { - robot => - <Robot - name={robot} - config={robots[robot]} - key={robot} - connections={connections} - /> - } - </For> - </XR> - </Canvas> - </> + <Canvas + tabIndex={0} + style={{ + }} + > + <For each={Object.values(robots)}> + { + robot => + <Robot + config={robot} + key={robot.name} + /> + } + </For> + </Canvas> ); } diff --git a/src/features/viewport/Viewport2D.tsx b/src/features/viewport/Viewport2D.tsx index 48bd5a90da7750af6f7e1e67af57dc2c135e4036..9579718b494197ed0cd9b36134a0d89dc04d462d 100644 --- a/src/features/viewport/Viewport2D.tsx +++ b/src/features/viewport/Viewport2D.tsx @@ -1,10 +1,8 @@ import { Canvas } from '@react-three/fiber'; -import { useContext, useEffect, useRef, useState } from 'react'; +import { useState } from 'react'; import { useAppSelector } from '../../app/hooks'; import For from '../../common/For'; import Robot from './Robot'; -import { RosbridgeConnectionsContext } from '../connections//RosbridgeConnections'; -import Mesh from './Mesh'; import { OrthographicCamera } from '@react-three/drei'; import { Box } from '@mui/system'; import LoopIcon from '@mui/icons-material/Loop'; @@ -19,7 +17,6 @@ export type ShipSide = 'portside' | 'starboard'; const Viewport2D = () => { const robots = useAppSelector(state => state.config.robots); - const connections = useContext(RosbridgeConnectionsContext); const [side, setSide] = useState<ShipSide>('portside'); const [scale, setScale] = useState(1.0); const [rightButtonDown, setRightButtonDown] = useState(false); @@ -86,15 +83,13 @@ const Viewport2D = () => { far={10000} /> <ambientLight /> - <Mesh ros={connections['New Connection']} topic="/crawler/leica_pts_parser/mesh" /> - <For each={Object.keys(robots)}> + {/*<Mesh ros={connections['New Connection']} topic="/crawler/leica_pts_parser/mesh" />*/} + <For each={robots}> { robot => <Robot - name={robot} - config={robots[robot]} - key={robot} - connections={connections} + config={robot} + key={robot.name} /> } </For> diff --git a/src/index.tsx b/src/index.tsx index 5b024ad85166ef0b2927e4194763ada565566172..0cccf21cfcac137b4437b4731668075b2cdb0c5c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,15 +5,12 @@ import { BrowserRouter } from 'react-router-dom'; import { store } from './app/store'; import App from './App' import './index.css' -import RosbridgeConnections from './features/connections/RosbridgeConnections'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( <React.StrictMode> <Provider store={store}> <BrowserRouter> - <RosbridgeConnections> - <App /> - </RosbridgeConnections> + <App /> </BrowserRouter> </Provider> </React.StrictMode> diff --git a/yarn.lock b/yarn.lock index 8b22f344e0fbbda54b4b44eaf436ed7af0f8413f..1ac143e4132869571885545be8e82163d011b5c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1091,6 +1091,16 @@ "@babel/helper-validator-identifier" "^7.18.6" to-fast-properties "^2.0.0" +"@bcherny/json-schema-ref-parser@9.0.9": + version "9.0.9" + resolved "https://registry.yarnpkg.com/@bcherny/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#09899d405bc708c0acac0066ae8db5b94d465ca4" + integrity sha512-vmEmnJCfpkLdas++9OYg6riIezTYqTHpqUTODJzHLzs5UnXujbOJW9VwcVCnyo1mVRt32FRr23iXBx/sX8YbeQ== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1688,6 +1698,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -2302,6 +2317,14 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/glob@^7.1.3": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -2369,7 +2392,7 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.11", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== @@ -2379,11 +2402,21 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash@^4.14.182": + version "4.14.189" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.189.tgz#975ff8c38da5ae58b751127b19ad5e44b5b7f6d2" + integrity sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA== + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== +"@types/minimatch@*": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + "@types/node@*", "@types/node@>=10.0.0": version "18.7.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154" @@ -2409,6 +2442,11 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.0.tgz#ea03e9f0376a4446f44797ca19d9c46c36e352dc" integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A== +"@types/prettier@^2.6.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e" + integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow== + "@types/prop-types@*", "@types/prop-types@^15.7.5": version "15.7.5" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" @@ -2842,6 +2880,11 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -2992,6 +3035,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -3429,6 +3477,11 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -3534,7 +3587,7 @@ chevrotain@^10.1.2: lodash "4.17.21" regexp-to-ast "0.5.0" -chokidar@^3.4.2, chokidar@^3.5.3: +chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -3571,6 +3624,17 @@ clean-css@^5.2.2: dependencies: source-map "~0.6.0" +cli-color@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-2.0.3.tgz#73769ba969080629670f3f2ef69a4bf4e7cc1879" + integrity sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ== + dependencies: + d "^1.0.1" + es5-ext "^0.10.61" + es6-iterator "^2.0.3" + memoizee "^0.4.15" + timers-ext "^0.1.7" + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -3580,6 +3644,15 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clsx@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" @@ -3700,6 +3773,21 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concurrently@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-7.5.0.tgz#4dd432d4634a8251f27ab000c4974e78e3906bd3" + integrity sha512-5E3mwiS+i2JYBzr5BpXkFxOnleZTMsG+WnE/dCG4/P+oiVXrbmrBwJ2ozn4SxwB2EZDrKR568X+puVohxz3/Mg== + dependencies: + chalk "^4.1.0" + date-fns "^2.29.1" + lodash "^4.17.21" + rxjs "^7.0.0" + shell-quote "^1.7.3" + spawn-command "^0.0.2-1" + supports-color "^8.1.0" + tree-kill "^1.2.2" + yargs "^17.3.1" + confusing-browser-globals@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" @@ -4005,6 +4093,14 @@ csstype@^3.0.2, csstype@^3.1.0: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -4019,6 +4115,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +date-fns@^2.29.1: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + debounce@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" @@ -4453,6 +4554,42 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@^0.10.61, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.62" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" + integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + +es6-weak-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -4752,6 +4889,14 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + eventemitter2@^6.4.0: version "6.4.9" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" @@ -4845,6 +4990,13 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +ext@^1.1.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -5135,6 +5287,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-stdin@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" + integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -5162,6 +5319,13 @@ glob-parent@^6.0.1, glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-promise@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-4.2.2.tgz#15f44bcba0e14219cd93af36da6bb905ff007877" + integrity sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw== + dependencies: + "@types/glob" "^7.1.3" + glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" @@ -5474,6 +5638,11 @@ identity-obj-proxy@^3.0.0: dependencies: harmony-reflect "^1.4.6" +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -5662,6 +5831,11 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-promise@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -6394,6 +6568,26 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-to-typescript@^11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/json-schema-to-typescript/-/json-schema-to-typescript-11.0.2.tgz#80348391abb4ffb75daf312380c2f01c552ffba8" + integrity sha512-XRyeXBJeo/IH4eTP5D1ptX78vCvH86nMDt2k3AxO28C3uYWEDmy4mgPyMpb8bLJ/pJMElOGuQbnKR5Y6NSh3QQ== + dependencies: + "@bcherny/json-schema-ref-parser" "9.0.9" + "@types/json-schema" "^7.0.11" + "@types/lodash" "^4.14.182" + "@types/prettier" "^2.6.1" + cli-color "^2.0.2" + get-stdin "^8.0.0" + glob "^7.1.6" + glob-promise "^4.2.2" + is-glob "^4.0.3" + lodash "^4.17.21" + minimist "^1.2.6" + mkdirp "^1.0.4" + mz "^2.7.0" + prettier "^2.6.2" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -6440,6 +6634,11 @@ jsonpointer@^5.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== +jsonschema@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.1.tgz#cc4c3f0077fb4542982973d8a083b6b34f482dab" + integrity sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ== + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: version "3.3.3" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" @@ -6623,6 +6822,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ== + dependencies: + es5-ext "~0.10.2" + lz-string@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -6671,6 +6877,20 @@ memfs@^3.1.2, memfs@^3.4.3: dependencies: fs-monkey "^1.0.3" +memoizee@^0.4.15: + version "0.4.15" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72" + integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ== + dependencies: + d "^1.0.1" + es5-ext "^0.10.53" + es6-weak-map "^2.0.3" + event-emitter "^0.3.5" + is-promise "^2.2.2" + lru-queue "^0.1.0" + next-tick "^1.1.0" + timers-ext "^0.1.7" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -6769,6 +6989,11 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mkdirp@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" @@ -6804,6 +7029,15 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" @@ -6824,6 +7058,11 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +next-tick@1, next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -6847,6 +7086,29 @@ node-releases@^2.0.6: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== +nodemon@^2.0.7: + version "2.0.20" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.20.tgz#e3537de768a492e8d74da5c5813cb0c7486fc701" + integrity sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw== + dependencies: + chokidar "^3.5.2" + debug "^3.2.7" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^5.7.1" + simple-update-notifier "^1.0.7" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== + dependencies: + abbrev "1" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -6869,6 +7131,14 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npm-watch@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/npm-watch/-/npm-watch-0.11.0.tgz#d052d9832ad2923dcf937a35aff0c2db678a8a2e" + integrity sha512-wAOd0moNX2kSA2FNvt8+7ORwYaJpQ1ZoWjUYdb1bBCxq4nkWuU0IiJa9VpVxrj5Ks+FGXQd62OC/Bjk0aSr+dg== + dependencies: + nodemon "^2.0.7" + through2 "^4.0.2" + nth-check@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -6888,7 +7158,7 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== -object-assign@^4, object-assign@^4.0.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -7777,6 +8047,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== +prettier@^2.6.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" @@ -7860,6 +8135,16 @@ psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== + punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -7877,6 +8162,11 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -8178,6 +8468,15 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" +readable-stream@3, readable-stream@^3.0.6: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@^2.0.1: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -8191,15 +8490,6 @@ readable-stream@^2.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -8464,6 +8754,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs@^7.0.0: + version "7.5.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" + integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== + dependencies: + tslib "^2.1.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -8567,6 +8864,11 @@ selfsigned@^2.0.1: dependencies: node-forge "^1" +semver@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -8579,6 +8881,11 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.3.7: dependencies: lru-cache "^6.0.0" +semver@~7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -8681,6 +8988,13 @@ signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-update-notifier@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz#7edf75c5bdd04f88828d632f762b2bc32996a9cc" + integrity sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew== + dependencies: + semver "~7.0.0" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -8784,6 +9098,11 @@ sourcemap-codec@^1.4.8: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +spawn-command@^0.0.2-1: + version "0.0.2-1" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" + integrity sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg== + spdy-transport@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" @@ -8865,7 +9184,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -string-width@^4.1.0, string-width@^4.2.0: +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8998,7 +9317,7 @@ stylis@4.0.13: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -9012,7 +9331,7 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: +supports-color@^8.0.0, supports-color@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -9175,6 +9494,20 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + three-mesh-bvh@^0.5.15: version "0.5.17" resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.5.17.tgz#1f9bdcd7992e86dc3665664c1fe5cedd7be4bd9f" @@ -9224,11 +9557,26 @@ throat@^6.0.1: resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== +through2@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== + dependencies: + readable-stream "3" + thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +timers-ext@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" + integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== + dependencies: + es5-ext "~0.10.46" + next-tick "1" + tiny-inflate@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" @@ -9256,6 +9604,13 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tough-cookie@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" @@ -9280,6 +9635,11 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + troika-three-text@^0.46.4: version "0.46.4" resolved "https://registry.yarnpkg.com/troika-three-text/-/troika-three-text-0.46.4.tgz#77627ac2ac4765d5248c857a8b42f82c25f2d034" @@ -9325,6 +9685,11 @@ tslib@^2.0.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -9374,6 +9739,16 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -9396,6 +9771,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -9474,7 +9854,15 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" -use-sync-external-store@^1.0.0: +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ== + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -10038,6 +10426,11 @@ yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" @@ -10051,6 +10444,19 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.3.1: + version "17.6.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" + integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" @@ -10065,3 +10471,10 @@ zustand@^3.5.13, zustand@^3.7.1: version "3.7.2" resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d" integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA== + +zustand@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.1.4.tgz#b0286da4cc9edd35e91c96414fa54bfa4652a54d" + integrity sha512-k2jVOlWo8p4R83mQ+/uyB8ILPO2PCJOf+QVjcL+1PbMCk1w5OoPYpAIxy9zd93FSfmJqoH6lGdwzzjwqJIRU5A== + dependencies: + use-sync-external-store "1.2.0"