diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml index 53e8dd6af..ad17be889 100644 --- a/.github/workflows/dashboard.yml +++ b/.github/workflows/dashboard.yml @@ -31,6 +31,8 @@ jobs: - name: setup python run: apt update && apt install -y python3-venv python-is-python3 - name: bootstrap + env: + NODE_OPTIONS: '--max_old_space_size=4096' uses: ./.github/actions/bootstrap with: package: rmf-dashboard diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 7a7ece5bb..edf033597 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -48,6 +48,8 @@ "@mui/styles": "^5.8.3", "@mui/system": "^5.8.3", "@mui/x-date-pickers": "^5.0.20", + "@react-three/drei": "^9.84.0", + "@react-three/fiber": "^8.14.2", "@types/debug": "^4.1.5", "@types/leaflet": "^1.5.17", "@types/react": "^18.2.14", @@ -56,6 +58,7 @@ "@types/react-leaflet": "^2.5.2", "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", + "@types/three": "^0.156.0", "ajv": "^8.10.0", "api-client": "workspace:*", "axios": "^0.21.1", @@ -75,7 +78,8 @@ "react-router-dom": "^6.14.1", "rmf-auth": "workspace:*", "rmf-models": "workspace:*", - "rxjs": "^7.5.5" + "rxjs": "^7.5.5", + "three": "^0.156.1" }, "devDependencies": { "@babel/core": "^7.18.6", diff --git a/packages/dashboard/src/components/app-events.ts b/packages/dashboard/src/components/app-events.ts index 21c1e7d02..18add3850 100644 --- a/packages/dashboard/src/components/app-events.ts +++ b/packages/dashboard/src/components/app-events.ts @@ -3,6 +3,7 @@ import { Dispenser, Door, Ingestor, + Level, Lift, TaskState, } from 'api-client'; @@ -21,4 +22,5 @@ export const AppEvents = { disabledLayers: new ReplaySubject>(), zoom: new BehaviorSubject(null), mapCenter: new BehaviorSubject<[number, number]>([0, 0]), + levelSelect: new BehaviorSubject(null), }; diff --git a/packages/dashboard/src/components/map-app.tsx b/packages/dashboard/src/components/map-app.tsx index a5b832103..ee8eebc09 100644 --- a/packages/dashboard/src/components/map-app.tsx +++ b/packages/dashboard/src/components/map-app.tsx @@ -7,32 +7,33 @@ import { Level, } from 'api-client'; import Debug from 'debug'; -import React from 'react'; +import React, { ChangeEvent, Suspense } from 'react'; import { - affineImageBounds, ColorManager, + findSceneBoundingBoxFromThreeFiber, fromRmfCoords, getPlaces, - LMap, - loadAffineImage, Place, + ReactThreeFiberImageMaker, RobotTableData, - TrajectoryTimeControl, + ShapeThreeRendering, + TextThreeRendering, } from 'react-components'; -import { AttributionControl, ImageOverlay, LayersControl, Pane, Viewport } from 'react-leaflet'; import { EMPTY, merge, scan, Subscription, switchMap } from 'rxjs'; import appConfig from '../app-config'; import { ResourcesContext } from './app-contexts'; import { AppEvents } from './app-events'; -import { DoorsOverlay } from './doors-overlay'; -import { LiftsOverlay } from './lifts-overlay'; import { createMicroApp } from './micro-app'; import { RmfAppContext } from './rmf-app'; -import { RobotData, RobotsOverlay } from './robots-overlay'; -import { TrajectoriesOverlay, TrajectoryData } from './trajectories-overlay'; -import { WaypointsOverlay } from './waypoints-overlay'; -import { WorkcellData, WorkcellsOverlay } from './workcells-overlay'; +import { RobotData } from './robots-overlay'; +import { TrajectoryData } from './trajectories-overlay'; +import { WorkcellData } from './workcells-overlay'; import { RobotSummary } from './robots/robot-summary'; +import { Box3, TextureLoader, Vector3 } from 'three'; +import { Canvas, useLoader } from '@react-three/fiber'; +import { Line } from '@react-three/drei'; +import { CameraControl, LayersController, updateZoom } from './three-fiber'; +import { Lifts, Door, RobotThree } from './three-fiber'; type FleetState = ApiServerModelsRmfApiFleetStateFleetState; @@ -41,25 +42,32 @@ const debug = Debug('MapApp'); const TrajectoryUpdateInterval = 2000; // schedule visualizer manages it's own settings so that it doesn't cause a re-render // of the whole app when it changes. -const SettingsKey = 'mapAppSettings'; const colorManager = new ColorManager(); -const DEFAULT_ZOOM_LEVEL = 5; +const DEFAULT_ZOOM_LEVEL = 20; function getRobotId(fleetName: string, robotName: string): string { return `${fleetName}/${robotName}`; } -interface MapSettings { - trajectoryTime: number; -} - export const MapApp = styled( createMicroApp('Map', () => { const rmf = React.useContext(RmfAppContext); const resourceManager = React.useContext(ResourcesContext); const [currentLevel, setCurrentLevel] = React.useState(undefined); - const [disabledLayers, setDisabledLayers] = React.useState>({}); + const [disabledLayers, setDisabledLayers] = React.useState>({ + Waypoints: false, + Dispensers: false, + Ingestors: false, + Lifts: false, + Doors: false, + Trajectories: false, + Robots: false, + Labels: false, + 'Waypoint labels': false, + 'Pickup point labels': false, + 'Dropoff point labels': false, + }); const [openRobotSummary, setOpenRobotSummary] = React.useState(false); const [selectedRobot, setSelectedRobot] = React.useState(); @@ -130,11 +138,7 @@ export const MapApp = styled( const [waypoints, setWaypoints] = React.useState([]); const [trajectories, setTrajectories] = React.useState([]); - const [mapSettings, setMapSettings] = React.useState(() => { - const settings = window.localStorage.getItem(SettingsKey); - return settings ? JSON.parse(settings) : { trajectoryTime: 300000 /* 5 min */ }; - }); - const trajectoryTime = mapSettings.trajectoryTime; + const trajectoryTime = 300000; const trajectoryAnimScale = trajectoryTime / (0.9 * TrajectoryUpdateInterval); const trajManager = rmf?.trajectoryManager; React.useEffect(() => { @@ -193,8 +197,10 @@ export const MapApp = styled( subs.push( rmf.buildingMapObs.subscribe((newMap) => { setBuildingMap(newMap); - const currentLevel = newMap.levels[0]; - setCurrentLevel(currentLevel); + const currentLevel = AppEvents.levelSelect.value + ? AppEvents.levelSelect.value + : newMap.levels[0]; + AppEvents.levelSelect.next(currentLevel); setWaypoints( getPlaces(newMap).filter( (p) => p.level === currentLevel.name && p.vertex.name.length > 0, @@ -214,8 +220,6 @@ export const MapApp = styled( }, [rmf]); const [imageUrl, setImageUrl] = React.useState(null); - const [bounds, setBounds] = React.useState(null); - const [center, setCenter] = React.useState([0, 0]); const [zoom, setZoom] = React.useState(DEFAULT_ZOOM_LEVEL); React.useEffect(() => { @@ -226,19 +230,8 @@ export const MapApp = styled( }, []); React.useEffect(() => { - const sub = AppEvents.mapCenter.subscribe((currentValue) => { - setCenter((prev) => { - const newCenter: L.LatLngTuple = [...currentValue]; - // react-leaftlet does not properly update state when the previous LatLng is the same, - // even when a new array is passed. - if (prev[0] === newCenter[0]) { - newCenter[0] += 0.00001; - } - if (prev[1] === newCenter[1]) { - newCenter[1] += 0.00001; - } - return newCenter; - }); + const sub = AppEvents.levelSelect.subscribe((currentValue) => { + setCurrentLevel(currentValue ?? undefined); }); return () => sub.unsubscribe(); }, []); @@ -250,14 +243,8 @@ export const MapApp = styled( } (async () => { - const affineImage = await loadAffineImage(currentLevel.images[0]); - const bounds = affineImageBounds( - currentLevel.images[0], - affineImage.naturalWidth, - affineImage.naturalHeight, - ); - setBounds(bounds); - setImageUrl(affineImage.src); + useLoader.preload(TextureLoader, currentLevel.images[0].data); + setImageUrl(currentLevel.images[0].data); })(); buildingMap && @@ -308,7 +295,7 @@ export const MapApp = styled( })(); }, [fleets, robotsStore, resourceManager, currentLevel]); - const { current: robotLocations } = React.useRef>({}); + const { current: robotLocations } = React.useRef>({}); // updates the robot location React.useEffect(() => { if (!rmf) { @@ -332,7 +319,11 @@ export const MapApp = styled( console.warn(`Map: Fail to update robot location for ${robotId} (missing location)`); return; } - robotLocations[robotId] = [robotState.location.x, robotState.location.y]; + robotLocations[robotId] = [ + robotState.location.x, + robotState.location.y, + robotState.location.yaw, + ]; }); }); return () => sub.unsubscribe(); @@ -361,155 +352,198 @@ export const MapApp = styled( console.warn(`Map: Failed to zoom to robot ${robotId} (robot location was not found)`); return; } - if (!bounds) { - console.warn( - `Map: Fail to zoom to robot ${robotId} (missing bounds, map was not loaded?)`, - ); - return; - } - const mapCoords = fromRmfCoords(robotLocation); + + const mapCoordsLocation: [number, number] = [robotLocation[0], robotLocation[1]]; + const mapCoords = fromRmfCoords(mapCoordsLocation); const newCenter: L.LatLngTuple = [mapCoords[1], mapCoords[0]]; AppEvents.mapCenter.next(newCenter); AppEvents.zoom.next(6); }); return () => sub.unsubscribe(); - }, [robotLocations, bounds]); + }, [robotLocations]); - const onViewportChanged = (viewport: Viewport) => { - if (viewport.zoom && viewport.center) { - AppEvents.zoom.next(viewport.zoom); - AppEvents.mapCenter.next(viewport.center); - } - }; + const ready = buildingMap && currentLevel; - const registeredLayersHandlers = React.useRef(false); - const ready = buildingMap && currentLevel && bounds; - return ready ? ( - { - if (registeredLayersHandlers.current || !cur) return; - cur.leafletElement.on('overlayadd', (ev: L.LayersControlEvent) => { - AppEvents.disabledLayers.next({ [ev.name]: false }); - }); - cur.leafletElement.on('overlayremove', (ev: L.LayersControlEvent) => { - AppEvents.disabledLayers.next({ [ev.name]: true }); - }); - registeredLayersHandlers.current = true; - }} - attributionControl={false} - zoomDelta={0.5} - zoomSnap={0.5} - center={center} - onViewportChanged={onViewportChanged} - zoom={zoom} - bounds={bounds} - maxBounds={bounds} - onbaselayerchange={({ name }: L.LayersControlEvent) => { - setCurrentLevel( - buildingMap.levels.find((l: Level) => l.name === name) || buildingMap.levels[0], - ); - }} - > - - - - {buildingMap.levels.map((level: Level) => - currentLevel.name === level.name ? ( - - {currentLevel.images.length > 0 && imageUrl && ( - - )} - - ) : ( - - {currentLevel.images.length > 0 && imageUrl && ( - - )} - - ), - )} + const [sceneBoundingBox, setSceneBoundingBox] = React.useState(undefined); + const [distance, setDistance] = React.useState(0); - - - - - - - AppEvents.dispenserSelect.next({ guid: workcell }) - } - /> - - - - AppEvents.ingestorSelect.next({ guid: workcell })} - /> - - - - AppEvents.liftSelect.next(lift)} - /> - - - - AppEvents.doorSelect.next(door)} - /> - + React.useMemo(() => { + setSceneBoundingBox(findSceneBoundingBoxFromThreeFiber(currentLevel)); + }, [currentLevel]); + + React.useEffect(() => { + if (!sceneBoundingBox) { + return; + } - - - + const size = sceneBoundingBox.getSize(new Vector3()); + setDistance(Math.max(size.x, size.y, size.z) * 0.7); + }, [sceneBoundingBox]); - - + , value: string) => { + AppEvents.levelSelect.next( + buildingMap.levels.find((l: Level) => l.name === value) || buildingMap.levels[0], + ); + }} + handleZoomIn={() => AppEvents.zoom.next(updateZoom(zoom, 2))} + handleZoomOut={() => AppEvents.zoom.next(updateZoom(zoom, -2))} + /> + { + if (!sceneBoundingBox) { + return; + } + const center = sceneBoundingBox.getCenter(new Vector3()); + camera.position.set(center.x, center.y, center.z + distance); + camera.zoom = zoom; + camera.updateProjectionMatrix(); + }} + orthographic={true} + > + + {currentLevel.doors.length > 0 + ? currentLevel.doors.map((door, i) => ( + + {!disabledLayers['Labels'] && ( + + )} + {!disabledLayers['Doors'] && ( + + )} + + )) + : null} + {currentLevel.images.length > 0 && imageUrl && ( + + )} + {buildingMap.lifts.length > 0 + ? buildingMap.lifts.map((lift, i) => + lift.doors.map((door, i) => ( + + {!disabledLayers['Labels'] && ( + + )} + {!disabledLayers['Doors'] && ( + + )} + + )), + ) + : null} + + {!disabledLayers['Lifts'] && buildingMap.lifts.length > 0 + ? buildingMap.lifts.map((lift) => + lift.doors.map(() => ( + + )), + ) + : null} + + {!disabledLayers['Waypoints'] && + waypoints.map((place, index) => ( + + ))} + + {!disabledLayers['Waypoint labels'] && + waypoints + .filter((waypoint) => !waypoint.pickupHandler && !waypoint.dropoffHandler) + .map((place, index) => ( + + ))} + {!disabledLayers['Pickup point labels'] && + waypoints + .filter((waypoint) => waypoint.pickupHandler) + .map((place, index) => ( + + ))} + {!disabledLayers['Dropoff point labels'] && + waypoints + .filter((waypoint) => waypoint.dropoffHandler) + .map((place, index) => ( + + ))} + {!disabledLayers['Ingestors'] && + ingestorsData.map((ingestor, index) => ( + + ))} + + {!disabledLayers['Dispensers'] && + dispensersData.map((dispenser, index) => ( + + ))} + {!disabledLayers['Trajectories'] && + trajectories.map((trajData) => ( + new Vector3(seg.x[0], seg.x[1], 4), + )} + color={trajData.color} + linewidth={5} + /> + ))} + {!disabledLayers['Robots'] && ( + { setOpenRobotSummary(true); setSelectedRobot(robot); }} /> - - + )} + + {openRobotSummary && selectedRobot && ( setOpenRobotSummary(false)} /> )} - - - setMapSettings((prev) => { - const newSettings = { ...prev, trajectoryTime: newValue }; - window.localStorage.setItem(SettingsKey, JSON.stringify(newSettings)); - return newSettings; - }) - } - /> - + ) : null; }), )({ diff --git a/packages/dashboard/src/components/robots/robot-info-app.tsx b/packages/dashboard/src/components/robots/robot-info-app.tsx index 6d0846874..2bfbc3a51 100644 --- a/packages/dashboard/src/components/robots/robot-info-app.tsx +++ b/packages/dashboard/src/components/robots/robot-info-app.tsx @@ -71,6 +71,7 @@ export const RobotInfoApp = createMicroApp('Robot Info', () => { /> ) : ( diff --git a/packages/dashboard/src/components/robots/robot-summary.tsx b/packages/dashboard/src/components/robots/robot-summary.tsx index 966328523..019bab9fd 100644 --- a/packages/dashboard/src/components/robots/robot-summary.tsx +++ b/packages/dashboard/src/components/robots/robot-summary.tsx @@ -63,11 +63,11 @@ const setTaskDialogColor = (robotStatus: Status2 | undefined) => { const LinearProgressWithLabel = (props: LinearProgressProps & { value: number }) => { return ( - - + + - + {`${Math.round( props.value, )}%`} @@ -273,7 +273,7 @@ export const RobotSummary = React.memo(({ onClose, robot }: RobotSummaryProps) = Task progress - + diff --git a/packages/dashboard/src/components/tasks/task-inspector.tsx b/packages/dashboard/src/components/tasks/task-inspector.tsx index 6e0d9ccc2..2eb114a44 100644 --- a/packages/dashboard/src/components/tasks/task-inspector.tsx +++ b/packages/dashboard/src/components/tasks/task-inspector.tsx @@ -109,7 +109,7 @@ export function TaskInspector({ task, onClose }: TableDataGridState): JSX.Elemen - + {taskState ? ( diff --git a/packages/dashboard/src/components/tasks/task-logs.tsx b/packages/dashboard/src/components/tasks/task-logs.tsx index 03dd01723..f8b4870af 100644 --- a/packages/dashboard/src/components/tasks/task-logs.tsx +++ b/packages/dashboard/src/components/tasks/task-logs.tsx @@ -49,7 +49,7 @@ export function TaskLogs({ taskLog, taskState, title }: TaskLogProps) { } return ( - + {taskState && (title ? title : taskState.booking.id)} diff --git a/packages/dashboard/src/components/tasks/task-summary.tsx b/packages/dashboard/src/components/tasks/task-summary.tsx index 5d06fd9de..3371012a2 100644 --- a/packages/dashboard/src/components/tasks/task-summary.tsx +++ b/packages/dashboard/src/components/tasks/task-summary.tsx @@ -32,11 +32,11 @@ const useStyles = makeStyles((theme: Theme) => const LinearProgressWithLabel = (props: LinearProgressProps & { value: number }) => { return ( - - + + - + {`${Math.round( props.value, )}%`} @@ -181,7 +181,7 @@ export const TaskSummary = React.memo((props: TaskSummaryProps) => { Task State {taskProgress && ( - + )} diff --git a/packages/dashboard/src/components/tasks/tasks-app.tsx b/packages/dashboard/src/components/tasks/tasks-app.tsx index 9b6ccc81e..3327620aa 100644 --- a/packages/dashboard/src/components/tasks/tasks-app.tsx +++ b/packages/dashboard/src/components/tasks/tasks-app.tsx @@ -60,7 +60,11 @@ function TabPanel(props: TabPanelProps) { aria-labelledby={tabId(index)} {...other} > - {selectedTabIndex === index && {children}} + {selectedTabIndex === index && ( + + {children} + + )} ); } diff --git a/packages/dashboard/src/components/three-fiber/camera-control.tsx b/packages/dashboard/src/components/three-fiber/camera-control.tsx new file mode 100644 index 000000000..ebd4ea3ea --- /dev/null +++ b/packages/dashboard/src/components/three-fiber/camera-control.tsx @@ -0,0 +1,63 @@ +import { useFrame, useThree } from '@react-three/fiber'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; +import React, { useEffect, useRef } from 'react'; +import { MOUSE, Vector3 } from 'three'; + +interface CameraControlProps { + zoom: number; +} + +export const updateZoom = (currentZoom: number, increment: number) => + Math.max(0, Math.min(Infinity, currentZoom + increment)); + +export const CameraControl: React.FC = ({ zoom }) => { + const { camera, gl } = useThree(); + const controlsRef = useRef(null); + + useEffect(() => { + const controls = new OrbitControls(camera, gl.domElement); + controls.target = new Vector3(0, 0, -1000); + controls.enableRotate = false; + controls.enableDamping = false; + controls.enableZoom = true; + controls.mouseButtons = { + LEFT: MOUSE.PAN, + MIDDLE: undefined, + RIGHT: undefined, + }; + + controlsRef.current = controls; + + // [CR]: Remove the icons for now. Code is left for future implementation. + + // camera.zoom = zoom; + // camera.updateProjectionMatrix(); + + // const handleWheel = (event: WheelEvent) => { + // const delta = event.deltaY; + // const sensitivity = 0.1; + + // setInternalZoom((prevZoom) => { + // const newZoom = prevZoom - delta * sensitivity; + // return Math.max(controls.minZoom, Math.min(controls.maxZoom, newZoom)); + // }); + + // updateExternalStateDebounced(internalZoom); + // }; + + // gl.domElement.addEventListener('wheel', handleWheel); + + return () => { + controls.dispose(); + // gl.domElement.removeEventListener('wheel', handleWheel); + }; + }, [camera, gl.domElement, zoom]); + + useFrame(() => { + if (controlsRef.current) { + controlsRef.current.update(); + } + }); + + return null; +}; diff --git a/packages/dashboard/src/components/three-fiber/door-three.tsx b/packages/dashboard/src/components/three-fiber/door-three.tsx new file mode 100644 index 000000000..1d2829797 --- /dev/null +++ b/packages/dashboard/src/components/three-fiber/door-three.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { DoorState, Lift, LiftState } from 'api-client'; +import { Door as DoorModel } from 'rmf-models'; +import { RmfAppContext } from '../rmf-app'; +import { DoorMode } from 'rmf-models'; +import { DoorThreeMaker } from 'react-components'; + +interface DoorProps { + door: DoorModel; + opacity: number; + height: number; + elevation: number; + lift?: Lift; +} + +function toDoorMode(liftState: LiftState): DoorMode { + return { value: liftState.door_state }; +} + +export const Door = React.memo(({ ...doorProps }: DoorProps): JSX.Element => { + const ref = React.useRef(null!); + const { door, lift } = doorProps; + const rmf = React.useContext(RmfAppContext); + const [doorState, setDoorState] = React.useState(null); + const [liftState, setLiftState] = React.useState(undefined); + const [color, setColor] = React.useState('red'); + + React.useEffect(() => { + if (!rmf) { + return; + } + if (!lift) { + return; + } + + const sub = rmf.getLiftStateObs(lift.name).subscribe(setLiftState); + return () => sub.unsubscribe(); + }, [rmf, lift]); + + React.useEffect(() => { + let doorStateValue = doorState?.current_mode.value; + if (liftState) { + doorStateValue = toDoorMode(liftState).value; + } + switch (doorStateValue) { + case DoorMode.MODE_CLOSED: + setColor('red'); + return; + case DoorMode.MODE_MOVING: + setColor('orange'); + return; + case DoorMode.MODE_OPEN: + default: + setColor('green'); + return; + } + }, [doorState?.current_mode.value, liftState]); + + React.useEffect(() => { + if (!rmf) { + return; + } + const sub = rmf.getDoorStateObs(door.name).subscribe(setDoorState); + return () => sub.unsubscribe(); + }, [rmf, door.name]); + + return ; +}); diff --git a/packages/dashboard/src/components/three-fiber/index.tsx b/packages/dashboard/src/components/three-fiber/index.tsx new file mode 100644 index 000000000..ef68a5344 --- /dev/null +++ b/packages/dashboard/src/components/three-fiber/index.tsx @@ -0,0 +1,5 @@ +export * from './layers-controller'; +export * from './lift-three'; +export * from './door-three'; +export * from './robot-three'; +export * from './camera-control'; diff --git a/packages/dashboard/src/components/three-fiber/layers-controller.tsx b/packages/dashboard/src/components/three-fiber/layers-controller.tsx new file mode 100644 index 000000000..59cd7a127 --- /dev/null +++ b/packages/dashboard/src/components/three-fiber/layers-controller.tsx @@ -0,0 +1,107 @@ +import { ChangeEvent } from 'react'; +import { Level } from 'api-client'; +import { AppEvents } from '../app-events'; +import React from 'react'; +import { + Box, + Checkbox, + FormControl, + FormControlLabel, + FormGroup, + MenuItem, + TextField, +} from '@mui/material'; +import LayersIcon from '@mui/icons-material/Layers'; + +interface LayersControllerProps { + disabledLayers: Record; + onChange: (event: ChangeEvent, value: string) => void; + levels: Level[]; + currentLevel: Level; + handleZoomIn: () => void; + handleZoomOut: () => void; +} + +export const LayersController = ({ + disabledLayers, + onChange, + levels, + currentLevel, + handleZoomIn, + handleZoomOut, +}: LayersControllerProps) => { + const [isHovered, setIsHovered] = React.useState(false); + + return ( + + {/* Remove the icons for now. Code is left for future implementation. */} + {/*
+ +
+
+ +
*/} + + ) => onChange(e, e.target.value as string)} + > + {levels.map((level, i) => ( + + {level.name} + + ))} + + +
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> + + {isHovered && ( +
+ {Object.keys(disabledLayers).map((layerName) => ( + + { + const updatedLayers = { ...disabledLayers }; + updatedLayers[layerName] = !updatedLayers[layerName]; + AppEvents.disabledLayers.next(updatedLayers); + }} + /> + } + label={layerName} + /> + + ))} +
+ )} +
+
+ ); +}; diff --git a/packages/dashboard/src/components/three-fiber/lift-three.tsx b/packages/dashboard/src/components/three-fiber/lift-three.tsx new file mode 100644 index 000000000..0bb266d73 --- /dev/null +++ b/packages/dashboard/src/components/three-fiber/lift-three.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Lift, LiftState } from 'api-client'; +import { RmfAppContext } from '../rmf-app'; +import { LiftThreeMaker } from 'react-components'; + +interface LiftsProps { + opacity: number; + height: number; + elevation: number; + lift?: Lift; +} + +export const Lifts = React.memo(({ lift }: LiftsProps): JSX.Element => { + const rmf = React.useContext(RmfAppContext); + const [liftState, setLiftState] = React.useState(undefined); + + React.useEffect(() => { + if (!rmf) { + return; + } + if (!lift) { + return; + } + + const sub = rmf.getLiftStateObs(lift.name).subscribe(setLiftState); + return () => sub.unsubscribe(); + }, [rmf, lift]); + + return ( + <> + {lift && liftState && ( + + )} + + ); +}); diff --git a/packages/dashboard/src/components/three-fiber/robot-three.tsx b/packages/dashboard/src/components/three-fiber/robot-three.tsx new file mode 100644 index 000000000..95edd62d5 --- /dev/null +++ b/packages/dashboard/src/components/three-fiber/robot-three.tsx @@ -0,0 +1,39 @@ +import { ThreeEvent } from '@react-three/fiber'; +import React from 'react'; +import { RobotThreeMaker } from 'react-components'; +import { Euler, Vector3 } from 'three'; +import { RobotData } from '../robots-overlay'; + +interface RobotThreeProps { + robots: RobotData[]; + robotLocations: Record; + onRobotClick?: (ev: ThreeEvent, robot: RobotData) => void; +} + +export const RobotThree = ({ robots, robotLocations, onRobotClick }: RobotThreeProps) => { + const STANDAR_Z_POSITION = 4; + const CIRCLE_SEGMENT = 64; + + return ( + <> + {robots.map((robot) => { + const robotId = `${robot.fleet}/${robot.name}`; + const robotLocation = robotLocations[robotId]; + const rotationZ = robotLocation[2] + Math.PI / 2; + + const position = new Vector3(robotLocation[0], robotLocation[1], STANDAR_Z_POSITION); + return ( + + + + ); + })} + + ); +}; diff --git a/packages/dashboard/src/components/workspace.tsx b/packages/dashboard/src/components/workspace.tsx index 10de7a8ae..1d835c235 100644 --- a/packages/dashboard/src/components/workspace.tsx +++ b/packages/dashboard/src/components/workspace.tsx @@ -123,7 +123,7 @@ export function ManagedWorkspace({ workspaceId }: ManagedWorkspaceProps) { }, [appController, designMode, theme]); return ( - + { @@ -134,6 +134,7 @@ export function ManagedWorkspace({ workspaceId }: ManagedWorkspaceProps) { /> {workspaceState.windows.length === 0 && ( { const LinearProgressWithLabel = (props: LinearProgressProps & { value: number }) => { return ( - - + + - + {`${Math.round( props.value * 100, )}%`} @@ -122,7 +122,7 @@ export const AlertDialog = React.memo((props: DialogAlertProps) => { Task progress - + diff --git a/packages/react-components/lib/beacons/beacon-table-datagrid.tsx b/packages/react-components/lib/beacons/beacon-table-datagrid.tsx index aea80ccf3..5e0a790fa 100644 --- a/packages/react-components/lib/beacons/beacon-table-datagrid.tsx +++ b/packages/react-components/lib/beacons/beacon-table-datagrid.tsx @@ -23,7 +23,7 @@ export function BeaconDataGridTable({ beacons }: BeaconDataGridTableProps): JSX. })(); return ( - + + + + { return ( - +