1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import { useState } from 'react';
19} from '@mui/material';
26} from '@/stores/scenarioStore';
27import { Section, NumberField } from './InspectorControls';
28import DeleteIcon from '@mui/icons-material/Delete';
29import EditIcon from '@mui/icons-material/Edit';
30import AddIcon from '@mui/icons-material/Add';
31import { v4 as uuidv4 } from 'uuid';
32import { PlatformComponentInspector } from './PlatformComponentInspector';
34interface PlatformInspectorProps {
38interface WaypointEditDialogProps {
41 finalWaypoint: PositionWaypoint | RotationWaypoint | null
43 waypoint: PositionWaypoint | RotationWaypoint | null;
44 waypointType: 'position' | 'rotation';
47function WaypointEditDialog({
52}: WaypointEditDialogProps) {
53 const [editedWaypoint, setEditedWaypoint] = useState(waypoint);
55 const handleFieldChange = (field: string, value: number | null) => {
56 setEditedWaypoint((prev) => {
57 if (!prev) return null;
58 return { ...prev, [field]: value };
62 const handleClose = () => {
63 if (!editedWaypoint) {
68 // Create a mutable copy and cast to a record for safe dynamic access.
69 const sanitizedWaypoint: Record<string, unknown> = {
73 // Sanitize the local state on close: convert any nulls back to 0.
74 for (const key in sanitizedWaypoint) {
76 Object.prototype.hasOwnProperty.call(sanitizedWaypoint, key) &&
77 sanitizedWaypoint[key] === null
79 sanitizedWaypoint[key] = 0;
82 // Cast back to the original type before calling the parent callback.
83 onClose(sanitizedWaypoint as PositionWaypoint | RotationWaypoint);
86 if (!editedWaypoint) return null;
89 <Dialog open={open} onClose={handleClose}>
90 <DialogTitle>Edit Waypoint</DialogTitle>
95 flexDirection: 'column',
100 {waypointType === 'position' && 'x' in editedWaypoint && (
104 value={editedWaypoint.x}
105 onChange={(v) => handleFieldChange('x', v)}
109 value={editedWaypoint.y}
110 onChange={(v) => handleFieldChange('y', v)}
114 value={editedWaypoint.altitude}
116 handleFieldChange('altitude', v)
121 {waypointType === 'rotation' &&
122 'azimuth' in editedWaypoint && (
125 label="Azimuth (deg)"
126 value={editedWaypoint.azimuth}
128 handleFieldChange('azimuth', v)
132 label="Elevation (deg)"
133 value={editedWaypoint.elevation}
135 handleFieldChange('elevation', v)
142 value={editedWaypoint.time}
143 onChange={(v) => handleFieldChange('time', v)}
148 <Button onClick={handleClose}>Close</Button>
154export function PlatformInspector({ item }: PlatformInspectorProps) {
158 removePositionWaypoint,
160 removeRotationWaypoint,
161 addPlatformComponent,
162 removePlatformComponent,
163 } = useScenarioStore.getState();
165 const handleChange = (path: string, value: unknown) =>
166 updateItem(item.id, path, value);
168 const allowMultiplePosWaypoints =
169 item.motionPath.interpolation !== 'static';
171 const [editingWaypointInfo, setEditingWaypointInfo] = useState<{
172 type: 'position' | 'rotation';
176 const [newComponentType, setNewComponentType] =
177 useState<PlatformComponent['type']>('monostatic');
179 const handleDialogClose = (
180 finalWaypoint: PositionWaypoint | RotationWaypoint | null
182 if (finalWaypoint && editingWaypointInfo) {
183 const { type, index } = editingWaypointInfo;
184 const pathPrefix = type === 'position' ? 'motionPath' : 'rotation';
185 handleChange(`${pathPrefix}.waypoints.${index}`, finalWaypoint);
187 setEditingWaypointInfo(null);
190 const currentEditingWaypoint = editingWaypointInfo
191 ? editingWaypointInfo.type === 'position'
192 ? item.motionPath.waypoints[editingWaypointInfo.index]
193 : item.rotation.type === 'path'
194 ? item.rotation.waypoints[editingWaypointInfo.index]
198 const handleRotationTypeChange = (newType: 'fixed' | 'path') => {
199 if (newType === 'fixed' && item.rotation.type !== 'fixed') {
200 handleChange('rotation', {
207 } else if (newType === 'path' && item.rotation.type !== 'path') {
208 handleChange('rotation', {
210 interpolation: 'static',
212 { id: uuidv4(), azimuth: 0, elevation: 0, time: 0 },
219 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
226 onChange={(e) => handleChange('name', e.target.value)}
229 <Section title="Motion Path">
230 <FormControl fullWidth size="small">
231 <InputLabel>Interpolation</InputLabel>
233 label="Interpolation"
234 value={item.motionPath.interpolation}
237 'motionPath.interpolation',
242 <MenuItem value="static">Static</MenuItem>
243 <MenuItem value="linear">Linear</MenuItem>
244 <MenuItem value="cubic">Cubic</MenuItem>
247 {item.motionPath.waypoints
250 allowMultiplePosWaypoints
251 ? item.motionPath.waypoints.length
259 alignItems: 'center',
260 justifyContent: 'space-between',
263 borderColor: 'divider',
272 whiteSpace: 'nowrap',
274 textOverflow: 'ellipsis',
277 {`T: ${wp.time}s, X: ${wp.x}, Y: ${wp.y}, Alt: ${wp.altitude}`}
279 <Box sx={{ display: 'flex', flexShrink: 0 }}>
283 setEditingWaypointInfo({
289 <EditIcon fontSize="small" />
291 {allowMultiplePosWaypoints && (
295 removePositionWaypoint(
301 item.motionPath.waypoints.length <=
305 <DeleteIcon fontSize="small" />
311 {allowMultiplePosWaypoints && (
313 onClick={() => addPositionWaypoint(item.id)}
321 <Section title="Rotation">
322 <FormControl fullWidth size="small">
323 <InputLabel>Rotation Type</InputLabel>
325 label="Rotation Type"
326 value={item.rotation.type}
328 handleRotationTypeChange(
329 e.target.value as 'fixed' | 'path'
333 <MenuItem value="fixed">Fixed Rate</MenuItem>
334 <MenuItem value="path">Waypoint Path</MenuItem>
337 {item.rotation.type === 'fixed' && (
340 label="Start Azimuth (deg)"
341 value={item.rotation.startAzimuth}
343 handleChange('rotation.startAzimuth', v)
347 label="Start Elevation (deg)"
348 value={item.rotation.startElevation}
350 handleChange('rotation.startElevation', v)
354 label="Azimuth Rate (deg/s)"
355 value={item.rotation.azimuthRate}
357 handleChange('rotation.azimuthRate', v)
361 label="Elevation Rate (deg/s)"
362 value={item.rotation.elevationRate}
364 handleChange('rotation.elevationRate', v)
369 {item.rotation.type === 'path' &&
371 const rotation = item.rotation;
372 const allowMultipleWaypoints =
373 rotation.interpolation !== 'static';
376 <FormControl fullWidth size="small">
377 <InputLabel>Interpolation</InputLabel>
379 label="Interpolation"
380 value={rotation.interpolation}
383 'rotation.interpolation',
388 <MenuItem value="static">
391 <MenuItem value="linear">
394 <MenuItem value="cubic">Cubic</MenuItem>
400 allowMultipleWaypoints
401 ? rotation.waypoints.length
409 alignItems: 'center',
410 justifyContent: 'space-between',
413 borderColor: 'divider',
422 whiteSpace: 'nowrap',
424 textOverflow: 'ellipsis',
427 {`T: ${wp.time}s, Az: ${wp.azimuth}°, El: ${wp.elevation}°`}
438 setEditingWaypointInfo({
444 <EditIcon fontSize="small" />
446 {allowMultipleWaypoints && (
450 removeRotationWaypoint(
460 <DeleteIcon fontSize="small" />
466 {allowMultipleWaypoints && (
469 addRotationWaypoint(item.id)
481 <Section title="Components">
482 {item.components.map((comp, index) => (
487 borderColor: 'divider',
491 backgroundColor: 'background.default',
497 justifyContent: 'space-between',
498 alignItems: 'center',
502 <Typography variant="subtitle2" color="primary">
503 {comp.type.toUpperCase()}
509 removePlatformComponent(item.id, comp.id)
512 <DeleteIcon fontSize="small" />
515 <PlatformComponentInspector
527 alignItems: 'center',
531 <FormControl size="small" sx={{ flexGrow: 1 }}>
532 <InputLabel>New Component</InputLabel>
534 label="New Component"
535 value={newComponentType}
538 e.target.value as PlatformComponent['type']
542 <MenuItem value="monostatic">
545 <MenuItem value="transmitter">Transmitter</MenuItem>
546 <MenuItem value="receiver">Receiver</MenuItem>
547 <MenuItem value="target">Target</MenuItem>
553 addPlatformComponent(item.id, newComponentType)
555 startIcon={<AddIcon />}
565 ? `${editingWaypointInfo.type}-${editingWaypointInfo.index}`
568 open={!!editingWaypointInfo}
569 onClose={handleDialogClose}
570 waypoint={currentEditingWaypoint}
571 waypointType={editingWaypointInfo?.type ?? 'position'}