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.
Files changed (180) hide show
  1. lsurf/__init__.py +471 -0
  2. lsurf/analysis/__init__.py +107 -0
  3. lsurf/analysis/healpix_utils.py +418 -0
  4. lsurf/analysis/sphere_viz.py +1280 -0
  5. lsurf/cli/__init__.py +48 -0
  6. lsurf/cli/build.py +398 -0
  7. lsurf/cli/config_schema.py +318 -0
  8. lsurf/cli/gui_cmd.py +76 -0
  9. lsurf/cli/interactive.py +850 -0
  10. lsurf/cli/main.py +81 -0
  11. lsurf/cli/run.py +806 -0
  12. lsurf/detectors/__init__.py +266 -0
  13. lsurf/detectors/analysis.py +289 -0
  14. lsurf/detectors/base.py +284 -0
  15. lsurf/detectors/constant_size_rings.py +485 -0
  16. lsurf/detectors/directional.py +45 -0
  17. lsurf/detectors/extended/__init__.py +73 -0
  18. lsurf/detectors/extended/local_sphere.py +353 -0
  19. lsurf/detectors/extended/recording_sphere.py +368 -0
  20. lsurf/detectors/planar.py +45 -0
  21. lsurf/detectors/protocol.py +187 -0
  22. lsurf/detectors/recording_spheres.py +63 -0
  23. lsurf/detectors/results.py +1140 -0
  24. lsurf/detectors/small/__init__.py +79 -0
  25. lsurf/detectors/small/directional.py +330 -0
  26. lsurf/detectors/small/planar.py +401 -0
  27. lsurf/detectors/small/spherical.py +450 -0
  28. lsurf/detectors/spherical.py +45 -0
  29. lsurf/geometry/__init__.py +199 -0
  30. lsurf/geometry/builder.py +478 -0
  31. lsurf/geometry/cell.py +228 -0
  32. lsurf/geometry/cell_geometry.py +247 -0
  33. lsurf/geometry/detector_arrays.py +1785 -0
  34. lsurf/geometry/geometry.py +222 -0
  35. lsurf/geometry/surface_analysis.py +375 -0
  36. lsurf/geometry/validation.py +91 -0
  37. lsurf/gui/__init__.py +51 -0
  38. lsurf/gui/app.py +903 -0
  39. lsurf/gui/core/__init__.py +39 -0
  40. lsurf/gui/core/scene.py +343 -0
  41. lsurf/gui/core/simulation.py +264 -0
  42. lsurf/gui/renderers/__init__.py +40 -0
  43. lsurf/gui/renderers/ray_renderer.py +353 -0
  44. lsurf/gui/renderers/source_renderer.py +505 -0
  45. lsurf/gui/renderers/surface_renderer.py +477 -0
  46. lsurf/gui/views/__init__.py +48 -0
  47. lsurf/gui/views/config_editor.py +3199 -0
  48. lsurf/gui/views/properties.py +257 -0
  49. lsurf/gui/views/results.py +291 -0
  50. lsurf/gui/views/scene_tree.py +180 -0
  51. lsurf/gui/views/viewport_3d.py +555 -0
  52. lsurf/gui/views/visualizations.py +712 -0
  53. lsurf/materials/__init__.py +169 -0
  54. lsurf/materials/base/__init__.py +64 -0
  55. lsurf/materials/base/full_inhomogeneous.py +208 -0
  56. lsurf/materials/base/grid_inhomogeneous.py +319 -0
  57. lsurf/materials/base/homogeneous.py +342 -0
  58. lsurf/materials/base/material_field.py +527 -0
  59. lsurf/materials/base/simple_inhomogeneous.py +418 -0
  60. lsurf/materials/base/spectral_inhomogeneous.py +497 -0
  61. lsurf/materials/implementations/__init__.py +120 -0
  62. lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
  63. lsurf/materials/implementations/duct_atmosphere.py +390 -0
  64. lsurf/materials/implementations/exponential_atmosphere.py +435 -0
  65. lsurf/materials/implementations/gaussian_lens.py +120 -0
  66. lsurf/materials/implementations/interpolated_data.py +123 -0
  67. lsurf/materials/implementations/layered_atmosphere.py +134 -0
  68. lsurf/materials/implementations/linear_gradient.py +109 -0
  69. lsurf/materials/implementations/linsley_atmosphere.py +764 -0
  70. lsurf/materials/implementations/standard_materials.py +126 -0
  71. lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
  72. lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
  73. lsurf/materials/utils/__init__.py +77 -0
  74. lsurf/materials/utils/constants.py +45 -0
  75. lsurf/materials/utils/device_functions.py +117 -0
  76. lsurf/materials/utils/dispersion.py +160 -0
  77. lsurf/materials/utils/factories.py +142 -0
  78. lsurf/propagation/__init__.py +91 -0
  79. lsurf/propagation/detector_gpu.py +67 -0
  80. lsurf/propagation/gpu_device_rays.py +294 -0
  81. lsurf/propagation/kernels/__init__.py +175 -0
  82. lsurf/propagation/kernels/absorption/__init__.py +61 -0
  83. lsurf/propagation/kernels/absorption/grid.py +240 -0
  84. lsurf/propagation/kernels/absorption/simple.py +232 -0
  85. lsurf/propagation/kernels/absorption/spectral.py +410 -0
  86. lsurf/propagation/kernels/detection/__init__.py +64 -0
  87. lsurf/propagation/kernels/detection/protocol.py +102 -0
  88. lsurf/propagation/kernels/detection/spherical.py +255 -0
  89. lsurf/propagation/kernels/device_functions.py +790 -0
  90. lsurf/propagation/kernels/fresnel/__init__.py +64 -0
  91. lsurf/propagation/kernels/fresnel/protocol.py +97 -0
  92. lsurf/propagation/kernels/fresnel/standard.py +258 -0
  93. lsurf/propagation/kernels/intersection/__init__.py +79 -0
  94. lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
  95. lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
  96. lsurf/propagation/kernels/intersection/plane.py +166 -0
  97. lsurf/propagation/kernels/intersection/protocol.py +95 -0
  98. lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
  99. lsurf/propagation/kernels/intersection/sphere.py +190 -0
  100. lsurf/propagation/kernels/propagation/__init__.py +85 -0
  101. lsurf/propagation/kernels/propagation/grid.py +527 -0
  102. lsurf/propagation/kernels/propagation/protocol.py +105 -0
  103. lsurf/propagation/kernels/propagation/simple.py +460 -0
  104. lsurf/propagation/kernels/propagation/spectral.py +875 -0
  105. lsurf/propagation/kernels/registry.py +331 -0
  106. lsurf/propagation/kernels/surface/__init__.py +72 -0
  107. lsurf/propagation/kernels/surface/bisection.py +232 -0
  108. lsurf/propagation/kernels/surface/detection.py +402 -0
  109. lsurf/propagation/kernels/surface/reduction.py +166 -0
  110. lsurf/propagation/propagator_protocol.py +222 -0
  111. lsurf/propagation/propagators/__init__.py +101 -0
  112. lsurf/propagation/propagators/detector_handler.py +354 -0
  113. lsurf/propagation/propagators/factory.py +200 -0
  114. lsurf/propagation/propagators/fresnel_handler.py +305 -0
  115. lsurf/propagation/propagators/gpu_gradient.py +566 -0
  116. lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
  117. lsurf/propagation/propagators/gradient.py +429 -0
  118. lsurf/propagation/propagators/intersection_handler.py +327 -0
  119. lsurf/propagation/propagators/material_propagator.py +398 -0
  120. lsurf/propagation/propagators/signed_distance_handler.py +522 -0
  121. lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
  122. lsurf/propagation/propagators/surface_interaction.py +616 -0
  123. lsurf/propagation/propagators/surface_propagator.py +719 -0
  124. lsurf/py.typed +1 -0
  125. lsurf/simulation/__init__.py +70 -0
  126. lsurf/simulation/config.py +164 -0
  127. lsurf/simulation/orchestrator.py +462 -0
  128. lsurf/simulation/result.py +299 -0
  129. lsurf/simulation/simulation.py +262 -0
  130. lsurf/sources/__init__.py +128 -0
  131. lsurf/sources/base.py +264 -0
  132. lsurf/sources/collimated.py +252 -0
  133. lsurf/sources/custom.py +409 -0
  134. lsurf/sources/diverging.py +228 -0
  135. lsurf/sources/gaussian.py +272 -0
  136. lsurf/sources/parallel_from_positions.py +197 -0
  137. lsurf/sources/point.py +172 -0
  138. lsurf/sources/uniform_diverging.py +258 -0
  139. lsurf/surfaces/__init__.py +184 -0
  140. lsurf/surfaces/cpu/__init__.py +50 -0
  141. lsurf/surfaces/cpu/curved_wave.py +463 -0
  142. lsurf/surfaces/cpu/gerstner_wave.py +381 -0
  143. lsurf/surfaces/cpu/wave_params.py +118 -0
  144. lsurf/surfaces/gpu/__init__.py +72 -0
  145. lsurf/surfaces/gpu/annular_plane.py +453 -0
  146. lsurf/surfaces/gpu/bounded_plane.py +390 -0
  147. lsurf/surfaces/gpu/curved_wave.py +483 -0
  148. lsurf/surfaces/gpu/gerstner_wave.py +377 -0
  149. lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
  150. lsurf/surfaces/gpu/plane.py +299 -0
  151. lsurf/surfaces/gpu/recording_sphere.py +587 -0
  152. lsurf/surfaces/gpu/sphere.py +311 -0
  153. lsurf/surfaces/protocol.py +336 -0
  154. lsurf/surfaces/registry.py +373 -0
  155. lsurf/utilities/__init__.py +175 -0
  156. lsurf/utilities/detector_analysis.py +814 -0
  157. lsurf/utilities/fresnel.py +628 -0
  158. lsurf/utilities/interactions.py +1215 -0
  159. lsurf/utilities/propagation.py +602 -0
  160. lsurf/utilities/ray_data.py +532 -0
  161. lsurf/utilities/recording_sphere.py +745 -0
  162. lsurf/utilities/time_spread.py +463 -0
  163. lsurf/visualization/__init__.py +329 -0
  164. lsurf/visualization/absorption_plots.py +334 -0
  165. lsurf/visualization/atmospheric_plots.py +754 -0
  166. lsurf/visualization/common.py +348 -0
  167. lsurf/visualization/detector_plots.py +1350 -0
  168. lsurf/visualization/detector_sphere_plots.py +1173 -0
  169. lsurf/visualization/fresnel_plots.py +1061 -0
  170. lsurf/visualization/ocean_simulation_plots.py +999 -0
  171. lsurf/visualization/polarization_plots.py +916 -0
  172. lsurf/visualization/raytracing_plots.py +1521 -0
  173. lsurf/visualization/ring_detector_plots.py +1867 -0
  174. lsurf/visualization/time_spread_plots.py +531 -0
  175. lsurf-1.0.0.dist-info/METADATA +381 -0
  176. lsurf-1.0.0.dist-info/RECORD +180 -0
  177. lsurf-1.0.0.dist-info/WHEEL +5 -0
  178. lsurf-1.0.0.dist-info/entry_points.txt +2 -0
  179. lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
  180. 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()