FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
ScenarioView.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 React, { useRef, useState } from 'react';
5import { Box, IconButton, Tooltip } from '@mui/material';
6import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
7import { Canvas } from '@react-three/fiber';
8import {
9 Panel,
10 Group,
11 Separator,
12 type GroupImperativeHandle,
13} from 'react-resizable-panels';
14import WorldView from '@/components/WorldView';
15import SceneTree from '@/components/SceneTree';
16import PropertyInspector from '@/components/PropertyInspector';
17import Timeline from '@/components/Timeline';
18import ViewControls from '@/components/ViewControls';
19import { type MapControls as MapControlsImpl } from 'three-stdlib';
20import { ScaleManager } from '@/components/ScaleManager';
21import { useScenarioStore } from '@/stores/scenarioStore';
22import { fersColors } from '@/theme';
23
24/**
25 * A styled resize handle for the resizable panels.
26 */
27function ResizeHandle() {
28 return (
29 <Separator>
30 <Box
31 sx={{
32 width: '2px',
33 height: '100%',
34 backgroundColor: 'divider',
35 transition: 'background-color 0.2s ease-in-out',
36 '&[data-resize-handle-state="drag"]': {
37 backgroundColor: 'primary.main',
38 },
39 '&:hover': {
40 backgroundColor: 'primary.light',
41 },
42 }}
43 />
44 </Separator>
45 );
46}
47
48/**
49 * ScenarioView is the primary workbench for building and visualizing 3D scenes.
50 */
51export const ScenarioView = React.memo(function ScenarioView() {
52 const isSimulating = useScenarioStore((state) => state.isSimulating);
53 const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
54 const panelGroupRef = useRef<GroupImperativeHandle>(null);
55
56 // 1. Lift refs to this level to bridge Logic (Canvas) and UI (DOM)
57 const controlsRef = useRef<MapControlsImpl>(null);
58 const scaleLabelRef = useRef<HTMLDivElement>(null);
59 const scaleBarRef = useRef<HTMLDivElement>(null);
60
61 const handleExpandInspector = () => {
62 // Restore panels to their default sizes: [SceneTree, Main, Inspector]
63 panelGroupRef.current?.setLayout({
64 'scene-tree': 25,
65 'main-content': 50,
66 'property-inspector': 25,
67 });
68 };
69
70 return (
71 <Box
72 sx={{
73 height: '100%',
74 width: '100%',
75 overflow: 'hidden',
76 position: 'relative', // Establish positioning context
77 pointerEvents: isSimulating ? 'none' : 'auto',
78 opacity: isSimulating ? 0.5 : 1,
79 transition: 'opacity 0.3s ease-in-out',
80 userSelect: 'none',
81 WebkitUserSelect: 'none',
82 }}
83 >
84 <Group
85 orientation="horizontal"
86 groupRef={panelGroupRef}
87 defaultLayout={{
88 'scene-tree': 25,
89 'main-content': 50,
90 'property-inspector': 25,
91 }}
92 style={{ height: '100%', width: '100%' }}
93 >
94 <Panel id="scene-tree" defaultSize={25} minSize={20}>
95 <SceneTree />
96 </Panel>
97
98 <ResizeHandle />
99
100 <Panel id="main-content" minSize={30}>
101 <Box
102 sx={{
103 height: '100%',
104 display: 'flex',
105 flexDirection: 'column',
106 minWidth: 0, // Allow flex item to shrink below content size
107 overflow: 'hidden', // Prevent overflow
108 }}
109 >
110 <Box
111 sx={{
112 flex: 1,
113 position: 'relative',
114 minHeight: 0, // Allow flex item to shrink
115 overflow: 'hidden',
116 userSelect: 'none',
117 }}
118 >
119 <Canvas
120 shadows
121 camera={{
122 position: [100, 100, 100],
123 fov: 25,
124 near: 0.1,
125 far: 1e8,
126 }}
127 gl={{
128 logarithmicDepthBuffer: true,
129 }}
130 >
131 <color
132 attach="background"
133 args={[fersColors.background.canvas]}
134 />
135 {/* Pass controlsRef down to WorldView */}
136 <WorldView controlsRef={controlsRef} />
137
138 {/* Logic-only component for Scale Bar calculations */}
139 <ScaleManager
140 controlsRef={controlsRef}
141 labelRef={scaleLabelRef}
142 barRef={scaleBarRef}
143 />
144 </Canvas>
145
146 {/* View Controls (Top-Left) */}
147 <Box
148 sx={{
149 position: 'absolute',
150 top: 16,
151 left: 16,
152 zIndex: 1000,
153 }}
154 >
155 <ViewControls />
156 </Box>
157
158 {/* Scale Bar Overlay (Bottom-Left) */}
159 <Box
160 sx={{
161 position: 'absolute',
162 bottom: 16,
163 left: 16,
164 zIndex: 1000,
165 pointerEvents: 'none',
166 display: 'flex',
167 flexDirection: 'column',
168 alignItems: 'center',
169 textShadow: '0px 1px 2px rgba(0,0,0,0.8)',
170 }}
171 >
172 <div
173 ref={scaleLabelRef}
174 style={{
175 color: fersColors.text.primary,
176 fontSize: '11px',
177 fontWeight: 500,
178 marginBottom: '4px',
179 fontFamily: 'Roboto, sans-serif',
180 }}
181 >
182 -- m
183 </div>
184 <div
185 ref={scaleBarRef}
186 style={{
187 height: '6px',
188 border: `1px solid ${fersColors.text.primary}`,
189 borderTop: 'none',
190 width: '100px',
191 }}
192 />
193 </Box>
194 </Box>
195 <Box
196 sx={{
197 height: 100,
198 flexShrink: 0,
199 borderTop: 1,
200 borderColor: 'divider',
201 }}
202 >
203 <Timeline />
204 </Box>
205 </Box>
206 </Panel>
207
208 <ResizeHandle />
209
210 <Panel
211 id="property-inspector"
212 collapsible
213 collapsedSize={0}
214 onResize={(size) =>
215 setIsInspectorCollapsed(size.asPercentage === 0)
216 }
217 defaultSize={25}
218 minSize={5}
219 >
220 <PropertyInspector />
221 </Panel>
222 </Group>
223
224 {isInspectorCollapsed && (
225 <Tooltip title="Show Properties">
226 <IconButton
227 onClick={handleExpandInspector}
228 sx={{
229 position: 'absolute',
230 right: 0,
231 top: '50%',
232 transform: 'translateY(-50%)',
233 zIndex: 1,
234 bgcolor: 'background.paper',
235 border: 1,
236 borderColor: 'divider',
237 borderRight: 'none',
238 borderTopRightRadius: 0,
239 borderBottomRightRadius: 0,
240 '&:hover': {
241 bgcolor: 'action.hover',
242 },
243 }}
244 >
245 <ChevronLeftIcon />
246 </IconButton>
247 </Tooltip>
248 )}
249 </Box>
250 );
251});