lsurf 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lsurf/__init__.py +471 -0
- lsurf/analysis/__init__.py +107 -0
- lsurf/analysis/healpix_utils.py +418 -0
- lsurf/analysis/sphere_viz.py +1280 -0
- lsurf/cli/__init__.py +48 -0
- lsurf/cli/build.py +398 -0
- lsurf/cli/config_schema.py +318 -0
- lsurf/cli/gui_cmd.py +76 -0
- lsurf/cli/interactive.py +850 -0
- lsurf/cli/main.py +81 -0
- lsurf/cli/run.py +806 -0
- lsurf/detectors/__init__.py +266 -0
- lsurf/detectors/analysis.py +289 -0
- lsurf/detectors/base.py +284 -0
- lsurf/detectors/constant_size_rings.py +485 -0
- lsurf/detectors/directional.py +45 -0
- lsurf/detectors/extended/__init__.py +73 -0
- lsurf/detectors/extended/local_sphere.py +353 -0
- lsurf/detectors/extended/recording_sphere.py +368 -0
- lsurf/detectors/planar.py +45 -0
- lsurf/detectors/protocol.py +187 -0
- lsurf/detectors/recording_spheres.py +63 -0
- lsurf/detectors/results.py +1140 -0
- lsurf/detectors/small/__init__.py +79 -0
- lsurf/detectors/small/directional.py +330 -0
- lsurf/detectors/small/planar.py +401 -0
- lsurf/detectors/small/spherical.py +450 -0
- lsurf/detectors/spherical.py +45 -0
- lsurf/geometry/__init__.py +199 -0
- lsurf/geometry/builder.py +478 -0
- lsurf/geometry/cell.py +228 -0
- lsurf/geometry/cell_geometry.py +247 -0
- lsurf/geometry/detector_arrays.py +1785 -0
- lsurf/geometry/geometry.py +222 -0
- lsurf/geometry/surface_analysis.py +375 -0
- lsurf/geometry/validation.py +91 -0
- lsurf/gui/__init__.py +51 -0
- lsurf/gui/app.py +903 -0
- lsurf/gui/core/__init__.py +39 -0
- lsurf/gui/core/scene.py +343 -0
- lsurf/gui/core/simulation.py +264 -0
- lsurf/gui/renderers/__init__.py +40 -0
- lsurf/gui/renderers/ray_renderer.py +353 -0
- lsurf/gui/renderers/source_renderer.py +505 -0
- lsurf/gui/renderers/surface_renderer.py +477 -0
- lsurf/gui/views/__init__.py +48 -0
- lsurf/gui/views/config_editor.py +3199 -0
- lsurf/gui/views/properties.py +257 -0
- lsurf/gui/views/results.py +291 -0
- lsurf/gui/views/scene_tree.py +180 -0
- lsurf/gui/views/viewport_3d.py +555 -0
- lsurf/gui/views/visualizations.py +712 -0
- lsurf/materials/__init__.py +169 -0
- lsurf/materials/base/__init__.py +64 -0
- lsurf/materials/base/full_inhomogeneous.py +208 -0
- lsurf/materials/base/grid_inhomogeneous.py +319 -0
- lsurf/materials/base/homogeneous.py +342 -0
- lsurf/materials/base/material_field.py +527 -0
- lsurf/materials/base/simple_inhomogeneous.py +418 -0
- lsurf/materials/base/spectral_inhomogeneous.py +497 -0
- lsurf/materials/implementations/__init__.py +120 -0
- lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
- lsurf/materials/implementations/duct_atmosphere.py +390 -0
- lsurf/materials/implementations/exponential_atmosphere.py +435 -0
- lsurf/materials/implementations/gaussian_lens.py +120 -0
- lsurf/materials/implementations/interpolated_data.py +123 -0
- lsurf/materials/implementations/layered_atmosphere.py +134 -0
- lsurf/materials/implementations/linear_gradient.py +109 -0
- lsurf/materials/implementations/linsley_atmosphere.py +764 -0
- lsurf/materials/implementations/standard_materials.py +126 -0
- lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
- lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
- lsurf/materials/utils/__init__.py +77 -0
- lsurf/materials/utils/constants.py +45 -0
- lsurf/materials/utils/device_functions.py +117 -0
- lsurf/materials/utils/dispersion.py +160 -0
- lsurf/materials/utils/factories.py +142 -0
- lsurf/propagation/__init__.py +91 -0
- lsurf/propagation/detector_gpu.py +67 -0
- lsurf/propagation/gpu_device_rays.py +294 -0
- lsurf/propagation/kernels/__init__.py +175 -0
- lsurf/propagation/kernels/absorption/__init__.py +61 -0
- lsurf/propagation/kernels/absorption/grid.py +240 -0
- lsurf/propagation/kernels/absorption/simple.py +232 -0
- lsurf/propagation/kernels/absorption/spectral.py +410 -0
- lsurf/propagation/kernels/detection/__init__.py +64 -0
- lsurf/propagation/kernels/detection/protocol.py +102 -0
- lsurf/propagation/kernels/detection/spherical.py +255 -0
- lsurf/propagation/kernels/device_functions.py +790 -0
- lsurf/propagation/kernels/fresnel/__init__.py +64 -0
- lsurf/propagation/kernels/fresnel/protocol.py +97 -0
- lsurf/propagation/kernels/fresnel/standard.py +258 -0
- lsurf/propagation/kernels/intersection/__init__.py +79 -0
- lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
- lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
- lsurf/propagation/kernels/intersection/plane.py +166 -0
- lsurf/propagation/kernels/intersection/protocol.py +95 -0
- lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
- lsurf/propagation/kernels/intersection/sphere.py +190 -0
- lsurf/propagation/kernels/propagation/__init__.py +85 -0
- lsurf/propagation/kernels/propagation/grid.py +527 -0
- lsurf/propagation/kernels/propagation/protocol.py +105 -0
- lsurf/propagation/kernels/propagation/simple.py +460 -0
- lsurf/propagation/kernels/propagation/spectral.py +875 -0
- lsurf/propagation/kernels/registry.py +331 -0
- lsurf/propagation/kernels/surface/__init__.py +72 -0
- lsurf/propagation/kernels/surface/bisection.py +232 -0
- lsurf/propagation/kernels/surface/detection.py +402 -0
- lsurf/propagation/kernels/surface/reduction.py +166 -0
- lsurf/propagation/propagator_protocol.py +222 -0
- lsurf/propagation/propagators/__init__.py +101 -0
- lsurf/propagation/propagators/detector_handler.py +354 -0
- lsurf/propagation/propagators/factory.py +200 -0
- lsurf/propagation/propagators/fresnel_handler.py +305 -0
- lsurf/propagation/propagators/gpu_gradient.py +566 -0
- lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
- lsurf/propagation/propagators/gradient.py +429 -0
- lsurf/propagation/propagators/intersection_handler.py +327 -0
- lsurf/propagation/propagators/material_propagator.py +398 -0
- lsurf/propagation/propagators/signed_distance_handler.py +522 -0
- lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
- lsurf/propagation/propagators/surface_interaction.py +616 -0
- lsurf/propagation/propagators/surface_propagator.py +719 -0
- lsurf/py.typed +1 -0
- lsurf/simulation/__init__.py +70 -0
- lsurf/simulation/config.py +164 -0
- lsurf/simulation/orchestrator.py +462 -0
- lsurf/simulation/result.py +299 -0
- lsurf/simulation/simulation.py +262 -0
- lsurf/sources/__init__.py +128 -0
- lsurf/sources/base.py +264 -0
- lsurf/sources/collimated.py +252 -0
- lsurf/sources/custom.py +409 -0
- lsurf/sources/diverging.py +228 -0
- lsurf/sources/gaussian.py +272 -0
- lsurf/sources/parallel_from_positions.py +197 -0
- lsurf/sources/point.py +172 -0
- lsurf/sources/uniform_diverging.py +258 -0
- lsurf/surfaces/__init__.py +184 -0
- lsurf/surfaces/cpu/__init__.py +50 -0
- lsurf/surfaces/cpu/curved_wave.py +463 -0
- lsurf/surfaces/cpu/gerstner_wave.py +381 -0
- lsurf/surfaces/cpu/wave_params.py +118 -0
- lsurf/surfaces/gpu/__init__.py +72 -0
- lsurf/surfaces/gpu/annular_plane.py +453 -0
- lsurf/surfaces/gpu/bounded_plane.py +390 -0
- lsurf/surfaces/gpu/curved_wave.py +483 -0
- lsurf/surfaces/gpu/gerstner_wave.py +377 -0
- lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
- lsurf/surfaces/gpu/plane.py +299 -0
- lsurf/surfaces/gpu/recording_sphere.py +587 -0
- lsurf/surfaces/gpu/sphere.py +311 -0
- lsurf/surfaces/protocol.py +336 -0
- lsurf/surfaces/registry.py +373 -0
- lsurf/utilities/__init__.py +175 -0
- lsurf/utilities/detector_analysis.py +814 -0
- lsurf/utilities/fresnel.py +628 -0
- lsurf/utilities/interactions.py +1215 -0
- lsurf/utilities/propagation.py +602 -0
- lsurf/utilities/ray_data.py +532 -0
- lsurf/utilities/recording_sphere.py +745 -0
- lsurf/utilities/time_spread.py +463 -0
- lsurf/visualization/__init__.py +329 -0
- lsurf/visualization/absorption_plots.py +334 -0
- lsurf/visualization/atmospheric_plots.py +754 -0
- lsurf/visualization/common.py +348 -0
- lsurf/visualization/detector_plots.py +1350 -0
- lsurf/visualization/detector_sphere_plots.py +1173 -0
- lsurf/visualization/fresnel_plots.py +1061 -0
- lsurf/visualization/ocean_simulation_plots.py +999 -0
- lsurf/visualization/polarization_plots.py +916 -0
- lsurf/visualization/raytracing_plots.py +1521 -0
- lsurf/visualization/ring_detector_plots.py +1867 -0
- lsurf/visualization/time_spread_plots.py +531 -0
- lsurf-1.0.0.dist-info/METADATA +381 -0
- lsurf-1.0.0.dist-info/RECORD +180 -0
- lsurf-1.0.0.dist-info/WHEEL +5 -0
- lsurf-1.0.0.dist-info/entry_points.txt +2 -0
- lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
- lsurf-1.0.0.dist-info/top_level.txt +1 -0
lsurf/gui/app.py
ADDED
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
# The Clear BSD License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2026 Tobias Heibges
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted (subject to the limitations in the disclaimer
|
|
8
|
+
# below) provided that the following conditions are met:
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions of source code must retain the above copyright notice,
|
|
11
|
+
# this list of conditions and the following disclaimer.
|
|
12
|
+
#
|
|
13
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
14
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
15
|
+
# documentation and/or other materials provided with the distribution.
|
|
16
|
+
#
|
|
17
|
+
# * Neither the name of the copyright holder nor the names of its
|
|
18
|
+
# contributors may be used to endorse or promote products derived from this
|
|
19
|
+
# software without specific prior written permission.
|
|
20
|
+
#
|
|
21
|
+
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
|
22
|
+
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
23
|
+
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
24
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
25
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
26
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
27
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
28
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
29
|
+
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
30
|
+
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
31
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
32
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
34
|
+
"""L-SURF GUI Application - Main entry point.
|
|
35
|
+
|
|
36
|
+
Provides the main window, menu bar, and coordinates all GUI components.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import TYPE_CHECKING
|
|
41
|
+
|
|
42
|
+
import dearpygui.dearpygui as dpg
|
|
43
|
+
|
|
44
|
+
# Lazy imports - these are loaded after splash screen is shown
|
|
45
|
+
Scene = None
|
|
46
|
+
SimulationRunner = None
|
|
47
|
+
SimulationState = None
|
|
48
|
+
ConfigEditorPanel = None
|
|
49
|
+
ResultsPanel = None
|
|
50
|
+
SceneTreePanel = None
|
|
51
|
+
Viewport3D = None
|
|
52
|
+
VisualizationPanel = None
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from lsurf.geometry import Geometry
|
|
56
|
+
from lsurf.simulation import SimulationConfig
|
|
57
|
+
from lsurf.sources import RaySource
|
|
58
|
+
|
|
59
|
+
from .core.simulation import SimulationProgress
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_components() -> None:
|
|
63
|
+
"""Load heavy components after splash screen is shown."""
|
|
64
|
+
global Scene, SimulationRunner, SimulationState
|
|
65
|
+
global ConfigEditorPanel, ResultsPanel, SceneTreePanel, Viewport3D, VisualizationPanel
|
|
66
|
+
|
|
67
|
+
from .core.scene import Scene
|
|
68
|
+
from .core.simulation import SimulationRunner, SimulationState
|
|
69
|
+
from .views.config_editor import ConfigEditorPanel
|
|
70
|
+
from .views.results import ResultsPanel
|
|
71
|
+
from .views.scene_tree import SceneTreePanel
|
|
72
|
+
from .views.viewport_3d import Viewport3D
|
|
73
|
+
from .views.visualizations import VisualizationPanel
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class LSurfApp:
|
|
77
|
+
"""Main L-SURF GUI application."""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
self.scene = Scene()
|
|
81
|
+
self.simulation_runner = SimulationRunner()
|
|
82
|
+
|
|
83
|
+
# View components
|
|
84
|
+
self.viewport_3d: Viewport3D | None = None
|
|
85
|
+
self.scene_tree: SceneTreePanel | None = None
|
|
86
|
+
self.results: ResultsPanel | None = None
|
|
87
|
+
self.config_editor: ConfigEditorPanel | None = None
|
|
88
|
+
self.visualization: VisualizationPanel | None = None
|
|
89
|
+
self._showing_visualizations: bool = False
|
|
90
|
+
|
|
91
|
+
# Configuration state
|
|
92
|
+
self._config_path: Path | None = None
|
|
93
|
+
self._geometry: "Geometry | None" = None
|
|
94
|
+
self._source: "RaySource | None" = None
|
|
95
|
+
self._sim_config: "SimulationConfig | None" = None
|
|
96
|
+
|
|
97
|
+
# Font scaling
|
|
98
|
+
self._font_scale: float = 1.0 # Starting scale
|
|
99
|
+
self._min_font_scale: float = 0.5
|
|
100
|
+
self._max_font_scale: float = 5.0
|
|
101
|
+
self._font_scale_step: float = 0.25
|
|
102
|
+
|
|
103
|
+
# Register simulation callbacks
|
|
104
|
+
self.simulation_runner.on_progress(self._on_simulation_progress)
|
|
105
|
+
self.simulation_runner.on_complete(self._on_simulation_complete)
|
|
106
|
+
|
|
107
|
+
def setup(self) -> None:
|
|
108
|
+
"""Set up Dear PyGui context and create the main window."""
|
|
109
|
+
dpg.create_context()
|
|
110
|
+
|
|
111
|
+
# Configure viewport (resizable window)
|
|
112
|
+
# Panel widths: left=280, right=480, center fills remaining
|
|
113
|
+
# Total fixed = 280 + 480 = 760, plus ~40 for borders/padding
|
|
114
|
+
dpg.create_viewport(
|
|
115
|
+
title="L-SURF - GPU Ray Tracing Visualization",
|
|
116
|
+
width=1920,
|
|
117
|
+
height=1080,
|
|
118
|
+
resizable=True,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Load modern font
|
|
122
|
+
self._setup_font()
|
|
123
|
+
|
|
124
|
+
# Set dark theme
|
|
125
|
+
self._setup_theme()
|
|
126
|
+
|
|
127
|
+
# Create main window
|
|
128
|
+
self._create_main_window()
|
|
129
|
+
|
|
130
|
+
# Setup and show viewport
|
|
131
|
+
dpg.setup_dearpygui()
|
|
132
|
+
dpg.show_viewport()
|
|
133
|
+
|
|
134
|
+
# Set viewport resize callback to update main window
|
|
135
|
+
dpg.set_viewport_resize_callback(self._on_viewport_resize)
|
|
136
|
+
|
|
137
|
+
# Force initial resize to fill viewport properly
|
|
138
|
+
self._on_viewport_resize()
|
|
139
|
+
|
|
140
|
+
def _on_viewport_resize(self) -> None:
|
|
141
|
+
"""Handle viewport resize - update main window and panels to fill viewport."""
|
|
142
|
+
width = dpg.get_viewport_client_width()
|
|
143
|
+
height = dpg.get_viewport_client_height()
|
|
144
|
+
if width > 0 and height > 0:
|
|
145
|
+
dpg.configure_item("main_window", width=width, height=height)
|
|
146
|
+
|
|
147
|
+
# Resize panels proportionally (left ~13%, right ~28%, center fills rest)
|
|
148
|
+
left_width = max(150, int(width * 0.13))
|
|
149
|
+
right_width = max(520, int(width * 0.28))
|
|
150
|
+
|
|
151
|
+
if dpg.does_item_exist("left_panel"):
|
|
152
|
+
dpg.configure_item("left_panel", width=left_width)
|
|
153
|
+
if dpg.does_item_exist("right_panel"):
|
|
154
|
+
dpg.configure_item("right_panel", width=right_width)
|
|
155
|
+
if dpg.does_item_exist("center_panel"):
|
|
156
|
+
# Center panel fills remaining space (negative width relative to right panel)
|
|
157
|
+
dpg.configure_item("center_panel", width=-right_width)
|
|
158
|
+
|
|
159
|
+
# Force viewport refresh to update drawlist size
|
|
160
|
+
if self.viewport_3d:
|
|
161
|
+
self.viewport_3d._size_printed = False # Reset to re-print size
|
|
162
|
+
self.viewport_3d.refresh()
|
|
163
|
+
|
|
164
|
+
def _setup_font(self) -> None:
|
|
165
|
+
"""Set up a modern font for the UI."""
|
|
166
|
+
import platform
|
|
167
|
+
|
|
168
|
+
# Try to find a modern sans-serif font
|
|
169
|
+
font_paths = []
|
|
170
|
+
system = platform.system()
|
|
171
|
+
|
|
172
|
+
if system == "Linux":
|
|
173
|
+
font_paths = [
|
|
174
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
175
|
+
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
|
176
|
+
"/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf",
|
|
177
|
+
"/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf",
|
|
178
|
+
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
|
179
|
+
"/usr/share/fonts/google-noto/NotoSans-Regular.ttf",
|
|
180
|
+
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
|
|
181
|
+
]
|
|
182
|
+
elif system == "Darwin": # macOS
|
|
183
|
+
font_paths = [
|
|
184
|
+
"/System/Library/Fonts/SFNSText.ttf",
|
|
185
|
+
"/System/Library/Fonts/Helvetica.ttc",
|
|
186
|
+
"/Library/Fonts/Arial.ttf",
|
|
187
|
+
]
|
|
188
|
+
elif system == "Windows":
|
|
189
|
+
font_paths = [
|
|
190
|
+
"C:/Windows/Fonts/segoeui.ttf",
|
|
191
|
+
"C:/Windows/Fonts/arial.ttf",
|
|
192
|
+
"C:/Windows/Fonts/calibri.ttf",
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
# Find first available font
|
|
196
|
+
font_path = None
|
|
197
|
+
for path in font_paths:
|
|
198
|
+
if Path(path).exists():
|
|
199
|
+
font_path = path
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
# Load font or use default with scaling
|
|
203
|
+
if font_path:
|
|
204
|
+
with dpg.font_registry():
|
|
205
|
+
# Load at base size, will scale with global font scale
|
|
206
|
+
default_font = dpg.add_font(font_path, 18)
|
|
207
|
+
dpg.bind_font(default_font)
|
|
208
|
+
|
|
209
|
+
# Apply initial font scale
|
|
210
|
+
dpg.set_global_font_scale(self._font_scale)
|
|
211
|
+
|
|
212
|
+
def _increase_font_size(self) -> None:
|
|
213
|
+
"""Increase the global font size."""
|
|
214
|
+
self._font_scale = min(
|
|
215
|
+
self._font_scale + self._font_scale_step, self._max_font_scale
|
|
216
|
+
)
|
|
217
|
+
dpg.set_global_font_scale(self._font_scale)
|
|
218
|
+
|
|
219
|
+
def _decrease_font_size(self) -> None:
|
|
220
|
+
"""Decrease the global font size."""
|
|
221
|
+
self._font_scale = max(
|
|
222
|
+
self._font_scale - self._font_scale_step, self._min_font_scale
|
|
223
|
+
)
|
|
224
|
+
dpg.set_global_font_scale(self._font_scale)
|
|
225
|
+
|
|
226
|
+
def run(self) -> None:
|
|
227
|
+
"""Run the main application loop."""
|
|
228
|
+
while dpg.is_dearpygui_running():
|
|
229
|
+
# Refresh viewport (for animation, camera updates, etc.)
|
|
230
|
+
if self.viewport_3d:
|
|
231
|
+
self.viewport_3d.refresh()
|
|
232
|
+
|
|
233
|
+
dpg.render_dearpygui_frame()
|
|
234
|
+
|
|
235
|
+
dpg.destroy_context()
|
|
236
|
+
|
|
237
|
+
def load_config(self, config_path: str | Path) -> bool:
|
|
238
|
+
"""Load a configuration file (YAML or TOML).
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
config_path: Path to configuration file
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if loaded successfully
|
|
245
|
+
"""
|
|
246
|
+
config_path = Path(config_path)
|
|
247
|
+
|
|
248
|
+
if not config_path.exists():
|
|
249
|
+
self._show_error(f"File not found: {config_path}")
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
# Import config loading utilities
|
|
254
|
+
from lsurf.cli.config_schema import load_config_file
|
|
255
|
+
|
|
256
|
+
config = load_config_file(config_path)
|
|
257
|
+
|
|
258
|
+
# Build geometry
|
|
259
|
+
self._geometry = config.build_geometry()
|
|
260
|
+
self.scene.load_geometry(self._geometry)
|
|
261
|
+
|
|
262
|
+
# Build source
|
|
263
|
+
self._source = config.build_source()
|
|
264
|
+
self.scene.load_source(self._source)
|
|
265
|
+
|
|
266
|
+
# Store simulation config
|
|
267
|
+
self._sim_config = config.build_simulation_config()
|
|
268
|
+
|
|
269
|
+
self._config_path = config_path
|
|
270
|
+
self._update_title()
|
|
271
|
+
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
self._show_error(f"Failed to load config: {e}")
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
def run_simulation(self) -> bool:
|
|
279
|
+
"""Run the simulation with current configuration.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True if simulation started successfully
|
|
283
|
+
"""
|
|
284
|
+
if self._geometry is None:
|
|
285
|
+
self._show_error("No geometry loaded. Load a configuration first.")
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
if self._source is None:
|
|
289
|
+
self._show_error("No source loaded. Load a configuration first.")
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
if self.simulation_runner.is_running():
|
|
293
|
+
self._show_error("Simulation already running.")
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
# Clear previous results
|
|
297
|
+
if self.results:
|
|
298
|
+
self.results.clear()
|
|
299
|
+
|
|
300
|
+
# Start simulation
|
|
301
|
+
return self.simulation_runner.start(
|
|
302
|
+
self._geometry, self._source, self._sim_config
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def _setup_theme(self) -> None:
|
|
306
|
+
"""Set up the dark theme."""
|
|
307
|
+
with dpg.theme() as global_theme:
|
|
308
|
+
with dpg.theme_component(dpg.mvAll):
|
|
309
|
+
# Dark background
|
|
310
|
+
dpg.add_theme_color(dpg.mvThemeCol_WindowBg, (30, 30, 35, 255))
|
|
311
|
+
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (25, 25, 30, 255))
|
|
312
|
+
dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (45, 45, 50, 255))
|
|
313
|
+
dpg.add_theme_color(dpg.mvThemeCol_FrameBgHovered, (60, 60, 70, 255))
|
|
314
|
+
dpg.add_theme_color(dpg.mvThemeCol_FrameBgActive, (70, 70, 80, 255))
|
|
315
|
+
|
|
316
|
+
# Headers
|
|
317
|
+
dpg.add_theme_color(dpg.mvThemeCol_Header, (60, 80, 120, 255))
|
|
318
|
+
dpg.add_theme_color(dpg.mvThemeCol_HeaderHovered, (70, 90, 140, 255))
|
|
319
|
+
dpg.add_theme_color(dpg.mvThemeCol_HeaderActive, (80, 100, 160, 255))
|
|
320
|
+
|
|
321
|
+
# Buttons
|
|
322
|
+
dpg.add_theme_color(dpg.mvThemeCol_Button, (60, 80, 120, 255))
|
|
323
|
+
dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, (80, 100, 150, 255))
|
|
324
|
+
dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, (100, 120, 180, 255))
|
|
325
|
+
|
|
326
|
+
# Separators and borders
|
|
327
|
+
dpg.add_theme_color(dpg.mvThemeCol_Separator, (60, 60, 70, 255))
|
|
328
|
+
dpg.add_theme_color(dpg.mvThemeCol_Border, (50, 50, 60, 255))
|
|
329
|
+
|
|
330
|
+
# Text
|
|
331
|
+
dpg.add_theme_color(dpg.mvThemeCol_Text, (220, 220, 220, 255))
|
|
332
|
+
dpg.add_theme_color(dpg.mvThemeCol_TextDisabled, (128, 128, 128, 255))
|
|
333
|
+
|
|
334
|
+
# Slider/scroll
|
|
335
|
+
dpg.add_theme_color(dpg.mvThemeCol_SliderGrab, (80, 120, 180, 255))
|
|
336
|
+
dpg.add_theme_color(
|
|
337
|
+
dpg.mvThemeCol_SliderGrabActive, (100, 140, 200, 255)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Style
|
|
341
|
+
dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 4)
|
|
342
|
+
dpg.add_theme_style(dpg.mvStyleVar_WindowRounding, 6)
|
|
343
|
+
dpg.add_theme_style(dpg.mvStyleVar_ChildRounding, 4)
|
|
344
|
+
dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 6, 4)
|
|
345
|
+
dpg.add_theme_style(dpg.mvStyleVar_ItemSpacing, 8, 6)
|
|
346
|
+
|
|
347
|
+
dpg.bind_theme(global_theme)
|
|
348
|
+
|
|
349
|
+
def _create_main_window(self) -> None:
|
|
350
|
+
"""Create the main application window with all panels."""
|
|
351
|
+
# Get viewport size to fill it completely
|
|
352
|
+
vp_width = dpg.get_viewport_client_width() or 1920
|
|
353
|
+
vp_height = dpg.get_viewport_client_height() or 1080
|
|
354
|
+
|
|
355
|
+
with dpg.window(
|
|
356
|
+
tag="main_window",
|
|
357
|
+
no_title_bar=True,
|
|
358
|
+
no_move=True,
|
|
359
|
+
no_resize=False, # Allow resizing
|
|
360
|
+
no_collapse=True,
|
|
361
|
+
width=vp_width,
|
|
362
|
+
height=vp_height,
|
|
363
|
+
):
|
|
364
|
+
# Menu bar
|
|
365
|
+
self._create_menu_bar()
|
|
366
|
+
|
|
367
|
+
# Main content area - use horizontal group
|
|
368
|
+
# Window is 1920 wide, panels: left=250, center=flexible, right=520
|
|
369
|
+
with dpg.group(horizontal=True, tag="main_content"):
|
|
370
|
+
# Left panel: Scene tree (resizable width)
|
|
371
|
+
with dpg.child_window(
|
|
372
|
+
width=250, height=-1, tag="left_panel", resizable_x=True
|
|
373
|
+
):
|
|
374
|
+
self.scene_tree = SceneTreePanel(self.scene)
|
|
375
|
+
self.scene_tree.create("left_panel")
|
|
376
|
+
|
|
377
|
+
# Center: 3D Viewport / Visualizations + Results (takes remaining space)
|
|
378
|
+
with dpg.child_window(
|
|
379
|
+
width=-520,
|
|
380
|
+
height=-1,
|
|
381
|
+
tag="center_panel",
|
|
382
|
+
resizable_x=True,
|
|
383
|
+
border=False,
|
|
384
|
+
):
|
|
385
|
+
# Toggle button for switching between 3D view and visualizations
|
|
386
|
+
with dpg.group(horizontal=True):
|
|
387
|
+
dpg.add_button(
|
|
388
|
+
label="3D View",
|
|
389
|
+
callback=lambda: self._show_3d_view(),
|
|
390
|
+
tag="btn_3d_view",
|
|
391
|
+
)
|
|
392
|
+
dpg.add_button(
|
|
393
|
+
label="Visualizations",
|
|
394
|
+
callback=lambda: self._show_visualizations(),
|
|
395
|
+
tag="btn_visualizations",
|
|
396
|
+
)
|
|
397
|
+
dpg.add_separator()
|
|
398
|
+
|
|
399
|
+
# Container for viewport (shown by default)
|
|
400
|
+
with dpg.group(tag="viewport_container", show=True):
|
|
401
|
+
self.viewport_3d = Viewport3D(self.scene)
|
|
402
|
+
self.viewport_3d.create("viewport_container")
|
|
403
|
+
|
|
404
|
+
# Container for visualizations (hidden by default)
|
|
405
|
+
with dpg.group(tag="viz_container", show=False):
|
|
406
|
+
self.visualization = VisualizationPanel(
|
|
407
|
+
on_close=self._show_3d_view,
|
|
408
|
+
)
|
|
409
|
+
self.visualization.create("viz_container")
|
|
410
|
+
|
|
411
|
+
# Results panel at bottom (resizable height)
|
|
412
|
+
self.results = ResultsPanel(self.scene)
|
|
413
|
+
self.results.create("center_panel")
|
|
414
|
+
|
|
415
|
+
# Right panel: Config Editor (resizable width)
|
|
416
|
+
with dpg.child_window(
|
|
417
|
+
width=520, height=-1, tag="right_panel", resizable_x=True
|
|
418
|
+
):
|
|
419
|
+
self.config_editor = ConfigEditorPanel(
|
|
420
|
+
self.scene,
|
|
421
|
+
on_config_change=self._on_config_editor_change,
|
|
422
|
+
on_simulate=self._on_run_simulation,
|
|
423
|
+
results_panel=self.results,
|
|
424
|
+
visualization_panel=self.visualization,
|
|
425
|
+
)
|
|
426
|
+
self.config_editor.create("right_panel")
|
|
427
|
+
|
|
428
|
+
# Set primary window
|
|
429
|
+
dpg.set_primary_window("main_window", True)
|
|
430
|
+
|
|
431
|
+
# Register viewport mouse handlers (must be outside container context)
|
|
432
|
+
if self.viewport_3d:
|
|
433
|
+
self.viewport_3d.register_handlers()
|
|
434
|
+
|
|
435
|
+
def _on_config_editor_change(self) -> None:
|
|
436
|
+
"""Handle config editor changes."""
|
|
437
|
+
# Update internal state from config editor
|
|
438
|
+
if self.config_editor:
|
|
439
|
+
self._geometry = self.scene.geometry
|
|
440
|
+
self._source = self.scene.source
|
|
441
|
+
|
|
442
|
+
def _show_3d_view(self) -> None:
|
|
443
|
+
"""Show the 3D viewport and hide visualizations."""
|
|
444
|
+
self._showing_visualizations = False
|
|
445
|
+
if dpg.does_item_exist("viewport_container"):
|
|
446
|
+
dpg.show_item("viewport_container")
|
|
447
|
+
if dpg.does_item_exist("viz_container"):
|
|
448
|
+
dpg.hide_item("viz_container")
|
|
449
|
+
# Refresh viewport
|
|
450
|
+
if self.viewport_3d:
|
|
451
|
+
self.viewport_3d.refresh()
|
|
452
|
+
|
|
453
|
+
def _show_visualizations(self) -> None:
|
|
454
|
+
"""Show the visualizations panel and hide 3D viewport."""
|
|
455
|
+
self._showing_visualizations = True
|
|
456
|
+
if dpg.does_item_exist("viewport_container"):
|
|
457
|
+
dpg.hide_item("viewport_container")
|
|
458
|
+
if dpg.does_item_exist("viz_container"):
|
|
459
|
+
dpg.show_item("viz_container")
|
|
460
|
+
|
|
461
|
+
def _create_menu_bar(self) -> None:
|
|
462
|
+
"""Create the menu bar."""
|
|
463
|
+
with dpg.menu_bar():
|
|
464
|
+
with dpg.menu(label="File"):
|
|
465
|
+
dpg.add_menu_item(
|
|
466
|
+
label="New Config",
|
|
467
|
+
callback=self._on_new_config,
|
|
468
|
+
shortcut="Ctrl+N",
|
|
469
|
+
)
|
|
470
|
+
dpg.add_menu_item(
|
|
471
|
+
label="Open Config...",
|
|
472
|
+
callback=self._on_open_config,
|
|
473
|
+
shortcut="Ctrl+O",
|
|
474
|
+
)
|
|
475
|
+
dpg.add_menu_item(
|
|
476
|
+
label="Save Config...",
|
|
477
|
+
callback=self._on_save_config,
|
|
478
|
+
shortcut="Ctrl+S",
|
|
479
|
+
)
|
|
480
|
+
dpg.add_menu_item(
|
|
481
|
+
label="Reload Config",
|
|
482
|
+
callback=self._on_reload_config,
|
|
483
|
+
shortcut="Ctrl+R",
|
|
484
|
+
)
|
|
485
|
+
dpg.add_separator()
|
|
486
|
+
dpg.add_menu_item(
|
|
487
|
+
label="Exit",
|
|
488
|
+
callback=lambda: dpg.stop_dearpygui(),
|
|
489
|
+
shortcut="Ctrl+Q",
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
with dpg.menu(label="View"):
|
|
493
|
+
dpg.add_menu_item(
|
|
494
|
+
label="Fit to Scene",
|
|
495
|
+
callback=self._on_fit_to_scene,
|
|
496
|
+
shortcut="F",
|
|
497
|
+
)
|
|
498
|
+
dpg.add_separator()
|
|
499
|
+
dpg.add_menu_item(
|
|
500
|
+
label="Increase Font Size",
|
|
501
|
+
callback=lambda: self._increase_font_size(),
|
|
502
|
+
shortcut="Ctrl++",
|
|
503
|
+
)
|
|
504
|
+
dpg.add_menu_item(
|
|
505
|
+
label="Decrease Font Size",
|
|
506
|
+
callback=lambda: self._decrease_font_size(),
|
|
507
|
+
shortcut="Ctrl+-",
|
|
508
|
+
)
|
|
509
|
+
dpg.add_separator()
|
|
510
|
+
dpg.add_menu_item(
|
|
511
|
+
label="Top View",
|
|
512
|
+
callback=lambda: self._on_camera_preset("top"),
|
|
513
|
+
shortcut="Numpad 7",
|
|
514
|
+
)
|
|
515
|
+
dpg.add_menu_item(
|
|
516
|
+
label="Front View",
|
|
517
|
+
callback=lambda: self._on_camera_preset("front"),
|
|
518
|
+
shortcut="Numpad 1",
|
|
519
|
+
)
|
|
520
|
+
dpg.add_menu_item(
|
|
521
|
+
label="Side View",
|
|
522
|
+
callback=lambda: self._on_camera_preset("side"),
|
|
523
|
+
shortcut="Numpad 3",
|
|
524
|
+
)
|
|
525
|
+
dpg.add_menu_item(
|
|
526
|
+
label="Isometric View",
|
|
527
|
+
callback=lambda: self._on_camera_preset("isometric"),
|
|
528
|
+
shortcut="Numpad 5",
|
|
529
|
+
)
|
|
530
|
+
dpg.add_separator()
|
|
531
|
+
dpg.add_menu_item(
|
|
532
|
+
label="Show 3D Viewport",
|
|
533
|
+
callback=lambda: self._show_3d_view(),
|
|
534
|
+
shortcut="1",
|
|
535
|
+
)
|
|
536
|
+
dpg.add_menu_item(
|
|
537
|
+
label="Show Visualizations",
|
|
538
|
+
callback=lambda: self._show_visualizations(),
|
|
539
|
+
shortcut="2",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
with dpg.menu(label="Simulation"):
|
|
543
|
+
dpg.add_menu_item(
|
|
544
|
+
label="Run Simulation",
|
|
545
|
+
callback=self._on_run_simulation,
|
|
546
|
+
shortcut="F5",
|
|
547
|
+
)
|
|
548
|
+
dpg.add_menu_item(
|
|
549
|
+
label="Cancel Simulation",
|
|
550
|
+
callback=self._on_cancel_simulation,
|
|
551
|
+
shortcut="Escape",
|
|
552
|
+
)
|
|
553
|
+
dpg.add_separator()
|
|
554
|
+
dpg.add_menu_item(
|
|
555
|
+
label="Clear Results",
|
|
556
|
+
callback=self._on_clear_results,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
with dpg.menu(label="Help"):
|
|
560
|
+
dpg.add_menu_item(
|
|
561
|
+
label="About",
|
|
562
|
+
callback=self._on_about,
|
|
563
|
+
)
|
|
564
|
+
dpg.add_menu_item(
|
|
565
|
+
label="Keyboard Shortcuts",
|
|
566
|
+
callback=self._on_shortcuts,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Register keyboard shortcuts
|
|
570
|
+
with dpg.handler_registry():
|
|
571
|
+
dpg.add_key_press_handler(
|
|
572
|
+
dpg.mvKey_O,
|
|
573
|
+
callback=lambda: (
|
|
574
|
+
self._on_open_config()
|
|
575
|
+
if dpg.is_key_down(dpg.mvKey_Control)
|
|
576
|
+
else None
|
|
577
|
+
),
|
|
578
|
+
)
|
|
579
|
+
dpg.add_key_press_handler(
|
|
580
|
+
dpg.mvKey_R,
|
|
581
|
+
callback=lambda: (
|
|
582
|
+
self._on_reload_config()
|
|
583
|
+
if dpg.is_key_down(dpg.mvKey_Control)
|
|
584
|
+
else None
|
|
585
|
+
),
|
|
586
|
+
)
|
|
587
|
+
dpg.add_key_press_handler(dpg.mvKey_F5, callback=self._on_run_simulation)
|
|
588
|
+
dpg.add_key_press_handler(dpg.mvKey_F, callback=self._on_fit_to_scene)
|
|
589
|
+
dpg.add_key_press_handler(
|
|
590
|
+
dpg.mvKey_Escape, callback=self._on_cancel_simulation
|
|
591
|
+
)
|
|
592
|
+
# Font size shortcuts: Ctrl++/Ctrl+- and numpad +/-
|
|
593
|
+
dpg.add_key_press_handler(
|
|
594
|
+
dpg.mvKey_Plus, # + key
|
|
595
|
+
callback=lambda: (
|
|
596
|
+
self._increase_font_size()
|
|
597
|
+
if dpg.is_key_down(dpg.mvKey_LControl)
|
|
598
|
+
or dpg.is_key_down(dpg.mvKey_RControl)
|
|
599
|
+
else None
|
|
600
|
+
),
|
|
601
|
+
)
|
|
602
|
+
dpg.add_key_press_handler(
|
|
603
|
+
dpg.mvKey_Minus, # - key
|
|
604
|
+
callback=lambda: (
|
|
605
|
+
self._decrease_font_size()
|
|
606
|
+
if dpg.is_key_down(dpg.mvKey_LControl)
|
|
607
|
+
or dpg.is_key_down(dpg.mvKey_RControl)
|
|
608
|
+
else None
|
|
609
|
+
),
|
|
610
|
+
)
|
|
611
|
+
# Also support numpad + and - (no Ctrl needed)
|
|
612
|
+
dpg.add_key_press_handler(
|
|
613
|
+
dpg.mvKey_Add,
|
|
614
|
+
callback=lambda: self._increase_font_size(),
|
|
615
|
+
)
|
|
616
|
+
dpg.add_key_press_handler(
|
|
617
|
+
dpg.mvKey_Subtract,
|
|
618
|
+
callback=lambda: self._decrease_font_size(),
|
|
619
|
+
)
|
|
620
|
+
# View toggle shortcuts: 1 for 3D, 2 for visualizations
|
|
621
|
+
dpg.add_key_press_handler(
|
|
622
|
+
dpg.mvKey_1,
|
|
623
|
+
callback=lambda: self._show_3d_view(),
|
|
624
|
+
)
|
|
625
|
+
dpg.add_key_press_handler(
|
|
626
|
+
dpg.mvKey_2,
|
|
627
|
+
callback=lambda: self._show_visualizations(),
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
def _on_new_config(self) -> None:
|
|
631
|
+
"""Create a new empty configuration."""
|
|
632
|
+
if self.config_editor:
|
|
633
|
+
self.config_editor._on_new_config()
|
|
634
|
+
self._config_path = None
|
|
635
|
+
self._update_title()
|
|
636
|
+
|
|
637
|
+
def _on_open_config(self) -> None:
|
|
638
|
+
"""Handle open config menu item."""
|
|
639
|
+
|
|
640
|
+
# Use file dialog
|
|
641
|
+
def callback(sender, app_data):
|
|
642
|
+
if app_data.get("file_path_name"):
|
|
643
|
+
file_path = app_data["file_path_name"]
|
|
644
|
+
# Load into config editor (which also applies to scene)
|
|
645
|
+
if self.config_editor:
|
|
646
|
+
self.config_editor.load_from_file(file_path)
|
|
647
|
+
else:
|
|
648
|
+
# Fallback if no config editor
|
|
649
|
+
self.load_config(file_path)
|
|
650
|
+
|
|
651
|
+
with dpg.file_dialog(
|
|
652
|
+
callback=callback,
|
|
653
|
+
width=800,
|
|
654
|
+
height=500,
|
|
655
|
+
modal=True,
|
|
656
|
+
show=True,
|
|
657
|
+
):
|
|
658
|
+
dpg.add_file_extension(".yaml", color=(0, 255, 0))
|
|
659
|
+
dpg.add_file_extension(".yml", color=(0, 255, 0))
|
|
660
|
+
dpg.add_file_extension(".toml", color=(0, 200, 255))
|
|
661
|
+
|
|
662
|
+
def _on_save_config(self) -> None:
|
|
663
|
+
"""Save current configuration to file."""
|
|
664
|
+
if self.config_editor:
|
|
665
|
+
self.config_editor._on_export_yaml()
|
|
666
|
+
|
|
667
|
+
def _on_reload_config(self) -> None:
|
|
668
|
+
"""Reload the current configuration file."""
|
|
669
|
+
if self._config_path:
|
|
670
|
+
self.load_config(self._config_path)
|
|
671
|
+
else:
|
|
672
|
+
self._show_error("No configuration file loaded.")
|
|
673
|
+
|
|
674
|
+
def _on_fit_to_scene(self) -> None:
|
|
675
|
+
"""Fit camera to scene bounds."""
|
|
676
|
+
self.scene.fit_camera_to_scene()
|
|
677
|
+
if self.viewport_3d:
|
|
678
|
+
min_bounds, max_bounds = self.scene.get_bounds()
|
|
679
|
+
self.viewport_3d.camera.fit_to_bounds(min_bounds, max_bounds)
|
|
680
|
+
self.viewport_3d.refresh()
|
|
681
|
+
|
|
682
|
+
def _on_camera_preset(self, preset: str) -> None:
|
|
683
|
+
"""Set camera to preset view."""
|
|
684
|
+
if self.viewport_3d:
|
|
685
|
+
self.viewport_3d.camera.set_preset(preset)
|
|
686
|
+
self.viewport_3d.refresh()
|
|
687
|
+
|
|
688
|
+
def _on_run_simulation(self) -> None:
|
|
689
|
+
"""Run simulation."""
|
|
690
|
+
self.run_simulation()
|
|
691
|
+
|
|
692
|
+
def _on_cancel_simulation(self) -> None:
|
|
693
|
+
"""Cancel running simulation."""
|
|
694
|
+
if self.simulation_runner.is_running():
|
|
695
|
+
self.simulation_runner.cancel()
|
|
696
|
+
|
|
697
|
+
def _on_clear_results(self) -> None:
|
|
698
|
+
"""Clear simulation results."""
|
|
699
|
+
# Remove result objects from scene
|
|
700
|
+
from .core.scene import ObjectType
|
|
701
|
+
|
|
702
|
+
to_remove = [
|
|
703
|
+
name
|
|
704
|
+
for name, obj in self.scene.objects.items()
|
|
705
|
+
if obj.obj_type in (ObjectType.RAY_PATHS, ObjectType.DETECTIONS)
|
|
706
|
+
]
|
|
707
|
+
for name in to_remove:
|
|
708
|
+
self.scene.remove_object(name)
|
|
709
|
+
|
|
710
|
+
self.scene.result = None
|
|
711
|
+
if self.results:
|
|
712
|
+
self.results.clear()
|
|
713
|
+
|
|
714
|
+
def _on_about(self) -> None:
|
|
715
|
+
"""Show about dialog."""
|
|
716
|
+
with dpg.window(
|
|
717
|
+
label="About L-SURF",
|
|
718
|
+
modal=True,
|
|
719
|
+
width=400,
|
|
720
|
+
height=200,
|
|
721
|
+
no_resize=True,
|
|
722
|
+
):
|
|
723
|
+
dpg.add_text("L-SURF GUI", color=(100, 200, 255))
|
|
724
|
+
dpg.add_text("GPU-Accelerated Ray Tracing Visualization")
|
|
725
|
+
dpg.add_separator()
|
|
726
|
+
dpg.add_text("Interactive 3D visualization for ray tracing simulations.")
|
|
727
|
+
dpg.add_text("")
|
|
728
|
+
dpg.add_text("Controls:")
|
|
729
|
+
dpg.add_text(" Left Mouse: Orbit camera")
|
|
730
|
+
dpg.add_text(" Middle Mouse: Pan camera")
|
|
731
|
+
dpg.add_text(" Right Mouse/Scroll: Zoom")
|
|
732
|
+
dpg.add_separator()
|
|
733
|
+
dpg.add_button(
|
|
734
|
+
label="Close",
|
|
735
|
+
callback=lambda s, a: dpg.delete_item(dpg.get_item_parent(s)),
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
def _on_shortcuts(self) -> None:
|
|
739
|
+
"""Show keyboard shortcuts."""
|
|
740
|
+
with dpg.window(
|
|
741
|
+
label="Keyboard Shortcuts",
|
|
742
|
+
modal=True,
|
|
743
|
+
width=400,
|
|
744
|
+
height=440,
|
|
745
|
+
no_resize=True,
|
|
746
|
+
):
|
|
747
|
+
shortcuts = [
|
|
748
|
+
("Ctrl+O", "Open configuration file"),
|
|
749
|
+
("Ctrl+S", "Save configuration file"),
|
|
750
|
+
("Ctrl+R", "Reload configuration"),
|
|
751
|
+
("F5", "Run simulation"),
|
|
752
|
+
("Escape", "Cancel simulation"),
|
|
753
|
+
("F", "Fit camera to scene"),
|
|
754
|
+
("1", "Show 3D viewport"),
|
|
755
|
+
("2", "Show visualizations"),
|
|
756
|
+
("Ctrl++", "Increase font size"),
|
|
757
|
+
("Ctrl+-", "Decrease font size"),
|
|
758
|
+
("Numpad 7", "Top view"),
|
|
759
|
+
("Numpad 1", "Front view"),
|
|
760
|
+
("Numpad 3", "Side view"),
|
|
761
|
+
("Numpad 5", "Isometric view"),
|
|
762
|
+
]
|
|
763
|
+
|
|
764
|
+
for key, desc in shortcuts:
|
|
765
|
+
with dpg.group(horizontal=True):
|
|
766
|
+
dpg.add_text(key, color=(255, 200, 100))
|
|
767
|
+
dpg.add_spacer(width=20)
|
|
768
|
+
dpg.add_text(desc)
|
|
769
|
+
|
|
770
|
+
dpg.add_separator()
|
|
771
|
+
dpg.add_button(
|
|
772
|
+
label="Close",
|
|
773
|
+
callback=lambda s, a: dpg.delete_item(dpg.get_item_parent(s)),
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
def _on_simulation_progress(self, progress: "SimulationProgress") -> None:
|
|
777
|
+
"""Handle simulation progress updates."""
|
|
778
|
+
if self.results:
|
|
779
|
+
self.results.update_progress(progress)
|
|
780
|
+
|
|
781
|
+
def _on_simulation_complete(self, result) -> None:
|
|
782
|
+
"""Handle simulation completion."""
|
|
783
|
+
if result is not None:
|
|
784
|
+
self.scene.load_result(result)
|
|
785
|
+
if self.results:
|
|
786
|
+
self.results.update_results(result)
|
|
787
|
+
# Update visualization panel with results
|
|
788
|
+
if self.visualization:
|
|
789
|
+
self.visualization.set_result(result)
|
|
790
|
+
|
|
791
|
+
def _update_title(self) -> None:
|
|
792
|
+
"""Update viewport title with current file."""
|
|
793
|
+
title = "L-SURF - GPU Ray Tracing Visualization"
|
|
794
|
+
if self._config_path:
|
|
795
|
+
title = f"{self._config_path.name} - {title}"
|
|
796
|
+
dpg.set_viewport_title(title)
|
|
797
|
+
|
|
798
|
+
def _show_error(self, message: str) -> None:
|
|
799
|
+
"""Show an error dialog."""
|
|
800
|
+
with dpg.window(
|
|
801
|
+
label="Error",
|
|
802
|
+
modal=True,
|
|
803
|
+
width=400,
|
|
804
|
+
height=150,
|
|
805
|
+
no_resize=True,
|
|
806
|
+
):
|
|
807
|
+
dpg.add_text(message, wrap=380, color=(255, 100, 100))
|
|
808
|
+
dpg.add_separator()
|
|
809
|
+
dpg.add_button(
|
|
810
|
+
label="OK",
|
|
811
|
+
callback=lambda s, a: dpg.delete_item(dpg.get_item_parent(s)),
|
|
812
|
+
width=-1,
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _show_splash_screen() -> None:
|
|
817
|
+
"""Show a splash screen while loading."""
|
|
818
|
+
dpg.create_context()
|
|
819
|
+
|
|
820
|
+
# Create a minimal viewport for the splash
|
|
821
|
+
dpg.create_viewport(
|
|
822
|
+
title="L-SURF",
|
|
823
|
+
width=500,
|
|
824
|
+
height=300,
|
|
825
|
+
decorated=False, # No window decorations for splash
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
# Create splash window
|
|
829
|
+
with dpg.window(
|
|
830
|
+
tag="splash_window",
|
|
831
|
+
no_title_bar=True,
|
|
832
|
+
no_resize=True,
|
|
833
|
+
no_move=True,
|
|
834
|
+
no_scrollbar=True,
|
|
835
|
+
no_collapse=True,
|
|
836
|
+
width=500,
|
|
837
|
+
height=300,
|
|
838
|
+
):
|
|
839
|
+
dpg.add_spacer(height=60)
|
|
840
|
+
dpg.add_text(
|
|
841
|
+
"L-SURF",
|
|
842
|
+
color=(100, 200, 255),
|
|
843
|
+
)
|
|
844
|
+
dpg.add_spacer(height=20)
|
|
845
|
+
dpg.add_text(
|
|
846
|
+
"GPU Ray Tracing Visualization",
|
|
847
|
+
color=(180, 180, 180),
|
|
848
|
+
)
|
|
849
|
+
dpg.add_spacer(height=40)
|
|
850
|
+
dpg.add_text(
|
|
851
|
+
"Loading...",
|
|
852
|
+
tag="splash_status",
|
|
853
|
+
color=(128, 128, 128),
|
|
854
|
+
)
|
|
855
|
+
dpg.add_loading_indicator(
|
|
856
|
+
style=1, # Circular style
|
|
857
|
+
radius=4.0,
|
|
858
|
+
color=(100, 200, 255, 255),
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
# Center the splash window
|
|
862
|
+
dpg.set_primary_window("splash_window", True)
|
|
863
|
+
|
|
864
|
+
dpg.setup_dearpygui()
|
|
865
|
+
dpg.show_viewport()
|
|
866
|
+
|
|
867
|
+
# Render one frame to show the splash
|
|
868
|
+
dpg.render_dearpygui_frame()
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def _update_splash_status(message: str) -> None:
|
|
872
|
+
"""Update the splash screen status message."""
|
|
873
|
+
if dpg.does_item_exist("splash_status"):
|
|
874
|
+
dpg.set_value("splash_status", message)
|
|
875
|
+
dpg.render_dearpygui_frame()
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def launch_gui(config_path: str | Path | None = None) -> None:
|
|
879
|
+
"""Launch the L-SURF GUI application.
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
config_path: Optional path to a configuration file to load on startup
|
|
883
|
+
"""
|
|
884
|
+
# Show splash screen immediately
|
|
885
|
+
_show_splash_screen()
|
|
886
|
+
|
|
887
|
+
# Load components while splash is visible
|
|
888
|
+
_update_splash_status("Loading components...")
|
|
889
|
+
_load_components()
|
|
890
|
+
|
|
891
|
+
# Clean up splash and create main app
|
|
892
|
+
_update_splash_status("Initializing...")
|
|
893
|
+
dpg.stop_dearpygui()
|
|
894
|
+
dpg.destroy_context()
|
|
895
|
+
|
|
896
|
+
# Now create the main application
|
|
897
|
+
app = LSurfApp()
|
|
898
|
+
app.setup()
|
|
899
|
+
|
|
900
|
+
if config_path:
|
|
901
|
+
app.load_config(config_path)
|
|
902
|
+
|
|
903
|
+
app.run()
|