FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
SceneTree.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 { SimpleTreeView, TreeItem } from '@mui/x-tree-view';
5import { useScenarioStore, Platform } from '@/stores/scenarioStore';
6import { Box, Typography, IconButton, Tooltip, Divider } from '@mui/material';
7import { useShallow } from 'zustand/react/shallow';
8
9import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
10import ChevronRightIcon from '@mui/icons-material/ChevronRight';
11import PublicIcon from '@mui/icons-material/Public';
12import WavesIcon from '@mui/icons-material/Waves';
13import TimerIcon from '@mui/icons-material/Timer';
14import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
15import FlightIcon from '@mui/icons-material/Flight';
16import AddIcon from '@mui/icons-material/Add';
17import RemoveIcon from '@mui/icons-material/Remove';
18import SensorsIcon from '@mui/icons-material/Sensors';
19import PodcastsIcon from '@mui/icons-material/Podcasts';
20import RssFeedIcon from '@mui/icons-material/RssFeed';
21import AdjustIcon from '@mui/icons-material/Adjust';
22
23import ScenarioIO from './ScenarioIO';
24import React from 'react';
25
26const SectionHeader = ({
27 title,
28 onAdd,
29}: {
30 title: string;
31 onAdd: () => void;
32}) => (
33 <Box
34 sx={{
35 display: 'flex',
36 justifyContent: 'space-between',
37 alignItems: 'center',
38 width: '100%',
39 minWidth: 0, // Allow the component to shrink within its flex container
40 }}
41 >
42 <Typography
43 variant="overline"
44 sx={{
45 color: 'text.secondary',
46 whiteSpace: 'nowrap',
47 overflow: 'hidden',
48 textOverflow: 'ellipsis',
49 pr: 1, // Padding between text and add button
50 }}
51 >
52 {title}
53 </Typography>
54 <Tooltip title={`Add ${title.slice(0, -1)}`}>
55 <IconButton
56 size="small"
57 onClick={(e) => {
58 e.stopPropagation();
59 onAdd();
60 }}
61 >
62 <AddIcon fontSize="inherit" />
63 </IconButton>
64 </Tooltip>
65 </Box>
66);
67
68const getPlatformIcon = (platform: Platform) => {
69 const types = platform.components.map((c) => c.type);
70
71 if (types.includes('monostatic')) {
72 return <SensorsIcon sx={{ mr: 1 }} fontSize="small" />;
73 }
74 if (types.includes('transmitter')) {
75 return <PodcastsIcon sx={{ mr: 1 }} fontSize="small" />;
76 }
77 if (types.includes('receiver')) {
78 return <RssFeedIcon sx={{ mr: 1 }} fontSize="small" />;
79 }
80 if (types.includes('target')) {
81 return <AdjustIcon sx={{ mr: 1 }} fontSize="small" />;
82 }
83 return <FlightIcon sx={{ mr: 1 }} fontSize="small" />;
84};
85
86export default function SceneTree() {
87 const {
88 globalParameters,
89 waveforms,
90 timings,
91 antennas,
92 platforms,
93 selectedItemId,
94 selectedComponentId,
95 selectItem,
96 addWaveform,
97 addTiming,
98 addAntenna,
99 addPlatform,
100 removeItem,
101 } = useScenarioStore(
102 useShallow((state) => ({
103 globalParameters: state.globalParameters,
104 waveforms: state.waveforms,
105 timings: state.timings,
106 antennas: state.antennas,
107 platforms: state.platforms,
108 selectedItemId: state.selectedItemId,
109 selectedComponentId: state.selectedComponentId,
110 selectItem: state.selectItem,
111 addWaveform: state.addWaveform,
112 addTiming: state.addTiming,
113 addAntenna: state.addAntenna,
114 addPlatform: state.addPlatform,
115 removeItem: state.removeItem,
116 }))
117 );
118
119 const handleSelect = (
120 _event: React.SyntheticEvent | null,
121 nodeId: string | null
122 ) => {
123 const rootNodes = [
124 'waveforms-root',
125 'timings-root',
126 'antennas-root',
127 'platforms-root',
128 ];
129 if (nodeId && rootNodes.includes(nodeId)) {
130 return;
131 }
132 selectItem(nodeId);
133 };
134
135 return (
136 <Box
137 sx={{
138 height: '100%',
139 display: 'flex',
140 flexDirection: 'column',
141 }}
142 >
143 {/* 1. Fixed Header */}
144 <Box
145 sx={{
146 display: 'flex',
147 justifyContent: 'space-between',
148 alignItems: 'center',
149 flexShrink: 0,
150 px: 2,
151 pt: 2,
152 pb: 1,
153 }}
154 >
155 <Typography variant="overline" sx={{ color: 'text.secondary' }}>
156 Scenario Explorer
157 </Typography>
158 <Box>
159 <ScenarioIO />
160 </Box>
161 </Box>
162 <Divider sx={{ mx: 2, mb: 1 }} />
163
164 {/* 2. Scrollable Content Area */}
165 <Box
166 sx={{
167 flexGrow: 1, // Takes up all remaining space
168 overflowY: 'auto', // Enables vertical scrolling
169 minHeight: 0, // Crucial for flexbox scrolling
170 px: 2, // Apply horizontal padding inside scroll area
171 }}
172 >
173 <SimpleTreeView
174 selectedItems={selectedComponentId ?? selectedItemId}
175 onSelectedItemsChange={handleSelect}
176 slots={{
177 collapseIcon: ExpandMoreIcon,
178 expandIcon: ChevronRightIcon,
179 }}
180 sx={{
181 // Show remove button on hover
182 '& .MuiTreeItem-content .remove-button': {
183 visibility: 'hidden',
184 },
185 '& .MuiTreeItem-content:hover .remove-button': {
186 visibility: 'visible',
187 },
188 '& .MuiTreeItem-content': { py: 0.5 },
189 }}
190 >
191 <TreeItem
192 itemId={globalParameters.id}
193 label={
194 <Box sx={{ display: 'flex', alignItems: 'center' }}>
195 <PublicIcon sx={{ mr: 1 }} fontSize="small" />
196 <Typography variant="body2">
197 Global Parameters
198 </Typography>
199 </Box>
200 }
201 />
202 <TreeItem
203 itemId="waveforms-root"
204 label={
205 <Box
206 sx={{
207 display: 'flex',
208 alignItems: 'center',
209 width: '100%',
210 }}
211 >
212 <WavesIcon sx={{ mr: 1 }} fontSize="small" />
213 <SectionHeader
214 title="Waveforms"
215 onAdd={addWaveform}
216 />
217 </Box>
218 }
219 >
220 {waveforms.map((waveform) => (
221 <TreeItem
222 key={waveform.id}
223 itemId={waveform.id}
224 label={
225 <Box
226 sx={{
227 display: 'flex',
228 alignItems: 'center',
229 width: '100%',
230 }}
231 >
232 <WavesIcon
233 sx={{ mr: 1 }}
234 fontSize="small"
235 />
236 <Typography
237 variant="body2"
238 sx={{
239 flexGrow: 1,
240 whiteSpace: 'nowrap',
241 overflow: 'hidden',
242 textOverflow: 'ellipsis',
243 minWidth: 0,
244 pr: 1,
245 }}
246 >
247 {waveform.name}
248 </Typography>
249 <IconButton
250 size="small"
251 className="remove-button"
252 onClick={(e) => {
253 e.stopPropagation();
254 removeItem(waveform.id);
255 }}
256 >
257 <RemoveIcon fontSize="inherit" />
258 </IconButton>
259 </Box>
260 }
261 />
262 ))}
263 </TreeItem>
264 <TreeItem
265 itemId="timings-root"
266 label={
267 <Box
268 sx={{
269 display: 'flex',
270 alignItems: 'center',
271 width: '100%',
272 }}
273 >
274 <TimerIcon sx={{ mr: 1 }} fontSize="small" />
275 <SectionHeader
276 title="Timings"
277 onAdd={addTiming}
278 />
279 </Box>
280 }
281 >
282 {timings.map((timing) => (
283 <TreeItem
284 key={timing.id}
285 itemId={timing.id}
286 label={
287 <Box
288 sx={{
289 display: 'flex',
290 alignItems: 'center',
291 width: '100%',
292 }}
293 >
294 <TimerIcon
295 sx={{ mr: 1 }}
296 fontSize="small"
297 />
298 <Typography
299 variant="body2"
300 sx={{
301 flexGrow: 1,
302 whiteSpace: 'nowrap',
303 overflow: 'hidden',
304 textOverflow: 'ellipsis',
305 minWidth: 0,
306 pr: 1,
307 }}
308 >
309 {timing.name}
310 </Typography>
311 <IconButton
312 size="small"
313 className="remove-button"
314 onClick={(e) => {
315 e.stopPropagation();
316 removeItem(timing.id);
317 }}
318 >
319 <RemoveIcon fontSize="inherit" />
320 </IconButton>
321 </Box>
322 }
323 />
324 ))}
325 </TreeItem>
326 <TreeItem
327 itemId="antennas-root"
328 label={
329 <Box
330 sx={{
331 display: 'flex',
332 alignItems: 'center',
333 width: '100%',
334 }}
335 >
336 <SettingsInputAntennaIcon
337 sx={{ mr: 1 }}
338 fontSize="small"
339 />
340 <SectionHeader
341 title="Antennas"
342 onAdd={addAntenna}
343 />
344 </Box>
345 }
346 >
347 {antennas.map((antenna) => (
348 <TreeItem
349 key={antenna.id}
350 itemId={antenna.id}
351 label={
352 <Box
353 sx={{
354 display: 'flex',
355 alignItems: 'center',
356 width: '100%',
357 }}
358 >
359 <SettingsInputAntennaIcon
360 sx={{ mr: 1 }}
361 fontSize="small"
362 />
363 <Typography
364 variant="body2"
365 sx={{
366 flexGrow: 1,
367 whiteSpace: 'nowrap',
368 overflow: 'hidden',
369 textOverflow: 'ellipsis',
370 minWidth: 0,
371 pr: 1,
372 }}
373 >
374 {antenna.name}
375 </Typography>
376 <IconButton
377 size="small"
378 className="remove-button"
379 onClick={(e) => {
380 e.stopPropagation();
381 removeItem(antenna.id);
382 }}
383 >
384 <RemoveIcon fontSize="inherit" />
385 </IconButton>
386 </Box>
387 }
388 />
389 ))}
390 </TreeItem>
391 <TreeItem
392 itemId="platforms-root"
393 label={
394 <Box
395 sx={{
396 display: 'flex',
397 alignItems: 'center',
398 width: '100%',
399 }}
400 >
401 <FlightIcon sx={{ mr: 1 }} fontSize="small" />
402 <SectionHeader
403 title="Platforms"
404 onAdd={addPlatform}
405 />
406 </Box>
407 }
408 >
409 {platforms.map((platform) => {
410 return (
411 <TreeItem
412 key={platform.id}
413 itemId={platform.id}
414 label={
415 <Box
416 sx={{
417 display: 'flex',
418 alignItems: 'center',
419 width: '100%',
420 }}
421 >
422 {getPlatformIcon(platform)}
423 <Typography
424 variant="body2"
425 sx={{
426 flexGrow: 1,
427 whiteSpace: 'nowrap',
428 overflow: 'hidden',
429 textOverflow: 'ellipsis',
430 minWidth: 0,
431 pr: 1,
432 }}
433 >
434 {platform.name}
435 </Typography>
436 <IconButton
437 size="small"
438 className="remove-button"
439 onClick={(e) => {
440 e.stopPropagation();
441 removeItem(platform.id);
442 }}
443 >
444 <RemoveIcon fontSize="inherit" />
445 </IconButton>
446 </Box>
447 }
448 />
449 );
450 })}
451 </TreeItem>
452 </SimpleTreeView>
453 </Box>
454 </Box>
455 );
456}