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 selectItem,
95 addWaveform,
96 addTiming,
97 addAntenna,
98 addPlatform,
99 removeItem,
100 } = useScenarioStore(
101 useShallow((state) => ({
102 globalParameters: state.globalParameters,
103 waveforms: state.waveforms,
104 timings: state.timings,
105 antennas: state.antennas,
106 platforms: state.platforms,
107 selectedItemId: state.selectedItemId,
108 selectItem: state.selectItem,
109 addWaveform: state.addWaveform,
110 addTiming: state.addTiming,
111 addAntenna: state.addAntenna,
112 addPlatform: state.addPlatform,
113 removeItem: state.removeItem,
114 }))
115 );
116
117 const handleSelect = (
118 _event: React.SyntheticEvent | null,
119 nodeId: string | null
120 ) => {
121 const rootNodes = [
122 'waveforms-root',
123 'timings-root',
124 'antennas-root',
125 'platforms-root',
126 ];
127 if (nodeId && rootNodes.includes(nodeId)) {
128 return;
129 }
130 selectItem(nodeId);
131 };
132
133 return (
134 <Box
135 sx={{
136 height: '100%',
137 display: 'flex',
138 flexDirection: 'column',
139 }}
140 >
141 {/* 1. Fixed Header */}
142 <Box
143 sx={{
144 display: 'flex',
145 justifyContent: 'space-between',
146 alignItems: 'center',
147 flexShrink: 0,
148 px: 2,
149 pt: 2,
150 pb: 1,
151 }}
152 >
153 <Typography variant="overline" sx={{ color: 'text.secondary' }}>
154 Scenario Explorer
155 </Typography>
156 <Box>
157 <ScenarioIO />
158 </Box>
159 </Box>
160 <Divider sx={{ mx: 2, mb: 1 }} />
161
162 {/* 2. Scrollable Content Area */}
163 <Box
164 sx={{
165 flexGrow: 1, // Takes up all remaining space
166 overflowY: 'auto', // Enables vertical scrolling
167 minHeight: 0, // Crucial for flexbox scrolling
168 px: 2, // Apply horizontal padding inside scroll area
169 }}
170 >
171 <SimpleTreeView
172 selectedItems={selectedItemId}
173 onSelectedItemsChange={handleSelect}
174 slots={{
175 collapseIcon: ExpandMoreIcon,
176 expandIcon: ChevronRightIcon,
177 }}
178 sx={{
179 // Show remove button on hover
180 '& .MuiTreeItem-content .remove-button': {
181 visibility: 'hidden',
182 },
183 '& .MuiTreeItem-content:hover .remove-button': {
184 visibility: 'visible',
185 },
186 '& .MuiTreeItem-content': { py: 0.5 },
187 }}
188 >
189 <TreeItem
190 itemId={globalParameters.id}
191 label={
192 <Box sx={{ display: 'flex', alignItems: 'center' }}>
193 <PublicIcon sx={{ mr: 1 }} fontSize="small" />
194 <Typography variant="body2">
195 Global Parameters
196 </Typography>
197 </Box>
198 }
199 />
200 <TreeItem
201 itemId="waveforms-root"
202 label={
203 <Box
204 sx={{
205 display: 'flex',
206 alignItems: 'center',
207 width: '100%',
208 }}
209 >
210 <WavesIcon sx={{ mr: 1 }} fontSize="small" />
211 <SectionHeader
212 title="Waveforms"
213 onAdd={addWaveform}
214 />
215 </Box>
216 }
217 >
218 {waveforms.map((waveform) => (
219 <TreeItem
220 key={waveform.id}
221 itemId={waveform.id}
222 label={
223 <Box
224 sx={{
225 display: 'flex',
226 alignItems: 'center',
227 width: '100%',
228 }}
229 >
230 <WavesIcon
231 sx={{ mr: 1 }}
232 fontSize="small"
233 />
234 <Typography
235 variant="body2"
236 sx={{
237 flexGrow: 1,
238 whiteSpace: 'nowrap',
239 overflow: 'hidden',
240 textOverflow: 'ellipsis',
241 minWidth: 0,
242 pr: 1,
243 }}
244 >
245 {waveform.name}
246 </Typography>
247 <IconButton
248 size="small"
249 className="remove-button"
250 onClick={(e) => {
251 e.stopPropagation();
252 removeItem(waveform.id);
253 }}
254 >
255 <RemoveIcon fontSize="inherit" />
256 </IconButton>
257 </Box>
258 }
259 />
260 ))}
261 </TreeItem>
262 <TreeItem
263 itemId="timings-root"
264 label={
265 <Box
266 sx={{
267 display: 'flex',
268 alignItems: 'center',
269 width: '100%',
270 }}
271 >
272 <TimerIcon sx={{ mr: 1 }} fontSize="small" />
273 <SectionHeader
274 title="Timings"
275 onAdd={addTiming}
276 />
277 </Box>
278 }
279 >
280 {timings.map((timing) => (
281 <TreeItem
282 key={timing.id}
283 itemId={timing.id}
284 label={
285 <Box
286 sx={{
287 display: 'flex',
288 alignItems: 'center',
289 width: '100%',
290 }}
291 >
292 <TimerIcon
293 sx={{ mr: 1 }}
294 fontSize="small"
295 />
296 <Typography
297 variant="body2"
298 sx={{
299 flexGrow: 1,
300 whiteSpace: 'nowrap',
301 overflow: 'hidden',
302 textOverflow: 'ellipsis',
303 minWidth: 0,
304 pr: 1,
305 }}
306 >
307 {timing.name}
308 </Typography>
309 <IconButton
310 size="small"
311 className="remove-button"
312 onClick={(e) => {
313 e.stopPropagation();
314 removeItem(timing.id);
315 }}
316 >
317 <RemoveIcon fontSize="inherit" />
318 </IconButton>
319 </Box>
320 }
321 />
322 ))}
323 </TreeItem>
324 <TreeItem
325 itemId="antennas-root"
326 label={
327 <Box
328 sx={{
329 display: 'flex',
330 alignItems: 'center',
331 width: '100%',
332 }}
333 >
334 <SettingsInputAntennaIcon
335 sx={{ mr: 1 }}
336 fontSize="small"
337 />
338 <SectionHeader
339 title="Antennas"
340 onAdd={addAntenna}
341 />
342 </Box>
343 }
344 >
345 {antennas.map((antenna) => (
346 <TreeItem
347 key={antenna.id}
348 itemId={antenna.id}
349 label={
350 <Box
351 sx={{
352 display: 'flex',
353 alignItems: 'center',
354 width: '100%',
355 }}
356 >
357 <SettingsInputAntennaIcon
358 sx={{ mr: 1 }}
359 fontSize="small"
360 />
361 <Typography
362 variant="body2"
363 sx={{
364 flexGrow: 1,
365 whiteSpace: 'nowrap',
366 overflow: 'hidden',
367 textOverflow: 'ellipsis',
368 minWidth: 0,
369 pr: 1,
370 }}
371 >
372 {antenna.name}
373 </Typography>
374 <IconButton
375 size="small"
376 className="remove-button"
377 onClick={(e) => {
378 e.stopPropagation();
379 removeItem(antenna.id);
380 }}
381 >
382 <RemoveIcon fontSize="inherit" />
383 </IconButton>
384 </Box>
385 }
386 />
387 ))}
388 </TreeItem>
389 <TreeItem
390 itemId="platforms-root"
391 label={
392 <Box
393 sx={{
394 display: 'flex',
395 alignItems: 'center',
396 width: '100%',
397 }}
398 >
399 <FlightIcon sx={{ mr: 1 }} fontSize="small" />
400 <SectionHeader
401 title="Platforms"
402 onAdd={addPlatform}
403 />
404 </Box>
405 }
406 >
407 {platforms.map((platform) => {
408 return (
409 <TreeItem
410 key={platform.id}
411 itemId={platform.id}
412 label={
413 <Box
414 sx={{
415 display: 'flex',
416 alignItems: 'center',
417 width: '100%',
418 }}
419 >
420 {getPlatformIcon(platform)}
421 <Typography
422 variant="body2"
423 sx={{
424 flexGrow: 1,
425 whiteSpace: 'nowrap',
426 overflow: 'hidden',
427 textOverflow: 'ellipsis',
428 minWidth: 0,
429 pr: 1,
430 }}
431 >
432 {platform.name}
433 </Typography>
434 <IconButton
435 size="small"
436 className="remove-button"
437 onClick={(e) => {
438 e.stopPropagation();
439 removeItem(platform.id);
440 }}
441 >
442 <RemoveIcon fontSize="inherit" />
443 </IconButton>
444 </Box>
445 }
446 />
447 );
448 })}
449 </TreeItem>
450 </SimpleTreeView>
451 </Box>
452 </Box>
453 );
454}