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