FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
ScenarioView.tsx
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
3
4import React, { useRef, useState } from 'react';
5import { Box, IconButton, Tooltip } from '@mui/material';
6import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
7import { Canvas } from '@react-three/fiber';
8import {
9 Panel,
10 PanelGroup,
11 PanelResizeHandle,
12 type ImperativePanelGroupHandle,
13} from 'react-resizable-panels';
14import WorldView from '@/components/WorldView';
15import SceneTree from '@/components/SceneTree';
16import PropertyInspector from '@/components/PropertyInspector';
17import Timeline from '@/components/Timeline';
18import ViewControls from '@/components/ViewControls';
19import { type MapControls as MapControlsImpl } from 'three-stdlib';
20import { ScaleManager } from '@/components/ScaleManager';
21import { useScenarioStore } from '@/stores/scenarioStore';
22import { fersColors } from '@/theme';
23
24/**
25 * A styled resize handle for the resizable panels.
26 */
27function ResizeHandle() {
28 return (
29 <PanelResizeHandle>
30 <Box
31 sx={{
32 width: '2px',
33 height: '100%',
34 backgroundColor: 'divider',
35 transition: 'background-color 0.2s ease-in-out',
36 '&[data-resize-handle-state="drag"]': {
37 backgroundColor: 'primary.main',
38 },
39 '&:hover': {
40 backgroundColor: 'primary.light',
41 },
42 }}
43 />
44 </PanelResizeHandle>
45 );
46}
47
48/**
49 * ScenarioView is the primary workbench for building and visualizing 3D scenes.
50 */
51export const ScenarioView = React.memo(function ScenarioView() {
52 const isSimulating = useScenarioStore((state) => state.isSimulating);
53 const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
54 const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
55
56 // 1. Lift refs to this level to bridge Logic (Canvas) and UI (DOM)
57 const controlsRef = useRef<MapControlsImpl>(null);
58 const scaleLabelRef = useRef<HTMLDivElement>(null);
59 const scaleBarRef = useRef<HTMLDivElement>(null);
60
61 const handleExpandInspector = () => {
62 // Restore panels to their default sizes: [SceneTree, Main, Inspector]
63 panelGroupRef.current?.setLayout([25, 50, 25]);
64 };
65
66 return (
67 <Box
68 sx={{
69 height: '100%',
70 width: '100%',
71 overflow: 'hidden',
72 position: 'relative', // Establish positioning context
73 pointerEvents: isSimulating ? 'none' : 'auto',
74 opacity: isSimulating ? 0.5 : 1,
75 transition: 'opacity 0.3s ease-in-out',
76 userSelect: 'none',
77 WebkitUserSelect: 'none',
78 }}
79 >
80 <PanelGroup direction="horizontal" ref={panelGroupRef}>
81 <Panel id="scene-tree" defaultSize={25} minSize={20} order={1}>
82 <SceneTree />
83 </Panel>
84
85 <ResizeHandle />
86
87 <Panel id="main-content" minSize={30} order={2}>
88 <Box
89 sx={{
90 height: '100%',
91 display: 'flex',
92 flexDirection: 'column',
93 minWidth: 0, // Allow flex item to shrink below content size
94 overflow: 'hidden', // Prevent overflow
95 }}
96 >
97 <Box
98 sx={{
99 flex: 1,
100 position: 'relative',
101 minHeight: 0, // Allow flex item to shrink
102 overflow: 'hidden',
103 userSelect: 'none',
104 }}
105 >
106 <Canvas
107 shadows
108 camera={{
109 position: [100, 100, 100],
110 fov: 25,
111 near: 0.1,
112 far: 1e8,
113 }}
114 gl={{
115 logarithmicDepthBuffer: true,
116 }}
117 >
118 <color
119 attach="background"
120 args={[fersColors.background.canvas]}
121 />
122 {/* Pass controlsRef down to WorldView */}
123 <WorldView controlsRef={controlsRef} />
124
125 {/* Logic-only component for Scale Bar calculations */}
126 <ScaleManager
127 controlsRef={controlsRef}
128 labelRef={scaleLabelRef}
129 barRef={scaleBarRef}
130 />
131 </Canvas>
132
133 {/* View Controls (Top-Left) */}
134 <Box
135 sx={{
136 position: 'absolute',
137 top: 16,
138 left: 16,
139 zIndex: 1000,
140 }}
141 >
142 <ViewControls />
143 </Box>
144
145 {/* Scale Bar Overlay (Bottom-Left) */}
146 <Box
147 sx={{
148 position: 'absolute',
149 bottom: 16,
150 left: 16,
151 zIndex: 1000,
152 pointerEvents: 'none',
153 display: 'flex',
154 flexDirection: 'column',
155 alignItems: 'center',
156 textShadow: '0px 1px 2px rgba(0,0,0,0.8)',
157 }}
158 >
159 <div
160 ref={scaleLabelRef}
161 style={{
162 color: fersColors.text.primary,
163 fontSize: '11px',
164 fontWeight: 500,
165 marginBottom: '4px',
166 fontFamily: 'Roboto, sans-serif',
167 }}
168 >
169 -- m
170 </div>
171 <div
172 ref={scaleBarRef}
173 style={{
174 height: '6px',
175 border: `1px solid ${fersColors.text.primary}`,
176 borderTop: 'none',
177 width: '100px',
178 }}
179 />
180 </Box>
181 </Box>
182 <Box
183 sx={{
184 height: 100,
185 flexShrink: 0,
186 borderTop: 1,
187 borderColor: 'divider',
188 }}
189 >
190 <Timeline />
191 </Box>
192 </Box>
193 </Panel>
194
195 <ResizeHandle />
196
197 <Panel
198 id="property-inspector"
199 collapsible
200 collapsedSize={0}
201 onCollapse={() => setIsInspectorCollapsed(true)}
202 onExpand={() => setIsInspectorCollapsed(false)}
203 defaultSize={25}
204 minSize={5}
205 order={3}
206 >
207 <PropertyInspector />
208 </Panel>
209 </PanelGroup>
210
211 {isInspectorCollapsed && (
212 <Tooltip title="Show Properties">
213 <IconButton
214 onClick={handleExpandInspector}
215 sx={{
216 position: 'absolute',
217 right: 0,
218 top: '50%',
219 transform: 'translateY(-50%)',
220 zIndex: 1,
221 bgcolor: 'background.paper',
222 border: 1,
223 borderColor: 'divider',
224 borderRight: 'none',
225 borderTopRightRadius: 0,
226 borderBottomRightRadius: 0,
227 '&:hover': {
228 bgcolor: 'action.hover',
229 },
230 }}
231 >
232 <ChevronLeftIcon />
233 </IconButton>
234 </Tooltip>
235 )}
236 </Box>
237 );
238});