1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import AddIcon from '@mui/icons-material/Add';
5import DeleteIcon from '@mui/icons-material/Delete';
6import EditIcon from '@mui/icons-material/Edit';
21} from '@mui/material';
22import { useState } from 'react';
29} from '@/stores/scenarioStore';
30import { generateSimId } from '@/stores/scenarioStore/idUtils';
31import { NumberField, Section } from './InspectorControls';
32import { PlatformComponentInspector } from './PlatformComponentInspector';
34interface PlatformInspectorProps {
36 selectedComponentId: string | null;
39interface WaypointEditDialogProps {
42 finalWaypoint: PositionWaypoint | RotationWaypoint | null
44 waypoint: PositionWaypoint | RotationWaypoint | null;
45 waypointType: 'position' | 'rotation';
46 angleUnitLabel: 'deg' | 'rad';
49function WaypointEditDialog({
55}: WaypointEditDialogProps) {
56 const [editedWaypoint, setEditedWaypoint] = useState(waypoint);
58 const handleFieldChange = (field: string, value: number | null) => {
59 setEditedWaypoint((prev) => {
60 if (!prev) return null;
61 return { ...prev, [field]: value };
65 const handleClose = () => {
66 if (!editedWaypoint) {
71 // Create a mutable copy and cast to a record for safe dynamic access.
72 const sanitizedWaypoint: Record<string, unknown> = {
76 // Sanitize the local state on close: convert any nulls back to 0.
77 for (const key in sanitizedWaypoint) {
79 Object.hasOwn(sanitizedWaypoint, key) &&
80 sanitizedWaypoint[key] === null
82 sanitizedWaypoint[key] = 0;
85 // Cast back to the original type before calling the parent callback.
86 onClose(sanitizedWaypoint as PositionWaypoint | RotationWaypoint);
89 if (!editedWaypoint) return null;
92 <Dialog open={open} onClose={handleClose}>
93 <DialogTitle>Edit Waypoint</DialogTitle>
98 flexDirection: 'column',
103 {waypointType === 'position' && 'x' in editedWaypoint && (
107 value={editedWaypoint.x}
108 onChange={(v) => handleFieldChange('x', v)}
112 value={editedWaypoint.y}
113 onChange={(v) => handleFieldChange('y', v)}
117 value={editedWaypoint.altitude}
119 handleFieldChange('altitude', v)
124 {waypointType === 'rotation' &&
125 'azimuth' in editedWaypoint && (
128 label={`Azimuth (${angleUnitLabel})`}
129 value={editedWaypoint.azimuth}
131 handleFieldChange('azimuth', v)
135 label={`Elevation (${angleUnitLabel})`}
136 value={editedWaypoint.elevation}
138 handleFieldChange('elevation', v)
145 value={editedWaypoint.time}
146 onChange={(v) => handleFieldChange('time', v)}
151 <Button onClick={handleClose}>Close</Button>
157export function PlatformInspector({
160}: PlatformInspectorProps) {
164 removePositionWaypoint,
166 removeRotationWaypoint,
167 addPlatformComponent,
168 removePlatformComponent,
169 } = useScenarioStore.getState();
170 const angleUnitLabel = useScenarioStore(
171 (state) => state.globalParameters.rotationAngleUnit
174 const handleChange = (path: string, value: unknown) =>
175 updateItem(item.id, path, value);
177 const allowMultiplePosWaypoints =
178 item.motionPath.interpolation !== 'static';
180 const [editingWaypointInfo, setEditingWaypointInfo] = useState<{
181 type: 'position' | 'rotation';
185 const [newComponentType, setNewComponentType] =
186 useState<PlatformComponent['type']>('monostatic');
188 const handleDialogClose = (
189 finalWaypoint: PositionWaypoint | RotationWaypoint | null
191 if (finalWaypoint && editingWaypointInfo) {
192 const { type, index } = editingWaypointInfo;
193 const pathPrefix = type === 'position' ? 'motionPath' : 'rotation';
194 handleChange(`${pathPrefix}.waypoints.${index}`, finalWaypoint);
196 setEditingWaypointInfo(null);
199 const currentEditingWaypoint = editingWaypointInfo
200 ? editingWaypointInfo.type === 'position'
201 ? item.motionPath.waypoints[editingWaypointInfo.index]
202 : item.rotation.type === 'path'
203 ? item.rotation.waypoints[editingWaypointInfo.index]
207 const handleRotationTypeChange = (newType: 'fixed' | 'path') => {
208 if (newType === 'fixed' && item.rotation.type !== 'fixed') {
209 handleChange('rotation', {
216 } else if (newType === 'path' && item.rotation.type !== 'path') {
217 handleChange('rotation', {
219 interpolation: 'static',
222 id: generateSimId('Platform'),
233 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
240 onChange={(e) => handleChange('name', e.target.value)}
243 <Section title="Motion Path">
244 <FormControl fullWidth size="small">
245 <InputLabel>Interpolation</InputLabel>
247 label="Interpolation"
248 value={item.motionPath.interpolation}
251 'motionPath.interpolation',
256 <MenuItem value="static">Static</MenuItem>
257 <MenuItem value="linear">Linear</MenuItem>
258 <MenuItem value="cubic">Cubic</MenuItem>
261 {item.motionPath.waypoints
264 allowMultiplePosWaypoints
265 ? item.motionPath.waypoints.length
273 alignItems: 'center',
274 justifyContent: 'space-between',
277 borderColor: 'divider',
286 whiteSpace: 'nowrap',
288 textOverflow: 'ellipsis',
291 {`T: ${wp.time}s, X: ${wp.x}, Y: ${wp.y}, Alt: ${wp.altitude}`}
293 <Box sx={{ display: 'flex', flexShrink: 0 }}>
297 setEditingWaypointInfo({
303 <EditIcon fontSize="small" />
305 {allowMultiplePosWaypoints && (
309 removePositionWaypoint(
315 item.motionPath.waypoints.length <=
319 <DeleteIcon fontSize="small" />
325 {allowMultiplePosWaypoints && (
327 onClick={() => addPositionWaypoint(item.id)}
335 <Section title="Rotation">
336 <FormControl fullWidth size="small">
337 <InputLabel>Rotation Type</InputLabel>
339 label="Rotation Type"
340 value={item.rotation.type}
342 handleRotationTypeChange(
343 e.target.value as 'fixed' | 'path'
347 <MenuItem value="fixed">Fixed Rate</MenuItem>
348 <MenuItem value="path">Waypoint Path</MenuItem>
351 {item.rotation.type === 'fixed' && (
354 label={`Start Azimuth (${angleUnitLabel})`}
355 value={item.rotation.startAzimuth}
357 handleChange('rotation.startAzimuth', v)
361 label={`Start Elevation (${angleUnitLabel})`}
362 value={item.rotation.startElevation}
364 handleChange('rotation.startElevation', v)
368 label={`Azimuth Rate (${angleUnitLabel}/s)`}
369 value={item.rotation.azimuthRate}
371 handleChange('rotation.azimuthRate', v)
375 label={`Elevation Rate (${angleUnitLabel}/s)`}
376 value={item.rotation.elevationRate}
378 handleChange('rotation.elevationRate', v)
383 {item.rotation.type === 'path' &&
385 const rotation = item.rotation;
386 const allowMultipleWaypoints =
387 rotation.interpolation !== 'static';
390 <FormControl fullWidth size="small">
391 <InputLabel>Interpolation</InputLabel>
393 label="Interpolation"
394 value={rotation.interpolation}
397 'rotation.interpolation',
402 <MenuItem value="static">
405 <MenuItem value="linear">
408 <MenuItem value="cubic">Cubic</MenuItem>
414 allowMultipleWaypoints
415 ? rotation.waypoints.length
423 alignItems: 'center',
424 justifyContent: 'space-between',
427 borderColor: 'divider',
436 whiteSpace: 'nowrap',
438 textOverflow: 'ellipsis',
441 {`T: ${wp.time}s, Az: ${wp.azimuth} ${angleUnitLabel}, El: ${wp.elevation} ${angleUnitLabel}`}
452 setEditingWaypointInfo({
458 <EditIcon fontSize="small" />
460 {allowMultipleWaypoints && (
464 removeRotationWaypoint(
474 <DeleteIcon fontSize="small" />
480 {allowMultipleWaypoints && (
483 addRotationWaypoint(item.id)
495 <Section title="Components">
496 {item.components.map((comp, index) => (
501 borderColor: 'divider',
505 backgroundColor: 'background.default',
511 justifyContent: 'space-between',
512 alignItems: 'center',
516 <Typography variant="subtitle2" color="primary">
517 {comp.type.toUpperCase()}
518 {selectedComponentId === comp.id &&
525 removePlatformComponent(item.id, comp.id)
528 <DeleteIcon fontSize="small" />
531 <PlatformComponentInspector
543 alignItems: 'center',
547 <FormControl size="small" sx={{ flexGrow: 1 }}>
548 <InputLabel>New Component</InputLabel>
550 label="New Component"
551 value={newComponentType}
554 e.target.value as PlatformComponent['type']
558 <MenuItem value="monostatic">
561 <MenuItem value="transmitter">Transmitter</MenuItem>
562 <MenuItem value="receiver">Receiver</MenuItem>
563 <MenuItem value="target">Target</MenuItem>
569 addPlatformComponent(item.id, newComponentType)
571 startIcon={<AddIcon />}
581 ? `${editingWaypointInfo.type}-${editingWaypointInfo.index}`
584 open={!!editingWaypointInfo}
585 onClose={handleDialogClose}
586 waypoint={currentEditingWaypoint}
587 waypointType={editingWaypointInfo?.type ?? 'position'}
588 angleUnitLabel={angleUnitLabel}