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
|
@@ -0,0 +1,477 @@
|
|
|
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
|
+
"""Surface renderer - converts lsurf surfaces to 3D mesh data.
|
|
35
|
+
|
|
36
|
+
Generates vertices and indices for rendering surfaces in the 3D viewport.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from typing import TYPE_CHECKING, Any
|
|
40
|
+
|
|
41
|
+
import numpy as np
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from lsurf.surfaces import (
|
|
45
|
+
AnnularPlaneSurface,
|
|
46
|
+
BoundedPlaneSurface,
|
|
47
|
+
LocalRecordingSphereSurface,
|
|
48
|
+
PlaneSurface,
|
|
49
|
+
RecordingSphereSurface,
|
|
50
|
+
SphereSurface,
|
|
51
|
+
Surface,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SurfaceRenderer:
|
|
56
|
+
"""Renders lsurf surfaces to 3D mesh data for Dear PyGui.
|
|
57
|
+
|
|
58
|
+
Each surface type has a specialized rendering method that generates
|
|
59
|
+
appropriate vertices and indices for visualization.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, default_size: float = 10.0, resolution: int = 32) -> None:
|
|
63
|
+
"""Initialize the renderer.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
default_size: Default size for unbounded surfaces like planes
|
|
67
|
+
resolution: Number of segments for curved surfaces
|
|
68
|
+
"""
|
|
69
|
+
self.default_size = default_size
|
|
70
|
+
self.resolution = resolution
|
|
71
|
+
|
|
72
|
+
def render(self, surface: "Surface") -> dict[str, Any]:
|
|
73
|
+
"""Render a surface to mesh data.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
surface: The surface to render
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dict with 'vertices', 'indices', and 'wireframe' keys
|
|
80
|
+
"""
|
|
81
|
+
# Import here to avoid circular imports
|
|
82
|
+
from lsurf.surfaces import (
|
|
83
|
+
AnnularPlaneSurface,
|
|
84
|
+
BoundedPlaneSurface,
|
|
85
|
+
CurvedWaveSurface,
|
|
86
|
+
GerstnerWaveSurface,
|
|
87
|
+
GPUCurvedWaveSurface,
|
|
88
|
+
GPUGerstnerWaveSurface,
|
|
89
|
+
GPUMultiCurvedWaveSurface,
|
|
90
|
+
LocalRecordingSphereSurface,
|
|
91
|
+
PlaneSurface,
|
|
92
|
+
RecordingSphereSurface,
|
|
93
|
+
SphereSurface,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Dispatch to appropriate renderer based on surface type
|
|
97
|
+
if isinstance(surface, BoundedPlaneSurface):
|
|
98
|
+
return self._render_bounded_plane(surface)
|
|
99
|
+
elif isinstance(surface, AnnularPlaneSurface):
|
|
100
|
+
return self._render_annular_plane(surface)
|
|
101
|
+
elif isinstance(surface, PlaneSurface):
|
|
102
|
+
return self._render_plane(surface)
|
|
103
|
+
elif isinstance(surface, LocalRecordingSphereSurface):
|
|
104
|
+
return self._render_local_sphere(surface)
|
|
105
|
+
elif isinstance(surface, RecordingSphereSurface):
|
|
106
|
+
return self._render_recording_sphere(surface)
|
|
107
|
+
elif isinstance(surface, SphereSurface):
|
|
108
|
+
return self._render_sphere(surface)
|
|
109
|
+
elif isinstance(
|
|
110
|
+
surface, (GPUGerstnerWaveSurface, GPUCurvedWaveSurface, GerstnerWaveSurface)
|
|
111
|
+
):
|
|
112
|
+
return self._render_wave(surface)
|
|
113
|
+
elif isinstance(surface, (GPUMultiCurvedWaveSurface, CurvedWaveSurface)):
|
|
114
|
+
return self._render_multi_wave(surface)
|
|
115
|
+
else:
|
|
116
|
+
# Fallback: render as a small marker
|
|
117
|
+
return self._render_marker(getattr(surface, "point", (0, 0, 0)))
|
|
118
|
+
|
|
119
|
+
def _render_plane(self, surface: "PlaneSurface") -> dict[str, Any]:
|
|
120
|
+
"""Render an infinite plane as a finite wireframe rectangle."""
|
|
121
|
+
point = np.array(surface.point, dtype=np.float32)
|
|
122
|
+
normal = np.array(surface.normal, dtype=np.float32)
|
|
123
|
+
normal = normal / np.linalg.norm(normal)
|
|
124
|
+
|
|
125
|
+
# Create orthonormal basis
|
|
126
|
+
u, v = self._create_perpendicular_basis(normal)
|
|
127
|
+
|
|
128
|
+
# Create rectangle vertices
|
|
129
|
+
size = self.default_size
|
|
130
|
+
corners = [
|
|
131
|
+
point + size * u + size * v,
|
|
132
|
+
point - size * u + size * v,
|
|
133
|
+
point - size * u - size * v,
|
|
134
|
+
point + size * u - size * v,
|
|
135
|
+
]
|
|
136
|
+
vertices = np.array(corners, dtype=np.float32)
|
|
137
|
+
|
|
138
|
+
# Wireframe indices (line segments)
|
|
139
|
+
indices = np.array([0, 1, 1, 2, 2, 3, 3, 0], dtype=np.uint32)
|
|
140
|
+
|
|
141
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
142
|
+
|
|
143
|
+
def _render_bounded_plane(self, surface: "BoundedPlaneSurface") -> dict[str, Any]:
|
|
144
|
+
"""Render a bounded rectangular plane as a solid quad."""
|
|
145
|
+
point = np.array(surface.point, dtype=np.float32)
|
|
146
|
+
normal = np.array(surface.normal, dtype=np.float32)
|
|
147
|
+
normal = normal / np.linalg.norm(normal)
|
|
148
|
+
|
|
149
|
+
width = surface.width
|
|
150
|
+
height = surface.height
|
|
151
|
+
|
|
152
|
+
# Create orthonormal basis
|
|
153
|
+
u, v = self._create_perpendicular_basis(normal)
|
|
154
|
+
|
|
155
|
+
# Create rectangle vertices
|
|
156
|
+
hw, hh = width / 2, height / 2
|
|
157
|
+
corners = [
|
|
158
|
+
point + hw * u + hh * v,
|
|
159
|
+
point - hw * u + hh * v,
|
|
160
|
+
point - hw * u - hh * v,
|
|
161
|
+
point + hw * u - hh * v,
|
|
162
|
+
]
|
|
163
|
+
vertices = np.array(corners, dtype=np.float32)
|
|
164
|
+
|
|
165
|
+
# Triangle indices for solid rendering
|
|
166
|
+
indices = np.array([0, 1, 2, 0, 2, 3], dtype=np.uint32)
|
|
167
|
+
|
|
168
|
+
return {"vertices": vertices, "indices": indices, "wireframe": False}
|
|
169
|
+
|
|
170
|
+
def _render_annular_plane(self, surface: "AnnularPlaneSurface") -> dict[str, Any]:
|
|
171
|
+
"""Render an annular (ring) plane."""
|
|
172
|
+
# AnnularPlaneSurface uses 'center' not 'point'
|
|
173
|
+
center = np.array(surface.center, dtype=np.float32)
|
|
174
|
+
normal = np.array(surface.normal, dtype=np.float32)
|
|
175
|
+
normal = normal / np.linalg.norm(normal)
|
|
176
|
+
|
|
177
|
+
inner_r = surface.inner_radius
|
|
178
|
+
outer_r = surface.outer_radius
|
|
179
|
+
|
|
180
|
+
# Create orthonormal basis
|
|
181
|
+
u, v = self._create_perpendicular_basis(normal)
|
|
182
|
+
|
|
183
|
+
# Generate ring vertices
|
|
184
|
+
n_segments = self.resolution
|
|
185
|
+
angles = np.linspace(0, 2 * np.pi, n_segments, endpoint=False)
|
|
186
|
+
|
|
187
|
+
vertices = []
|
|
188
|
+
for angle in angles:
|
|
189
|
+
c, s = np.cos(angle), np.sin(angle)
|
|
190
|
+
# Inner vertex
|
|
191
|
+
vertices.append(center + inner_r * (c * u + s * v))
|
|
192
|
+
# Outer vertex
|
|
193
|
+
vertices.append(center + outer_r * (c * u + s * v))
|
|
194
|
+
|
|
195
|
+
vertices = np.array(vertices, dtype=np.float32)
|
|
196
|
+
|
|
197
|
+
# Triangle indices for the ring
|
|
198
|
+
indices = []
|
|
199
|
+
for i in range(n_segments):
|
|
200
|
+
i0 = i * 2
|
|
201
|
+
i1 = i * 2 + 1
|
|
202
|
+
i2 = ((i + 1) % n_segments) * 2
|
|
203
|
+
i3 = ((i + 1) % n_segments) * 2 + 1
|
|
204
|
+
# Two triangles per segment
|
|
205
|
+
indices.extend([i0, i1, i3, i0, i3, i2])
|
|
206
|
+
|
|
207
|
+
indices = np.array(indices, dtype=np.uint32)
|
|
208
|
+
|
|
209
|
+
return {"vertices": vertices, "indices": indices, "wireframe": False}
|
|
210
|
+
|
|
211
|
+
def _render_sphere(self, surface: "SphereSurface") -> dict[str, Any]:
|
|
212
|
+
"""Render a sphere as a wireframe."""
|
|
213
|
+
center = np.array(surface.center, dtype=np.float32)
|
|
214
|
+
radius = surface.radius
|
|
215
|
+
|
|
216
|
+
return self._generate_sphere_mesh(center, radius, wireframe=True)
|
|
217
|
+
|
|
218
|
+
def _render_local_sphere(
|
|
219
|
+
self, surface: "LocalRecordingSphereSurface"
|
|
220
|
+
) -> dict[str, Any]:
|
|
221
|
+
"""Render a local recording sphere."""
|
|
222
|
+
center = np.array(surface.center, dtype=np.float32)
|
|
223
|
+
radius = surface.radius
|
|
224
|
+
|
|
225
|
+
return self._generate_sphere_mesh(center, radius, wireframe=True)
|
|
226
|
+
|
|
227
|
+
def _render_recording_sphere(
|
|
228
|
+
self, surface: "RecordingSphereSurface"
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
"""Render a recording sphere at altitude.
|
|
231
|
+
|
|
232
|
+
For large spheres (like Earth-scale), we only render a small
|
|
233
|
+
portion near the relevant area.
|
|
234
|
+
"""
|
|
235
|
+
# For now, render as a horizontal plane at the altitude
|
|
236
|
+
# (full Earth sphere would be too large)
|
|
237
|
+
earth_center = np.array(surface.earth_center, dtype=np.float32)
|
|
238
|
+
radius = surface.earth_radius + surface.altitude
|
|
239
|
+
|
|
240
|
+
# Create a circular section at the top of the sphere
|
|
241
|
+
n_segments = self.resolution
|
|
242
|
+
angles = np.linspace(0, 2 * np.pi, n_segments, endpoint=False)
|
|
243
|
+
|
|
244
|
+
# Size of the section to render
|
|
245
|
+
section_radius = min(radius * 0.1, self.default_size * 5)
|
|
246
|
+
|
|
247
|
+
# Center at top of sphere
|
|
248
|
+
section_center = earth_center + np.array([0, 0, radius], dtype=np.float32)
|
|
249
|
+
|
|
250
|
+
vertices = [section_center]
|
|
251
|
+
for angle in angles:
|
|
252
|
+
x = section_radius * np.cos(angle)
|
|
253
|
+
y = section_radius * np.sin(angle)
|
|
254
|
+
vertices.append(section_center + np.array([x, y, 0], dtype=np.float32))
|
|
255
|
+
|
|
256
|
+
vertices = np.array(vertices, dtype=np.float32)
|
|
257
|
+
|
|
258
|
+
# Triangle fan indices
|
|
259
|
+
indices = []
|
|
260
|
+
for i in range(n_segments):
|
|
261
|
+
indices.extend([0, i + 1, ((i + 1) % n_segments) + 1])
|
|
262
|
+
|
|
263
|
+
indices = np.array(indices, dtype=np.uint32)
|
|
264
|
+
|
|
265
|
+
return {"vertices": vertices, "indices": indices, "wireframe": False}
|
|
266
|
+
|
|
267
|
+
def _render_wave(self, surface: Any) -> dict[str, Any]:
|
|
268
|
+
"""Render a single Gerstner wave surface."""
|
|
269
|
+
# Get wave parameters
|
|
270
|
+
amplitude = getattr(surface, "amplitude", 1.0)
|
|
271
|
+
wavelength = getattr(surface, "wavelength", 10.0)
|
|
272
|
+
direction = np.array(getattr(surface, "direction", (1, 0)), dtype=np.float32)
|
|
273
|
+
direction = direction / np.linalg.norm(direction)
|
|
274
|
+
reference_z = getattr(surface, "reference_z", 0.0)
|
|
275
|
+
phase = getattr(surface, "phase", 0.0)
|
|
276
|
+
time = getattr(surface, "time", 0.0)
|
|
277
|
+
|
|
278
|
+
# Generate mesh grid
|
|
279
|
+
size = max(wavelength * 3, self.default_size)
|
|
280
|
+
n = self.resolution
|
|
281
|
+
x = np.linspace(-size, size, n)
|
|
282
|
+
y = np.linspace(-size, size, n)
|
|
283
|
+
X, Y = np.meshgrid(x, y)
|
|
284
|
+
|
|
285
|
+
# Compute wave height
|
|
286
|
+
k = 2 * np.pi / wavelength
|
|
287
|
+
omega = np.sqrt(9.81 * k) # Deep water dispersion
|
|
288
|
+
dot = direction[0] * X + direction[1] * Y
|
|
289
|
+
Z = reference_z + amplitude * np.cos(k * dot - omega * time + phase)
|
|
290
|
+
|
|
291
|
+
return self._grid_to_mesh(X, Y, Z)
|
|
292
|
+
|
|
293
|
+
def _render_multi_wave(self, surface: Any) -> dict[str, Any]:
|
|
294
|
+
"""Render a multi-wave surface."""
|
|
295
|
+
wave_params = getattr(surface, "wave_params", [])
|
|
296
|
+
time = getattr(surface, "time", 0.0)
|
|
297
|
+
|
|
298
|
+
if not wave_params:
|
|
299
|
+
# Empty wave, render as flat plane
|
|
300
|
+
return self._render_flat_grid(0.0)
|
|
301
|
+
|
|
302
|
+
# Determine grid size from wavelengths
|
|
303
|
+
max_wavelength = max(wp.wavelength for wp in wave_params)
|
|
304
|
+
size = max(max_wavelength * 3, self.default_size)
|
|
305
|
+
n = self.resolution
|
|
306
|
+
x = np.linspace(-size, size, n)
|
|
307
|
+
y = np.linspace(-size, size, n)
|
|
308
|
+
X, Y = np.meshgrid(x, y)
|
|
309
|
+
|
|
310
|
+
# Sum all wave contributions
|
|
311
|
+
Z = np.zeros_like(X)
|
|
312
|
+
for wp in wave_params:
|
|
313
|
+
direction = np.array(wp.direction_normalized, dtype=np.float32)
|
|
314
|
+
k = wp.wave_number
|
|
315
|
+
omega = wp.angular_frequency
|
|
316
|
+
dot = direction[0] * X + direction[1] * Y
|
|
317
|
+
Z += wp.amplitude * np.cos(k * dot - omega * time + wp.phase)
|
|
318
|
+
|
|
319
|
+
return self._grid_to_mesh(X, Y, Z)
|
|
320
|
+
|
|
321
|
+
def _render_flat_grid(self, z: float) -> dict[str, Any]:
|
|
322
|
+
"""Render a flat grid at height z."""
|
|
323
|
+
size = self.default_size
|
|
324
|
+
n = self.resolution
|
|
325
|
+
x = np.linspace(-size, size, n)
|
|
326
|
+
y = np.linspace(-size, size, n)
|
|
327
|
+
X, Y = np.meshgrid(x, y)
|
|
328
|
+
Z = np.full_like(X, z)
|
|
329
|
+
|
|
330
|
+
return self._grid_to_mesh(X, Y, Z)
|
|
331
|
+
|
|
332
|
+
def _render_marker(self, position: tuple) -> dict[str, Any]:
|
|
333
|
+
"""Render a small marker at a position (fallback)."""
|
|
334
|
+
pos = np.array(position, dtype=np.float32)
|
|
335
|
+
size = 0.5
|
|
336
|
+
|
|
337
|
+
# Small cube
|
|
338
|
+
vertices = np.array(
|
|
339
|
+
[
|
|
340
|
+
pos + [-size, -size, -size],
|
|
341
|
+
pos + [size, -size, -size],
|
|
342
|
+
pos + [size, size, -size],
|
|
343
|
+
pos + [-size, size, -size],
|
|
344
|
+
pos + [-size, -size, size],
|
|
345
|
+
pos + [size, -size, size],
|
|
346
|
+
pos + [size, size, size],
|
|
347
|
+
pos + [-size, size, size],
|
|
348
|
+
],
|
|
349
|
+
dtype=np.float32,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Wireframe indices
|
|
353
|
+
indices = np.array(
|
|
354
|
+
[
|
|
355
|
+
0,
|
|
356
|
+
1,
|
|
357
|
+
1,
|
|
358
|
+
2,
|
|
359
|
+
2,
|
|
360
|
+
3,
|
|
361
|
+
3,
|
|
362
|
+
0, # Bottom
|
|
363
|
+
4,
|
|
364
|
+
5,
|
|
365
|
+
5,
|
|
366
|
+
6,
|
|
367
|
+
6,
|
|
368
|
+
7,
|
|
369
|
+
7,
|
|
370
|
+
4, # Top
|
|
371
|
+
0,
|
|
372
|
+
4,
|
|
373
|
+
1,
|
|
374
|
+
5,
|
|
375
|
+
2,
|
|
376
|
+
6,
|
|
377
|
+
3,
|
|
378
|
+
7, # Vertical
|
|
379
|
+
],
|
|
380
|
+
dtype=np.uint32,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
384
|
+
|
|
385
|
+
def _generate_sphere_mesh(
|
|
386
|
+
self, center: np.ndarray, radius: float, wireframe: bool = True
|
|
387
|
+
) -> dict[str, Any]:
|
|
388
|
+
"""Generate a sphere mesh."""
|
|
389
|
+
n_lat = self.resolution // 2
|
|
390
|
+
n_lon = self.resolution
|
|
391
|
+
|
|
392
|
+
vertices = []
|
|
393
|
+
for i in range(n_lat + 1):
|
|
394
|
+
lat = np.pi * i / n_lat - np.pi / 2
|
|
395
|
+
for j in range(n_lon):
|
|
396
|
+
lon = 2 * np.pi * j / n_lon
|
|
397
|
+
x = radius * np.cos(lat) * np.cos(lon)
|
|
398
|
+
y = radius * np.cos(lat) * np.sin(lon)
|
|
399
|
+
z = radius * np.sin(lat)
|
|
400
|
+
vertices.append(center + np.array([x, y, z], dtype=np.float32))
|
|
401
|
+
|
|
402
|
+
vertices = np.array(vertices, dtype=np.float32)
|
|
403
|
+
|
|
404
|
+
if wireframe:
|
|
405
|
+
# Wireframe: latitude and longitude lines
|
|
406
|
+
indices = []
|
|
407
|
+
# Latitude lines
|
|
408
|
+
for i in range(n_lat + 1):
|
|
409
|
+
for j in range(n_lon):
|
|
410
|
+
idx1 = i * n_lon + j
|
|
411
|
+
idx2 = i * n_lon + (j + 1) % n_lon
|
|
412
|
+
indices.extend([idx1, idx2])
|
|
413
|
+
# Longitude lines
|
|
414
|
+
for j in range(n_lon):
|
|
415
|
+
for i in range(n_lat):
|
|
416
|
+
idx1 = i * n_lon + j
|
|
417
|
+
idx2 = (i + 1) * n_lon + j
|
|
418
|
+
indices.extend([idx1, idx2])
|
|
419
|
+
|
|
420
|
+
indices = np.array(indices, dtype=np.uint32)
|
|
421
|
+
else:
|
|
422
|
+
# Solid: triangles
|
|
423
|
+
indices = []
|
|
424
|
+
for i in range(n_lat):
|
|
425
|
+
for j in range(n_lon):
|
|
426
|
+
i00 = i * n_lon + j
|
|
427
|
+
i10 = (i + 1) * n_lon + j
|
|
428
|
+
i01 = i * n_lon + (j + 1) % n_lon
|
|
429
|
+
i11 = (i + 1) * n_lon + (j + 1) % n_lon
|
|
430
|
+
indices.extend([i00, i10, i11, i00, i11, i01])
|
|
431
|
+
|
|
432
|
+
indices = np.array(indices, dtype=np.uint32)
|
|
433
|
+
|
|
434
|
+
return {"vertices": vertices, "indices": indices, "wireframe": wireframe}
|
|
435
|
+
|
|
436
|
+
def _grid_to_mesh(
|
|
437
|
+
self, X: np.ndarray, Y: np.ndarray, Z: np.ndarray
|
|
438
|
+
) -> dict[str, Any]:
|
|
439
|
+
"""Convert a grid to a triangle mesh."""
|
|
440
|
+
n_rows, n_cols = X.shape
|
|
441
|
+
|
|
442
|
+
# Create vertices
|
|
443
|
+
vertices = np.stack([X.ravel(), Y.ravel(), Z.ravel()], axis=1).astype(
|
|
444
|
+
np.float32
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Create triangle indices
|
|
448
|
+
indices = []
|
|
449
|
+
for i in range(n_rows - 1):
|
|
450
|
+
for j in range(n_cols - 1):
|
|
451
|
+
i00 = i * n_cols + j
|
|
452
|
+
i10 = (i + 1) * n_cols + j
|
|
453
|
+
i01 = i * n_cols + (j + 1)
|
|
454
|
+
i11 = (i + 1) * n_cols + (j + 1)
|
|
455
|
+
# Two triangles per cell
|
|
456
|
+
indices.extend([i00, i10, i11, i00, i11, i01])
|
|
457
|
+
|
|
458
|
+
indices = np.array(indices, dtype=np.uint32)
|
|
459
|
+
|
|
460
|
+
return {"vertices": vertices, "indices": indices, "wireframe": False}
|
|
461
|
+
|
|
462
|
+
def _create_perpendicular_basis(
|
|
463
|
+
self, normal: np.ndarray
|
|
464
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
465
|
+
"""Create two perpendicular unit vectors to the given normal."""
|
|
466
|
+
# Find a vector not parallel to normal
|
|
467
|
+
if abs(normal[0]) < 0.9:
|
|
468
|
+
ref = np.array([1, 0, 0], dtype=np.float32)
|
|
469
|
+
else:
|
|
470
|
+
ref = np.array([0, 1, 0], dtype=np.float32)
|
|
471
|
+
|
|
472
|
+
u = np.cross(normal, ref)
|
|
473
|
+
u = u / np.linalg.norm(u)
|
|
474
|
+
v = np.cross(normal, u)
|
|
475
|
+
v = v / np.linalg.norm(v)
|
|
476
|
+
|
|
477
|
+
return u, v
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
"""GUI view components."""
|
|
35
|
+
|
|
36
|
+
from .config_editor import ConfigEditorPanel
|
|
37
|
+
from .results import ResultsPanel
|
|
38
|
+
from .scene_tree import SceneTreePanel
|
|
39
|
+
from .viewport_3d import Viewport3D
|
|
40
|
+
from .visualizations import VisualizationPanel
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"Viewport3D",
|
|
44
|
+
"SceneTreePanel",
|
|
45
|
+
"ResultsPanel",
|
|
46
|
+
"ConfigEditorPanel",
|
|
47
|
+
"VisualizationPanel",
|
|
48
|
+
]
|