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,505 @@
|
|
|
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
|
+
"""Source renderer - visualizes ray sources.
|
|
35
|
+
|
|
36
|
+
Generates 3D representations of ray sources including spheres for
|
|
37
|
+
point sources, cylinders for collimated beams, and cones for diverging beams.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from typing import TYPE_CHECKING, Any
|
|
41
|
+
|
|
42
|
+
import numpy as np
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from lsurf.sources import (
|
|
46
|
+
CollimatedBeam,
|
|
47
|
+
CustomRaySource,
|
|
48
|
+
DivergingBeam,
|
|
49
|
+
GaussianBeam,
|
|
50
|
+
ParallelBeamFromPositions,
|
|
51
|
+
PointSource,
|
|
52
|
+
RaySource,
|
|
53
|
+
UniformDivergingBeam,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SourceRenderer:
|
|
58
|
+
"""Renders ray sources to 3D mesh data for Dear PyGui."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, arrow_scale: float = 1.0, resolution: int = 16) -> None:
|
|
61
|
+
"""Initialize the renderer.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
arrow_scale: Scale factor for direction arrows
|
|
65
|
+
resolution: Number of segments for curved surfaces
|
|
66
|
+
"""
|
|
67
|
+
self.arrow_scale = arrow_scale
|
|
68
|
+
self.resolution = resolution
|
|
69
|
+
|
|
70
|
+
def render(self, source: "RaySource") -> dict[str, Any]:
|
|
71
|
+
"""Render a source to mesh data.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
source: The source to render
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dict with 'vertices', 'indices', and 'wireframe' keys
|
|
78
|
+
"""
|
|
79
|
+
from lsurf.sources import (
|
|
80
|
+
CollimatedBeam,
|
|
81
|
+
CustomRaySource,
|
|
82
|
+
DivergingBeam,
|
|
83
|
+
GaussianBeam,
|
|
84
|
+
ParallelBeamFromPositions,
|
|
85
|
+
PointSource,
|
|
86
|
+
UniformDivergingBeam,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if isinstance(source, PointSource):
|
|
90
|
+
return self._render_point_source(source)
|
|
91
|
+
elif isinstance(source, CollimatedBeam):
|
|
92
|
+
return self._render_collimated_beam(source)
|
|
93
|
+
elif isinstance(source, (DivergingBeam, UniformDivergingBeam)):
|
|
94
|
+
return self._render_diverging_beam(source)
|
|
95
|
+
elif isinstance(source, GaussianBeam):
|
|
96
|
+
return self._render_gaussian_beam(source)
|
|
97
|
+
elif isinstance(source, ParallelBeamFromPositions):
|
|
98
|
+
return self._render_parallel_from_positions(source)
|
|
99
|
+
elif isinstance(source, CustomRaySource):
|
|
100
|
+
return self._render_custom_source(source)
|
|
101
|
+
else:
|
|
102
|
+
# Fallback: try to render based on available attributes
|
|
103
|
+
return self._render_fallback(source)
|
|
104
|
+
|
|
105
|
+
def _render_point_source(self, source: "PointSource") -> dict[str, Any]:
|
|
106
|
+
"""Render a point source as a sphere with direction arrows."""
|
|
107
|
+
position = np.array(source.position, dtype=np.float32)
|
|
108
|
+
|
|
109
|
+
# Size based on power (arbitrary scaling)
|
|
110
|
+
size = 0.5 * self.arrow_scale
|
|
111
|
+
|
|
112
|
+
# Generate sphere
|
|
113
|
+
sphere_data = self._generate_sphere(position, size)
|
|
114
|
+
|
|
115
|
+
# Generate arrows pointing outward in cardinal directions
|
|
116
|
+
arrows = self._generate_point_arrows(position, size * 2)
|
|
117
|
+
|
|
118
|
+
# Combine sphere and arrows
|
|
119
|
+
vertices = np.vstack([sphere_data["vertices"], arrows["vertices"]])
|
|
120
|
+
indices = np.concatenate(
|
|
121
|
+
[sphere_data["indices"], arrows["indices"] + len(sphere_data["vertices"])]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
125
|
+
|
|
126
|
+
def _render_collimated_beam(self, source: "CollimatedBeam") -> dict[str, Any]:
|
|
127
|
+
"""Render a collimated beam as a cylinder with arrow."""
|
|
128
|
+
center = np.array(source.center, dtype=np.float32)
|
|
129
|
+
direction = np.array(source.direction, dtype=np.float32)
|
|
130
|
+
direction = direction / np.linalg.norm(direction)
|
|
131
|
+
radius = source.radius
|
|
132
|
+
|
|
133
|
+
# Cylinder length proportional to radius
|
|
134
|
+
length = max(radius * 3, self.arrow_scale * 2)
|
|
135
|
+
|
|
136
|
+
# Generate cylinder
|
|
137
|
+
cylinder_data = self._generate_cylinder(center, direction, radius, length)
|
|
138
|
+
|
|
139
|
+
# Generate arrow at the front
|
|
140
|
+
arrow_start = center + direction * length * 0.5
|
|
141
|
+
arrow_data = self._generate_arrow(
|
|
142
|
+
arrow_start, direction, length * 0.3, radius * 0.3
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Combine
|
|
146
|
+
vertices = np.vstack([cylinder_data["vertices"], arrow_data["vertices"]])
|
|
147
|
+
indices = np.concatenate(
|
|
148
|
+
[
|
|
149
|
+
cylinder_data["indices"],
|
|
150
|
+
arrow_data["indices"] + len(cylinder_data["vertices"]),
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
155
|
+
|
|
156
|
+
def _render_diverging_beam(
|
|
157
|
+
self, source: "DivergingBeam | UniformDivergingBeam"
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
"""Render a diverging beam as a cone."""
|
|
160
|
+
origin = np.array(source.origin, dtype=np.float32)
|
|
161
|
+
direction = np.array(source.mean_direction, dtype=np.float32)
|
|
162
|
+
direction = direction / np.linalg.norm(direction)
|
|
163
|
+
half_angle = source.divergence_angle
|
|
164
|
+
|
|
165
|
+
# Cone length
|
|
166
|
+
length = self.arrow_scale * 3
|
|
167
|
+
radius = length * np.tan(half_angle)
|
|
168
|
+
|
|
169
|
+
# Generate cone
|
|
170
|
+
return self._generate_cone(origin, direction, radius, length)
|
|
171
|
+
|
|
172
|
+
def _render_gaussian_beam(self, source: "GaussianBeam") -> dict[str, Any]:
|
|
173
|
+
"""Render a Gaussian beam with waist indicator."""
|
|
174
|
+
waist_pos = np.array(source.waist_position, dtype=np.float32)
|
|
175
|
+
direction = np.array(source.direction, dtype=np.float32)
|
|
176
|
+
direction = direction / np.linalg.norm(direction)
|
|
177
|
+
waist_radius = source.waist_radius
|
|
178
|
+
|
|
179
|
+
# Generate waist circle
|
|
180
|
+
waist_data = self._generate_circle(waist_pos, direction, waist_radius)
|
|
181
|
+
|
|
182
|
+
# Generate beam axis with arrow
|
|
183
|
+
length = self.arrow_scale * 4
|
|
184
|
+
axis_start = waist_pos - direction * length * 0.3
|
|
185
|
+
arrow_data = self._generate_arrow(
|
|
186
|
+
axis_start, direction, length, waist_radius * 0.2
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Combine
|
|
190
|
+
vertices = np.vstack([waist_data["vertices"], arrow_data["vertices"]])
|
|
191
|
+
indices = np.concatenate(
|
|
192
|
+
[waist_data["indices"], arrow_data["indices"] + len(waist_data["vertices"])]
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
196
|
+
|
|
197
|
+
def _render_parallel_from_positions(
|
|
198
|
+
self, source: "ParallelBeamFromPositions"
|
|
199
|
+
) -> dict[str, Any]:
|
|
200
|
+
"""Render parallel rays from explicit positions."""
|
|
201
|
+
positions = np.array(source.positions, dtype=np.float32)
|
|
202
|
+
direction = np.array(source.direction, dtype=np.float32)
|
|
203
|
+
direction = direction / np.linalg.norm(direction)
|
|
204
|
+
|
|
205
|
+
# Render a sample of positions as points with arrows
|
|
206
|
+
max_points = 100
|
|
207
|
+
if len(positions) > max_points:
|
|
208
|
+
indices_sample = np.random.choice(len(positions), max_points, replace=False)
|
|
209
|
+
positions = positions[indices_sample]
|
|
210
|
+
|
|
211
|
+
all_vertices = []
|
|
212
|
+
all_indices = []
|
|
213
|
+
vertex_offset = 0
|
|
214
|
+
|
|
215
|
+
for pos in positions:
|
|
216
|
+
arrow_data = self._generate_arrow(
|
|
217
|
+
pos, direction, self.arrow_scale, self.arrow_scale * 0.1
|
|
218
|
+
)
|
|
219
|
+
all_vertices.append(arrow_data["vertices"])
|
|
220
|
+
all_indices.append(arrow_data["indices"] + vertex_offset)
|
|
221
|
+
vertex_offset += len(arrow_data["vertices"])
|
|
222
|
+
|
|
223
|
+
if all_vertices:
|
|
224
|
+
vertices = np.vstack(all_vertices)
|
|
225
|
+
indices = np.concatenate(all_indices)
|
|
226
|
+
else:
|
|
227
|
+
vertices = np.zeros((0, 3), dtype=np.float32)
|
|
228
|
+
indices = np.zeros(0, dtype=np.uint32)
|
|
229
|
+
|
|
230
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
231
|
+
|
|
232
|
+
def _render_custom_source(self, source: "CustomRaySource") -> dict[str, Any]:
|
|
233
|
+
"""Render a custom source as a point cloud."""
|
|
234
|
+
positions = np.array(source.positions, dtype=np.float32)
|
|
235
|
+
directions = np.array(source.directions, dtype=np.float32)
|
|
236
|
+
|
|
237
|
+
# Render a sample of rays as arrows
|
|
238
|
+
max_rays = 50
|
|
239
|
+
if len(positions) > max_rays:
|
|
240
|
+
indices_sample = np.random.choice(len(positions), max_rays, replace=False)
|
|
241
|
+
positions = positions[indices_sample]
|
|
242
|
+
directions = directions[indices_sample]
|
|
243
|
+
|
|
244
|
+
all_vertices = []
|
|
245
|
+
all_indices = []
|
|
246
|
+
vertex_offset = 0
|
|
247
|
+
|
|
248
|
+
for pos, dir_ in zip(positions, directions):
|
|
249
|
+
dir_norm = dir_ / np.linalg.norm(dir_)
|
|
250
|
+
arrow_data = self._generate_arrow(
|
|
251
|
+
pos, dir_norm, self.arrow_scale * 0.5, self.arrow_scale * 0.05
|
|
252
|
+
)
|
|
253
|
+
all_vertices.append(arrow_data["vertices"])
|
|
254
|
+
all_indices.append(arrow_data["indices"] + vertex_offset)
|
|
255
|
+
vertex_offset += len(arrow_data["vertices"])
|
|
256
|
+
|
|
257
|
+
if all_vertices:
|
|
258
|
+
vertices = np.vstack(all_vertices)
|
|
259
|
+
indices = np.concatenate(all_indices)
|
|
260
|
+
else:
|
|
261
|
+
vertices = np.zeros((0, 3), dtype=np.float32)
|
|
262
|
+
indices = np.zeros(0, dtype=np.uint32)
|
|
263
|
+
|
|
264
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
265
|
+
|
|
266
|
+
def _render_fallback(self, source: "RaySource") -> dict[str, Any]:
|
|
267
|
+
"""Fallback renderer for unknown source types."""
|
|
268
|
+
# Try to find a position
|
|
269
|
+
position = np.array([0, 0, 0], dtype=np.float32)
|
|
270
|
+
for attr in ["position", "center", "origin", "waist_position"]:
|
|
271
|
+
if hasattr(source, attr):
|
|
272
|
+
position = np.array(getattr(source, attr), dtype=np.float32)
|
|
273
|
+
break
|
|
274
|
+
|
|
275
|
+
# Render as a simple marker
|
|
276
|
+
return self._generate_sphere(position, self.arrow_scale * 0.5)
|
|
277
|
+
|
|
278
|
+
def _generate_sphere(self, center: np.ndarray, radius: float) -> dict[str, Any]:
|
|
279
|
+
"""Generate a wireframe sphere."""
|
|
280
|
+
n_lat = self.resolution // 2
|
|
281
|
+
n_lon = self.resolution
|
|
282
|
+
|
|
283
|
+
vertices = []
|
|
284
|
+
for i in range(n_lat + 1):
|
|
285
|
+
lat = np.pi * i / n_lat - np.pi / 2
|
|
286
|
+
for j in range(n_lon):
|
|
287
|
+
lon = 2 * np.pi * j / n_lon
|
|
288
|
+
x = radius * np.cos(lat) * np.cos(lon)
|
|
289
|
+
y = radius * np.cos(lat) * np.sin(lon)
|
|
290
|
+
z = radius * np.sin(lat)
|
|
291
|
+
vertices.append(center + np.array([x, y, z], dtype=np.float32))
|
|
292
|
+
|
|
293
|
+
vertices = np.array(vertices, dtype=np.float32)
|
|
294
|
+
|
|
295
|
+
# Wireframe indices
|
|
296
|
+
indices = []
|
|
297
|
+
for i in range(n_lat + 1):
|
|
298
|
+
for j in range(n_lon):
|
|
299
|
+
idx1 = i * n_lon + j
|
|
300
|
+
idx2 = i * n_lon + (j + 1) % n_lon
|
|
301
|
+
indices.extend([idx1, idx2])
|
|
302
|
+
for j in range(n_lon):
|
|
303
|
+
for i in range(n_lat):
|
|
304
|
+
idx1 = i * n_lon + j
|
|
305
|
+
idx2 = (i + 1) * n_lon + j
|
|
306
|
+
indices.extend([idx1, idx2])
|
|
307
|
+
|
|
308
|
+
indices = np.array(indices, dtype=np.uint32)
|
|
309
|
+
|
|
310
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
311
|
+
|
|
312
|
+
def _generate_point_arrows(
|
|
313
|
+
self, center: np.ndarray, length: float
|
|
314
|
+
) -> dict[str, Any]:
|
|
315
|
+
"""Generate arrows pointing in cardinal directions from a point."""
|
|
316
|
+
directions = [
|
|
317
|
+
np.array([1, 0, 0], dtype=np.float32),
|
|
318
|
+
np.array([-1, 0, 0], dtype=np.float32),
|
|
319
|
+
np.array([0, 1, 0], dtype=np.float32),
|
|
320
|
+
np.array([0, -1, 0], dtype=np.float32),
|
|
321
|
+
np.array([0, 0, 1], dtype=np.float32),
|
|
322
|
+
np.array([0, 0, -1], dtype=np.float32),
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
all_vertices = []
|
|
326
|
+
all_indices = []
|
|
327
|
+
vertex_offset = 0
|
|
328
|
+
|
|
329
|
+
for direction in directions:
|
|
330
|
+
arrow_data = self._generate_arrow(center, direction, length, length * 0.15)
|
|
331
|
+
all_vertices.append(arrow_data["vertices"])
|
|
332
|
+
all_indices.append(arrow_data["indices"] + vertex_offset)
|
|
333
|
+
vertex_offset += len(arrow_data["vertices"])
|
|
334
|
+
|
|
335
|
+
vertices = (
|
|
336
|
+
np.vstack(all_vertices)
|
|
337
|
+
if all_vertices
|
|
338
|
+
else np.zeros((0, 3), dtype=np.float32)
|
|
339
|
+
)
|
|
340
|
+
indices = (
|
|
341
|
+
np.concatenate(all_indices) if all_indices else np.zeros(0, dtype=np.uint32)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
345
|
+
|
|
346
|
+
def _generate_arrow(
|
|
347
|
+
self, start: np.ndarray, direction: np.ndarray, length: float, head_size: float
|
|
348
|
+
) -> dict[str, Any]:
|
|
349
|
+
"""Generate an arrow (line with arrowhead)."""
|
|
350
|
+
end = start + direction * length
|
|
351
|
+
head_base = end - direction * head_size
|
|
352
|
+
|
|
353
|
+
# Create perpendicular basis for arrowhead
|
|
354
|
+
if abs(direction[0]) < 0.9:
|
|
355
|
+
ref = np.array([1, 0, 0], dtype=np.float32)
|
|
356
|
+
else:
|
|
357
|
+
ref = np.array([0, 1, 0], dtype=np.float32)
|
|
358
|
+
u = np.cross(direction, ref)
|
|
359
|
+
u = u / np.linalg.norm(u)
|
|
360
|
+
v = np.cross(direction, u)
|
|
361
|
+
|
|
362
|
+
# Arrow vertices
|
|
363
|
+
vertices = np.array(
|
|
364
|
+
[
|
|
365
|
+
start,
|
|
366
|
+
end,
|
|
367
|
+
head_base + u * head_size * 0.5,
|
|
368
|
+
head_base - u * head_size * 0.5,
|
|
369
|
+
head_base + v * head_size * 0.5,
|
|
370
|
+
head_base - v * head_size * 0.5,
|
|
371
|
+
],
|
|
372
|
+
dtype=np.float32,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Line indices
|
|
376
|
+
indices = np.array([0, 1, 1, 2, 1, 3, 1, 4, 1, 5], dtype=np.uint32)
|
|
377
|
+
|
|
378
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
379
|
+
|
|
380
|
+
def _generate_cylinder(
|
|
381
|
+
self,
|
|
382
|
+
center: np.ndarray,
|
|
383
|
+
direction: np.ndarray,
|
|
384
|
+
radius: float,
|
|
385
|
+
length: float,
|
|
386
|
+
) -> dict[str, Any]:
|
|
387
|
+
"""Generate a wireframe cylinder."""
|
|
388
|
+
# Create perpendicular basis
|
|
389
|
+
if abs(direction[0]) < 0.9:
|
|
390
|
+
ref = np.array([1, 0, 0], dtype=np.float32)
|
|
391
|
+
else:
|
|
392
|
+
ref = np.array([0, 1, 0], dtype=np.float32)
|
|
393
|
+
u = np.cross(direction, ref)
|
|
394
|
+
u = u / np.linalg.norm(u)
|
|
395
|
+
v = np.cross(direction, u)
|
|
396
|
+
|
|
397
|
+
# End points
|
|
398
|
+
start = center - direction * length * 0.5
|
|
399
|
+
end = center + direction * length * 0.5
|
|
400
|
+
|
|
401
|
+
# Generate circles at both ends
|
|
402
|
+
n = self.resolution
|
|
403
|
+
angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
|
|
404
|
+
|
|
405
|
+
vertices = []
|
|
406
|
+
for angle in angles:
|
|
407
|
+
offset = radius * (np.cos(angle) * u + np.sin(angle) * v)
|
|
408
|
+
vertices.append(start + offset)
|
|
409
|
+
for angle in angles:
|
|
410
|
+
offset = radius * (np.cos(angle) * u + np.sin(angle) * v)
|
|
411
|
+
vertices.append(end + offset)
|
|
412
|
+
|
|
413
|
+
vertices = np.array(vertices, dtype=np.float32)
|
|
414
|
+
|
|
415
|
+
# Wireframe indices
|
|
416
|
+
indices = []
|
|
417
|
+
# Start circle
|
|
418
|
+
for i in range(n):
|
|
419
|
+
indices.extend([i, (i + 1) % n])
|
|
420
|
+
# End circle
|
|
421
|
+
for i in range(n):
|
|
422
|
+
indices.extend([n + i, n + (i + 1) % n])
|
|
423
|
+
# Connecting lines
|
|
424
|
+
for i in range(0, n, max(1, n // 4)):
|
|
425
|
+
indices.extend([i, n + i])
|
|
426
|
+
|
|
427
|
+
indices = np.array(indices, dtype=np.uint32)
|
|
428
|
+
|
|
429
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
430
|
+
|
|
431
|
+
def _generate_cone(
|
|
432
|
+
self,
|
|
433
|
+
apex: np.ndarray,
|
|
434
|
+
direction: np.ndarray,
|
|
435
|
+
radius: float,
|
|
436
|
+
length: float,
|
|
437
|
+
) -> dict[str, Any]:
|
|
438
|
+
"""Generate a wireframe cone."""
|
|
439
|
+
# Create perpendicular basis
|
|
440
|
+
if abs(direction[0]) < 0.9:
|
|
441
|
+
ref = np.array([1, 0, 0], dtype=np.float32)
|
|
442
|
+
else:
|
|
443
|
+
ref = np.array([0, 1, 0], dtype=np.float32)
|
|
444
|
+
u = np.cross(direction, ref)
|
|
445
|
+
u = u / np.linalg.norm(u)
|
|
446
|
+
v = np.cross(direction, u)
|
|
447
|
+
|
|
448
|
+
# Base center
|
|
449
|
+
base_center = apex + direction * length
|
|
450
|
+
|
|
451
|
+
# Generate base circle
|
|
452
|
+
n = self.resolution
|
|
453
|
+
angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
|
|
454
|
+
|
|
455
|
+
vertices = [apex]
|
|
456
|
+
for angle in angles:
|
|
457
|
+
offset = radius * (np.cos(angle) * u + np.sin(angle) * v)
|
|
458
|
+
vertices.append(base_center + offset)
|
|
459
|
+
|
|
460
|
+
vertices = np.array(vertices, dtype=np.float32)
|
|
461
|
+
|
|
462
|
+
# Wireframe indices
|
|
463
|
+
indices = []
|
|
464
|
+
# Base circle
|
|
465
|
+
for i in range(n):
|
|
466
|
+
indices.extend([i + 1, ((i + 1) % n) + 1])
|
|
467
|
+
# Lines from apex to base
|
|
468
|
+
for i in range(0, n, max(1, n // 8)):
|
|
469
|
+
indices.extend([0, i + 1])
|
|
470
|
+
|
|
471
|
+
indices = np.array(indices, dtype=np.uint32)
|
|
472
|
+
|
|
473
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|
|
474
|
+
|
|
475
|
+
def _generate_circle(
|
|
476
|
+
self, center: np.ndarray, normal: np.ndarray, radius: float
|
|
477
|
+
) -> dict[str, Any]:
|
|
478
|
+
"""Generate a wireframe circle."""
|
|
479
|
+
# Create perpendicular basis
|
|
480
|
+
if abs(normal[0]) < 0.9:
|
|
481
|
+
ref = np.array([1, 0, 0], dtype=np.float32)
|
|
482
|
+
else:
|
|
483
|
+
ref = np.array([0, 1, 0], dtype=np.float32)
|
|
484
|
+
u = np.cross(normal, ref)
|
|
485
|
+
u = u / np.linalg.norm(u)
|
|
486
|
+
v = np.cross(normal, u)
|
|
487
|
+
|
|
488
|
+
n = self.resolution
|
|
489
|
+
angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
|
|
490
|
+
|
|
491
|
+
vertices = []
|
|
492
|
+
for angle in angles:
|
|
493
|
+
offset = radius * (np.cos(angle) * u + np.sin(angle) * v)
|
|
494
|
+
vertices.append(center + offset)
|
|
495
|
+
|
|
496
|
+
vertices = np.array(vertices, dtype=np.float32)
|
|
497
|
+
|
|
498
|
+
# Circle indices
|
|
499
|
+
indices = []
|
|
500
|
+
for i in range(n):
|
|
501
|
+
indices.extend([i, (i + 1) % n])
|
|
502
|
+
|
|
503
|
+
indices = np.array(indices, dtype=np.uint32)
|
|
504
|
+
|
|
505
|
+
return {"vertices": vertices, "indices": indices, "wireframe": True}
|