FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
PlatformComponentInspector.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 DeleteIcon from '@mui/icons-material/Delete';
5import {
6 Alert,
7 Box,
8 Button,
9 Checkbox,
10 FormControl,
11 FormControlLabel,
12 IconButton,
13 InputLabel,
14 MenuItem,
15 Select,
16 Typography,
17} from '@mui/material';
18import {
19 MonostaticComponent,
20 Platform,
21 PlatformComponent,
22 ReceiverComponent,
23 SchedulePeriod,
24 TargetComponent,
25 TransmitterComponent,
26 useScenarioStore,
27 Waveform,
28} from '@/stores/scenarioStore';
29import {
30 createDechirpReference,
31 createFmcwModeConfig,
32 DECHIRP_MODE_OPTIONS,
33 DECHIRP_REFERENCE_SOURCE_OPTIONS,
34 DechirpMode,
35 DechirpReferenceSource,
36 getDechirpMode,
37 getDechirpReferenceSource,
38 isFmcwWaveformType,
39 ReceiverFmcwModeConfig,
40} from '@/stores/scenarioStore/fmcwModeConfig';
41import { validateFmcwScenario } from '@/stores/scenarioStore/fmcwValidation';
42import {
43 BufferedTextField,
44 FileInput,
45 NumberField,
46 Section,
47} from './InspectorControls';
48
49export type RadarType = 'pulsed' | 'cw' | 'fmcw' | 'sfcw';
50type CompatibleWaveform = {
51 id: string;
52 name: string;
53 waveformType: string;
54};
55type FmcwIfChainKey =
56 | 'if_sample_rate'
57 | 'if_filter_bandwidth'
58 | 'if_filter_transition_width';
59
60export const RADAR_MODE_OPTIONS: ReadonlyArray<{
61 value: RadarType;
62 label: string;
63}> = [
64 { value: 'pulsed', label: 'Pulsed' },
65 { value: 'cw', label: 'CW' },
66 { value: 'fmcw', label: 'FMCW' },
67 { value: 'sfcw', label: 'SFCW' },
68];
69
70const WAVEFORM_TYPE_BY_RADAR_TYPE: Record<RadarType, string[]> = {
71 pulsed: ['pulsed_from_file'],
72 cw: ['cw_from_file', 'cw'],
73 fmcw: ['fmcw_from_file', 'fmcw_linear_chirp', 'fmcw_triangle'],
74 sfcw: ['stepped_frequency'],
75};
76
77export function isWaveformCompatibleWithRadarType(
78 waveform: CompatibleWaveform | undefined,
79 radarType: RadarType
80): boolean {
81 return waveform
82 ? WAVEFORM_TYPE_BY_RADAR_TYPE[radarType].includes(waveform.waveformType)
83 : false;
84}
85
86export function getCompatibleWaveforms(
87 waveforms: CompatibleWaveform[],
88 radarType: RadarType
89): CompatibleWaveform[] {
90 return waveforms.filter((waveform) =>
91 isWaveformCompatibleWithRadarType(waveform, radarType)
92 );
93}
94
95export function shouldClearWaveformForRadarType(
96 waveformId: string | null | undefined,
97 waveforms: CompatibleWaveform[],
98 radarType: RadarType
99): boolean {
100 if (!waveformId) {
101 return false;
102 }
103
104 return !isWaveformCompatibleWithRadarType(
105 waveforms.find((waveform) => waveform.id === waveformId),
106 radarType
107 );
108}
109
110export function resolveWaveformSelectValue(
111 waveformId: string | null | undefined,
112 waveforms: CompatibleWaveform[],
113 radarType: RadarType
114): string {
115 return shouldClearWaveformForRadarType(waveformId, waveforms, radarType)
116 ? ''
117 : (waveformId ?? '');
118}
119
120export function getPulsedRadarFieldLabels(radarType: RadarType): string[] {
121 return radarType === 'pulsed'
122 ? ['PRF (Hz)', 'Window Skip (s)', 'Window Length (s)']
123 : [];
124}
125
126export {
127 createDechirpReference,
128 createFmcwModeConfig,
129 DECHIRP_MODE_OPTIONS,
130 DECHIRP_REFERENCE_SOURCE_OPTIONS,
131};
132
133const uniqueNames = (names: string[]): string[] =>
134 Array.from(new Set(names.filter((name) => name.trim().length > 0)));
135
136const includeCurrentName = (names: string[], currentName?: string): string[] =>
137 currentName && !names.includes(currentName)
138 ? [currentName, ...names]
139 : names;
140
141export function getFmcwWaveformNames(waveforms: Waveform[]): string[] {
142 return uniqueNames(
143 waveforms
144 .filter((waveform) => isFmcwWaveformType(waveform.waveformType))
145 .map((waveform) => waveform.name)
146 );
147}
148
149export function getFmcwEmitterNames(
150 platforms: Platform[],
151 waveforms: Waveform[]
152): string[] {
153 const waveformsById = new Map(
154 waveforms.map((waveform) => [waveform.id, waveform])
155 );
156 return uniqueNames(
157 platforms.flatMap((platform) =>
158 platform.components.flatMap((component) => {
159 if (
160 component.type !== 'transmitter' &&
161 component.type !== 'monostatic'
162 ) {
163 return [];
164 }
165 if (
166 component.radarType !== 'fmcw' ||
167 !component.waveformId ||
168 !isFmcwWaveformType(
169 waveformsById.get(component.waveformId)?.waveformType
170 )
171 ) {
172 return [];
173 }
174 return [component.name];
175 })
176 )
177 );
178}
179
180export function getAvailableDechirpReferenceSourceOptions(
181 componentType: MonostaticComponent['type'] | ReceiverComponent['type'],
182 currentSource?: DechirpReferenceSource
183) {
184 return DECHIRP_REFERENCE_SOURCE_OPTIONS.filter(
185 (option) =>
186 option.value !== 'attached' ||
187 componentType === 'monostatic' ||
188 currentSource === 'attached'
189 );
190}
191
192interface PlatformComponentInspectorProps {
193 component: PlatformComponent;
194 platformId: string;
195 index: number;
196}
197
198export function PlatformComponentInspector({
199 component,
200 platformId,
201 index,
202}: PlatformComponentInspectorProps) {
203 const {
204 updateItem,
205 waveforms,
206 timings,
207 antennas,
208 platforms,
209 globalParameters,
210 setPlatformRcsModel,
211 } = useScenarioStore.getState();
212 const fmcwIssues = validateFmcwScenario({
213 globalParameters,
214 waveforms,
215 platforms,
216 });
217 const fmcwEmitterNames = getFmcwEmitterNames(platforms, waveforms);
218 const fmcwWaveformNames = getFmcwWaveformNames(waveforms);
219
220 // Updates are targeted using the array index in the path string
221 const handleChange = (path: string, value: unknown) =>
222 updateItem(platformId, `components.${index}.${path}`, value);
223 const handleComponentChange = (value: PlatformComponent) =>
224 updateItem(platformId, `components.${index}`, value);
225
226 const renderSchedule = (
227 c: MonostaticComponent | TransmitterComponent | ReceiverComponent
228 ) => {
229 const schedule = c.schedule || [];
230 const scheduleIssues = fmcwIssues.filter(
231 (issue) => issue.componentId === c.id && issue.field === 'schedule'
232 );
233
234 const handleAddPeriod = () => {
235 handleChange('schedule', [...schedule, { start: 0, end: 0 }]);
236 };
237
238 const handleRemovePeriod = (idx: number) => {
239 const newSchedule = [...schedule];
240 newSchedule.splice(idx, 1);
241 handleChange('schedule', newSchedule);
242 };
243
244 const handlePeriodChange = (
245 idx: number,
246 field: keyof SchedulePeriod,
247 val: number | null
248 ) => {
249 const newSchedule = [...schedule];
250 newSchedule[idx] = { ...newSchedule[idx], [field]: val ?? 0 };
251 handleChange('schedule', newSchedule);
252 };
253
254 return (
255 <Section title="Operating Schedule">
256 {scheduleIssues.map((issue) => (
257 <Alert
258 key={issue.message}
259 severity={issue.severity}
260 variant="outlined"
261 >
262 {issue.message}
263 </Alert>
264 ))}
265 {schedule.length === 0 && (
266 <Typography variant="body2" color="text.secondary">
267 No specific schedule defined (always active).
268 </Typography>
269 )}
270 {schedule.map((period, i) => (
271 <Box
272 key={i}
273 sx={{
274 display: 'flex',
275 alignItems: 'center',
276 gap: 1,
277 p: 1,
278 border: 1,
279 borderColor: 'divider',
280 borderRadius: 1,
281 }}
282 >
283 <NumberField
284 label="Start (s)"
285 value={period.start}
286 emptyBehavior="revert"
287 onChange={(v) => handlePeriodChange(i, 'start', v)}
288 />
289 <NumberField
290 label="End (s)"
291 value={period.end}
292 emptyBehavior="revert"
293 onChange={(v) => handlePeriodChange(i, 'end', v)}
294 />
295 <IconButton
296 size="small"
297 onClick={() => handleRemovePeriod(i)}
298 color="error"
299 >
300 <DeleteIcon fontSize="small" />
301 </IconButton>
302 </Box>
303 ))}
304 <Button
305 onClick={handleAddPeriod}
306 size="small"
307 variant="outlined"
308 sx={{ mt: 1 }}
309 >
310 Add Schedule Period
311 </Button>
312 </Section>
313 );
314 };
315
316 const renderCommonRadarFields = (
317 c: MonostaticComponent | TransmitterComponent | ReceiverComponent
318 ) => {
319 const radarType = c.radarType as RadarType;
320 const compatibleWaveforms = getCompatibleWaveforms(
321 waveforms,
322 radarType
323 );
324 const handleRadarTypeChange = (nextRadarType: RadarType) => {
325 const prepareModeChange = <
326 T extends
327 | MonostaticComponent
328 | TransmitterComponent
329 | ReceiverComponent,
330 >(
331 component: T,
332 waveformId?: string | null
333 ): T => {
334 const nextComponent = {
335 ...component,
336 radarType: nextRadarType,
337 ...(waveformId !== undefined ? { waveformId } : {}),
338 } as T;
339
340 if (
341 nextComponent.type === 'receiver' ||
342 nextComponent.type === 'monostatic'
343 ) {
344 if (nextRadarType === 'fmcw') {
345 return {
346 ...nextComponent,
347 fmcwModeConfig: nextComponent.fmcwModeConfig ?? {},
348 } as T;
349 }
350
351 const {
352 fmcwModeConfig: _fmcwModeConfig,
353 ...withoutFmcwMode
354 } = nextComponent;
355 return withoutFmcwMode as T;
356 }
357
358 return nextComponent;
359 };
360
361 if ('waveformId' in c) {
362 const waveformId = shouldClearWaveformForRadarType(
363 c.waveformId,
364 waveforms,
365 nextRadarType
366 )
367 ? null
368 : c.waveformId;
369
370 handleComponentChange(prepareModeChange(c, waveformId));
371 return;
372 }
373
374 handleComponentChange(prepareModeChange(c));
375 };
376
377 return (
378 <>
379 <BufferedTextField
380 label="Component Name"
381 size="small"
382 fullWidth
383 value={c.name}
384 allowEmpty={false}
385 onChange={(v) => handleChange('name', v)}
386 sx={{ mb: 2 }}
387 />
388 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
389 <InputLabel>Radar Mode</InputLabel>
390 <Select
391 label="Radar Mode"
392 value={radarType}
393 onChange={(e) =>
394 handleRadarTypeChange(e.target.value as RadarType)
395 }
396 >
397 {RADAR_MODE_OPTIONS.map((option) => (
398 <MenuItem key={option.value} value={option.value}>
399 {option.label}
400 </MenuItem>
401 ))}
402 </Select>
403 </FormControl>
404
405 {'waveformId' in c && (
406 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
407 <InputLabel>Waveform</InputLabel>
408 <Select
409 label="Waveform"
410 value={resolveWaveformSelectValue(
411 c.waveformId,
412 waveforms,
413 radarType
414 )}
415 onChange={(e) =>
416 handleChange(
417 'waveformId',
418 e.target.value === ''
419 ? null
420 : e.target.value
421 )
422 }
423 >
424 <MenuItem value="">
425 <em>None</em>
426 </MenuItem>
427 {compatibleWaveforms.map((w) => (
428 <MenuItem key={w.id} value={w.id}>
429 {w.name}
430 </MenuItem>
431 ))}
432 </Select>
433 </FormControl>
434 )}
435
436 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
437 <InputLabel>Antenna</InputLabel>
438 <Select
439 label="Antenna"
440 value={c.antennaId ?? ''}
441 onChange={(e) =>
442 handleChange(
443 'antennaId',
444 e.target.value === '' ? null : e.target.value
445 )
446 }
447 >
448 <MenuItem value="">
449 <em>None</em>
450 </MenuItem>
451 {antennas.map((a) => (
452 <MenuItem key={a.id} value={a.id}>
453 {a.name}
454 </MenuItem>
455 ))}
456 </Select>
457 </FormControl>
458 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
459 <InputLabel>Timing Source</InputLabel>
460 <Select
461 label="Timing Source"
462 value={c.timingId ?? ''}
463 onChange={(e) =>
464 handleChange(
465 'timingId',
466 e.target.value === '' ? null : e.target.value
467 )
468 }
469 >
470 <MenuItem value="">
471 <em>None</em>
472 </MenuItem>
473 {timings.map((t) => (
474 <MenuItem key={t.id} value={t.id}>
475 {t.name}
476 </MenuItem>
477 ))}
478 </Select>
479 </FormControl>
480 </>
481 );
482 };
483
484 const renderFmcwReceiverFields = (
485 c: MonostaticComponent | ReceiverComponent
486 ) => {
487 const config = c.fmcwModeConfig ?? {};
488 const dechirpMode = getDechirpMode(config);
489 const reference = config.dechirp_reference;
490 const referenceSource = getDechirpReferenceSource(reference);
491 const transmitterName =
492 reference?.source === 'transmitter'
493 ? reference.transmitter_name
494 : undefined;
495 const waveformName =
496 reference?.source === 'custom'
497 ? reference.waveform_name
498 : undefined;
499 const referenceIssues = fmcwIssues.filter(
500 (issue) =>
501 issue.componentId === c.id && issue.field === 'fmcwModeConfig'
502 );
503 const referenceSourceOptions =
504 getAvailableDechirpReferenceSourceOptions(c.type, referenceSource);
505
506 const createDefaultReference = () => {
507 if (c.type === 'monostatic') {
508 return createDechirpReference('attached');
509 }
510 const transmitterName = fmcwEmitterNames[0];
511 if (transmitterName) {
512 return {
513 source: 'transmitter' as const,
514 transmitter_name: transmitterName,
515 };
516 }
517 const waveformName = fmcwWaveformNames[0];
518 if (waveformName) {
519 return {
520 source: 'custom' as const,
521 waveform_name: waveformName,
522 };
523 }
524 return createDechirpReference('transmitter');
525 };
526
527 const commitConfig = (nextConfig: ReceiverFmcwModeConfig) => {
528 handleChange('fmcwModeConfig', nextConfig);
529 };
530
531 const handleModeChange = (nextMode: DechirpMode) => {
532 const nextConfig = createFmcwModeConfig(nextMode, config);
533 if (
534 nextMode !== 'none' &&
535 (!nextConfig.dechirp_reference ||
536 (c.type === 'receiver' &&
537 nextConfig.dechirp_reference.source === 'attached'))
538 ) {
539 nextConfig.dechirp_reference = createDefaultReference();
540 }
541 commitConfig(nextConfig);
542 };
543
544 const handleReferenceSourceChange = (
545 nextSource: DechirpReferenceSource
546 ) => {
547 const nextReference = createDechirpReference(nextSource, reference);
548 if (
549 nextReference.source === 'transmitter' &&
550 !nextReference.transmitter_name &&
551 fmcwEmitterNames[0]
552 ) {
553 nextReference.transmitter_name = fmcwEmitterNames[0];
554 }
555 if (
556 nextReference.source === 'custom' &&
557 !nextReference.waveform_name &&
558 fmcwWaveformNames[0]
559 ) {
560 nextReference.waveform_name = fmcwWaveformNames[0];
561 }
562 commitConfig({
563 ...config,
564 dechirp_mode: dechirpMode,
565 dechirp_reference: nextReference,
566 });
567 };
568
569 const handleTransmitterReferenceChange = (name: string) => {
570 commitConfig({
571 ...config,
572 dechirp_mode: dechirpMode,
573 dechirp_reference: {
574 source: 'transmitter',
575 ...(name ? { transmitter_name: name } : {}),
576 },
577 });
578 };
579
580 const handleCustomWaveformReferenceChange = (name: string) => {
581 commitConfig({
582 ...config,
583 dechirp_mode: dechirpMode,
584 dechirp_reference: {
585 source: 'custom',
586 ...(name ? { waveform_name: name } : {}),
587 },
588 });
589 };
590
591 const handleIfChainNumberChange = (
592 key: FmcwIfChainKey,
593 value: number | null
594 ) => {
595 const nextConfig: ReceiverFmcwModeConfig = {
596 ...config,
597 dechirp_mode: dechirpMode,
598 };
599 if (value === null) {
600 delete nextConfig[key];
601 } else {
602 nextConfig[key] = value;
603 }
604 commitConfig(nextConfig);
605 };
606
607 return (
608 <Section title="FMCW Receiver">
609 {referenceIssues.map((issue) => (
610 <Alert
611 key={issue.message}
612 severity={issue.severity}
613 variant="outlined"
614 >
615 {issue.message}
616 </Alert>
617 ))}
618 <FormControl fullWidth size="small">
619 <InputLabel>Dechirp Mode</InputLabel>
620 <Select
621 label="Dechirp Mode"
622 value={dechirpMode}
623 onChange={(e) =>
624 handleModeChange(e.target.value as DechirpMode)
625 }
626 >
627 {DECHIRP_MODE_OPTIONS.map((option) => (
628 <MenuItem key={option.value} value={option.value}>
629 {option.label}
630 </MenuItem>
631 ))}
632 </Select>
633 </FormControl>
634
635 {dechirpMode !== 'none' && (
636 <>
637 <FormControl fullWidth size="small">
638 <InputLabel>Dechirp Reference</InputLabel>
639 <Select
640 label="Dechirp Reference"
641 value={referenceSource}
642 onChange={(e) =>
643 handleReferenceSourceChange(
644 e.target.value as DechirpReferenceSource
645 )
646 }
647 >
648 {referenceSourceOptions.map((option) => (
649 <MenuItem
650 key={option.value}
651 value={option.value}
652 >
653 {option.label}
654 </MenuItem>
655 ))}
656 </Select>
657 </FormControl>
658
659 {referenceSource === 'transmitter' && (
660 <FormControl fullWidth size="small">
661 <InputLabel>Reference Transmitter</InputLabel>
662 <Select
663 label="Reference Transmitter"
664 value={transmitterName ?? ''}
665 onChange={(e) =>
666 handleTransmitterReferenceChange(
667 e.target.value
668 )
669 }
670 >
671 <MenuItem value="">
672 <em>None</em>
673 </MenuItem>
674 {includeCurrentName(
675 fmcwEmitterNames,
676 transmitterName
677 ).map((name) => (
678 <MenuItem key={name} value={name}>
679 {name}
680 </MenuItem>
681 ))}
682 </Select>
683 </FormControl>
684 )}
685
686 {referenceSource === 'custom' && (
687 <FormControl fullWidth size="small">
688 <InputLabel>Reference Waveform</InputLabel>
689 <Select
690 label="Reference Waveform"
691 value={waveformName ?? ''}
692 onChange={(e) =>
693 handleCustomWaveformReferenceChange(
694 e.target.value
695 )
696 }
697 >
698 <MenuItem value="">
699 <em>None</em>
700 </MenuItem>
701 {includeCurrentName(
702 fmcwWaveformNames,
703 waveformName
704 ).map((name) => (
705 <MenuItem key={name} value={name}>
706 {name}
707 </MenuItem>
708 ))}
709 </Select>
710 </FormControl>
711 )}
712
713 <NumberField
714 label="IF Sample Rate (Hz)"
715 value={config.if_sample_rate ?? null}
716 emptyBehavior="null"
717 onChange={(value) =>
718 handleIfChainNumberChange(
719 'if_sample_rate',
720 value
721 )
722 }
723 />
724 <NumberField
725 label="IF Filter Bandwidth (Hz)"
726 value={config.if_filter_bandwidth ?? null}
727 emptyBehavior="null"
728 onChange={(value) =>
729 handleIfChainNumberChange(
730 'if_filter_bandwidth',
731 value
732 )
733 }
734 />
735 <NumberField
736 label="IF Transition Width (Hz)"
737 value={config.if_filter_transition_width ?? null}
738 emptyBehavior="null"
739 onChange={(value) =>
740 handleIfChainNumberChange(
741 'if_filter_transition_width',
742 value
743 )
744 }
745 />
746 </>
747 )}
748 </Section>
749 );
750 };
751
752 const renderReceiverFields = (
753 c: MonostaticComponent | ReceiverComponent
754 ) => (
755 <>
756 {(c.radarType as RadarType) === 'pulsed' && (
757 <>
758 <NumberField
759 label="Window Skip (s)"
760 value={c.window_skip}
761 emptyBehavior="revert"
762 onChange={(v) => handleChange('window_skip', v)}
763 />
764 <NumberField
765 label="Window Length (s)"
766 value={c.window_length}
767 emptyBehavior="revert"
768 onChange={(v) => handleChange('window_length', v)}
769 />
770 </>
771 )}
772 <NumberField
773 label="Noise Temperature (K)"
774 value={c.noiseTemperature}
775 emptyBehavior="revert"
776 onChange={(v) => handleChange('noiseTemperature', v)}
777 />
778 <FormControlLabel
779 control={
780 <Checkbox
781 checked={c.noDirectPaths}
782 onChange={(e) =>
783 handleChange('noDirectPaths', e.target.checked)
784 }
785 />
786 }
787 label="Ignore Direct Paths"
788 />
789 <FormControlLabel
790 control={
791 <Checkbox
792 checked={c.noPropagationLoss}
793 onChange={(e) =>
794 handleChange('noPropagationLoss', e.target.checked)
795 }
796 />
797 }
798 label="Ignore Propagation Loss"
799 />
800 {(c.radarType as RadarType) === 'fmcw' &&
801 renderFmcwReceiverFields(c)}
802 </>
803 );
804
805 switch (component.type) {
806 case 'monostatic':
807 return (
808 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
809 {renderCommonRadarFields(component)}
810 {(component.radarType as RadarType) === 'pulsed' && (
811 <NumberField
812 label="PRF (Hz)"
813 value={component.prf}
814 emptyBehavior="revert"
815 onChange={(v) => handleChange('prf', v)}
816 />
817 )}
818 {renderReceiverFields(component)}
819 {renderSchedule(component)}
820 </Box>
821 );
822 case 'transmitter':
823 return (
824 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
825 {renderCommonRadarFields(component)}
826 {(component.radarType as RadarType) === 'pulsed' && (
827 <NumberField
828 label="PRF (Hz)"
829 value={component.prf}
830 emptyBehavior="revert"
831 onChange={(v) => handleChange('prf', v)}
832 />
833 )}
834 {renderSchedule(component)}
835 </Box>
836 );
837 case 'receiver':
838 return (
839 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
840 {renderCommonRadarFields(component)}
841 {(component.radarType as RadarType) === 'pulsed' && (
842 <NumberField
843 label="PRF (Hz)"
844 value={component.prf}
845 emptyBehavior="revert"
846 onChange={(v) => handleChange('prf', v)}
847 />
848 )}
849 {renderReceiverFields(component)}
850 {renderSchedule(component)}
851 </Box>
852 );
853 case 'target':
854 return (
855 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
856 <BufferedTextField
857 label="Component Name"
858 size="small"
859 fullWidth
860 value={component.name}
861 allowEmpty={false}
862 onChange={(v) => handleChange('name', v)}
863 />
864 <FormControl fullWidth size="small">
865 <InputLabel>RCS Type</InputLabel>
866 <Select
867 label="RCS Type"
868 value={component.rcs_type}
869 onChange={(e) =>
870 handleChange('rcs_type', e.target.value)
871 }
872 >
873 <MenuItem value="isotropic">Isotropic</MenuItem>
874 <MenuItem value="file">File</MenuItem>
875 </Select>
876 </FormControl>
877 {component.rcs_type === 'isotropic' && (
878 <NumberField
879 label="RCS Value (m^2)"
880 value={component.rcs_value ?? 0}
881 emptyBehavior="revert"
882 onChange={(v) => handleChange('rcs_value', v)}
883 />
884 )}
885 {component.rcs_type === 'file' && (
886 <FileInput
887 label="RCS File"
888 value={component.rcs_filename}
889 onChange={(v) => handleChange('rcs_filename', v)}
890 filters={[
891 {
892 name: 'Target RCS XML',
893 extensions: ['xml'],
894 },
895 ]}
896 />
897 )}
898
899 <FormControl fullWidth size="small">
900 <InputLabel>RCS Model</InputLabel>
901 <Select
902 label="RCS Model"
903 value={component.rcs_model}
904 onChange={(e) =>
905 setPlatformRcsModel(
906 platformId,
907 component.id,
908 e.target
909 .value as TargetComponent['rcs_model']
910 )
911 }
912 >
913 <MenuItem value="constant">Constant</MenuItem>
914 <MenuItem value="chisquare">Chi-Square</MenuItem>
915 <MenuItem value="gamma">Gamma</MenuItem>
916 </Select>
917 </FormControl>
918 {(component.rcs_model === 'chisquare' ||
919 component.rcs_model === 'gamma') && (
920 <NumberField
921 label="K Value"
922 value={component.rcs_k ?? 0}
923 emptyBehavior="revert"
924 onChange={(v) => handleChange('rcs_k', v)}
925 />
926 )}
927 </Box>
928 );
929 default:
930 return (
931 <Typography color="text.secondary">
932 Unknown component type.
933 </Typography>
934 );
935 }
936}