FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
ViewControls.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 { useState } from 'react';
5import {
6 Box,
7 IconButton,
8 Tooltip,
9 Paper,
10 Collapse,
11 Typography,
12 Switch,
13 FormControlLabel,
14 Divider,
15 Stack,
16 Checkbox,
17 Accordion,
18 AccordionSummary,
19 AccordionDetails,
20} from '@mui/material';
21
22import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
23import CenterFocusStrongIcon from '@mui/icons-material/CenterFocusStrong';
24import TravelExploreIcon from '@mui/icons-material/TravelExplore';
25import VideocamIcon from '@mui/icons-material/Videocam';
26import VideocamOffIcon from '@mui/icons-material/VideocamOff';
27import TuneIcon from '@mui/icons-material/Tune';
28import CloseIcon from '@mui/icons-material/Close';
29import { useScenarioStore, VisualizationLayers } from '@/stores/scenarioStore';
30
31// TODO: users should be able to drag the view controls button or pane to relocate it in the preview area. It should dynamically determine whether to expand upwards or downwards depending on whether it is opening from the bottom of the preview area or the top of the preview area.
32
33export default function ViewControls() {
34 const {
35 frameScene,
36 focusOnItem,
37 toggleFollowItem,
38 selectedItemId,
39 viewControlAction,
40 visibility,
41 toggleLayer,
42 } = useScenarioStore();
43
44 const [expanded, setExpanded] = useState(true);
45
46 // Helper to determine if we are following the currently selected item
47 const isFollowing =
48 viewControlAction.type === 'follow' &&
49 viewControlAction.targetId === selectedItemId;
50
51 const handleToggle = (key: keyof VisualizationLayers) => {
52 toggleLayer(key);
53 };
54
55 if (!expanded) {
56 return (
57 <Tooltip title="Open View Controls" placement="right">
58 <Paper
59 elevation={4}
60 sx={{
61 width: 40,
62 height: 40,
63 display: 'flex',
64 alignItems: 'center',
65 justifyContent: 'center',
66 borderRadius: '50%',
67 overflow: 'hidden',
68 }}
69 >
70 <IconButton onClick={() => setExpanded(true)} size="small">
71 <TuneIcon fontSize="small" />
72 </IconButton>
73 </Paper>
74 </Tooltip>
75 );
76 }
77
78 return (
79 <Paper
80 elevation={4}
81 sx={{
82 width: 280,
83 maxHeight: '80vh',
84 display: 'flex',
85 flexDirection: 'column',
86 overflow: 'hidden',
87 borderRadius: 2,
88 backgroundColor: 'background.paper',
89 }}
90 >
91 {/* 1. Header & Camera Controls (Always Visible) */}
92 <Box
93 sx={{
94 p: 1,
95 display: 'flex',
96 alignItems: 'center',
97 justifyContent: 'space-between',
98 bgcolor: 'action.hover',
99 borderBottom: 1,
100 borderColor: 'divider',
101 }}
102 >
103 <Stack direction="row" spacing={1}>
104 <Tooltip title="Frame Scene">
105 <IconButton size="small" onClick={frameScene}>
106 <TravelExploreIcon fontSize="small" />
107 </IconButton>
108 </Tooltip>
109 <Tooltip title="Focus on Selected">
110 <span>
111 <IconButton
112 size="small"
113 onClick={() =>
114 selectedItemId &&
115 focusOnItem(selectedItemId)
116 }
117 disabled={!selectedItemId}
118 color="primary"
119 >
120 <CenterFocusStrongIcon fontSize="small" />
121 </IconButton>
122 </span>
123 </Tooltip>
124 <Tooltip
125 title={
126 isFollowing ? 'Stop Following' : 'Follow Selected'
127 }
128 >
129 <span>
130 <IconButton
131 size="small"
132 onClick={() =>
133 selectedItemId &&
134 toggleFollowItem(selectedItemId)
135 }
136 disabled={!selectedItemId}
137 color={isFollowing ? 'secondary' : 'default'}
138 sx={{
139 bgcolor: isFollowing
140 ? 'secondary.main'
141 : 'transparent',
142 color: isFollowing ? 'white' : 'inherit',
143 '&:hover': {
144 bgcolor: isFollowing
145 ? 'secondary.dark'
146 : 'action.hover',
147 },
148 }}
149 >
150 {isFollowing ? (
151 <VideocamIcon fontSize="small" />
152 ) : (
153 <VideocamOffIcon fontSize="small" />
154 )}
155 </IconButton>
156 </span>
157 </Tooltip>
158 </Stack>
159 <IconButton size="small" onClick={() => setExpanded(false)}>
160 <CloseIcon fontSize="small" />
161 </IconButton>
162 </Box>
163
164 {/* 2. Scrollable Settings Area */}
165 <Box sx={{ overflowY: 'auto', p: 0 }}>
166 {/* SECTION: SCENE OBJECTS */}
167 <Accordion defaultExpanded disableGutters elevation={0}>
168 <AccordionSummary expandIcon={<ExpandMoreIcon />}>
169 <Typography variant="subtitle2">
170 Scene Objects
171 </Typography>
172 </AccordionSummary>
173 <AccordionDetails sx={{ pt: 0, pb: 2 }}>
174 <Stack spacing={0.5}>
175 <FormControlLabel
176 control={
177 <Switch
178 size="small"
179 checked={visibility.showPlatforms}
180 onChange={() =>
181 handleToggle('showPlatforms')
182 }
183 />
184 }
185 label={
186 <Typography variant="body2">
187 Platforms
188 </Typography>
189 }
190 />
191 <Collapse in={visibility.showPlatforms}>
192 <Box
193 sx={{
194 pl: 4,
195 display: 'flex',
196 flexDirection: 'column',
197 }}
198 >
199 <FormControlLabel
200 control={
201 <Checkbox
202 size="small"
203 checked={
204 visibility.showPlatformLabels
205 }
206 onChange={() =>
207 handleToggle(
208 'showPlatformLabels'
209 )
210 }
211 />
212 }
213 label={
214 <Typography variant="caption">
215 Labels
216 </Typography>
217 }
218 />
219 <FormControlLabel
220 control={
221 <Checkbox
222 size="small"
223 checked={visibility.showAxes}
224 onChange={() =>
225 handleToggle('showAxes')
226 }
227 />
228 }
229 label={
230 <Typography variant="caption">
231 Body Axes
232 </Typography>
233 }
234 />
235 <FormControlLabel
236 control={
237 <Checkbox
238 size="small"
239 checked={
240 visibility.showVelocities
241 }
242 onChange={() =>
243 handleToggle(
244 'showVelocities'
245 )
246 }
247 />
248 }
249 label={
250 <Typography variant="caption">
251 Velocity Vectors
252 </Typography>
253 }
254 />
255 <FormControlLabel
256 control={
257 <Checkbox
258 size="small"
259 checked={
260 visibility.showPatterns
261 }
262 onChange={() =>
263 handleToggle('showPatterns')
264 }
265 />
266 }
267 label={
268 <Typography variant="caption">
269 Antenna Patterns
270 </Typography>
271 }
272 />
273 <FormControlLabel
274 control={
275 <Checkbox
276 size="small"
277 checked={
278 visibility.showBoresights
279 }
280 onChange={() =>
281 handleToggle(
282 'showBoresights'
283 )
284 }
285 />
286 }
287 label={
288 <Typography variant="caption">
289 Boresight Arrows
290 </Typography>
291 }
292 />
293 </Box>
294 </Collapse>
295
296 <Divider sx={{ my: 1 }} />
297
298 <FormControlLabel
299 control={
300 <Switch
301 size="small"
302 checked={visibility.showMotionPaths}
303 onChange={() =>
304 handleToggle('showMotionPaths')
305 }
306 />
307 }
308 label={
309 <Typography variant="body2">
310 Motion Paths
311 </Typography>
312 }
313 />
314 </Stack>
315 </AccordionDetails>
316 </Accordion>
317
318 <Divider />
319
320 {/* SECTION: RF LINKS */}
321 <Accordion defaultExpanded disableGutters elevation={0}>
322 <AccordionSummary expandIcon={<ExpandMoreIcon />}>
323 <Typography variant="subtitle2">RF Links</Typography>
324 </AccordionSummary>
325 <AccordionDetails sx={{ pt: 0 }}>
326 <Stack spacing={0.5}>
327 <FormControlLabel
328 control={
329 <Switch
330 size="small"
331 checked={visibility.showLinks}
332 onChange={() =>
333 handleToggle('showLinks')
334 }
335 />
336 }
337 label={
338 <Typography variant="body2">
339 Show Links
340 </Typography>
341 }
342 />
343 <Collapse in={visibility.showLinks}>
344 <Box sx={{ pl: 1 }}>
345 <FormControlLabel
346 control={
347 <Checkbox
348 size="small"
349 checked={
350 visibility.showLinkLabels
351 }
352 onChange={() =>
353 handleToggle(
354 'showLinkLabels'
355 )
356 }
357 />
358 }
359 label={
360 <Typography variant="body2">
361 Show Labels
362 </Typography>
363 }
364 />
365 <Typography
366 variant="caption"
367 color="text.secondary"
368 sx={{ mt: 1, display: 'block' }}
369 >
370 Filter by Type
371 </Typography>
372 <Box
373 sx={{
374 pl: 1,
375 display: 'flex',
376 flexDirection: 'column',
377 }}
378 >
379 <FormControlLabel
380 control={
381 <Checkbox
382 size="small"
383 checked={
384 visibility.showLinkMonostatic
385 }
386 onChange={() =>
387 handleToggle(
388 'showLinkMonostatic'
389 )
390 }
391 />
392 }
393 label={
394 <Typography variant="caption">
395 Monostatic
396 </Typography>
397 }
398 />
399 <FormControlLabel
400 control={
401 <Checkbox
402 size="small"
403 checked={
404 visibility.showLinkIlluminator
405 }
406 onChange={() =>
407 handleToggle(
408 'showLinkIlluminator'
409 )
410 }
411 />
412 }
413 label={
414 <Typography variant="caption">
415 Bistatic (Illuminator)
416 </Typography>
417 }
418 />
419 <FormControlLabel
420 control={
421 <Checkbox
422 size="small"
423 checked={
424 visibility.showLinkScattered
425 }
426 onChange={() =>
427 handleToggle(
428 'showLinkScattered'
429 )
430 }
431 />
432 }
433 label={
434 <Typography variant="caption">
435 Bistatic (Scattered)
436 </Typography>
437 }
438 />
439 <FormControlLabel
440 control={
441 <Checkbox
442 size="small"
443 checked={
444 visibility.showLinkDirect
445 }
446 onChange={() =>
447 handleToggle(
448 'showLinkDirect'
449 )
450 }
451 />
452 }
453 label={
454 <Typography variant="caption">
455 Direct (Interference)
456 </Typography>
457 }
458 />
459 </Box>
460 </Box>
461 </Collapse>
462 </Stack>
463 </AccordionDetails>
464 </Accordion>
465 </Box>
466 </Paper>
467 );
468}