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
@@ -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}