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