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,39 @@
|
|
|
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
|
+
"""Core GUI components for scene and simulation management."""
|
|
35
|
+
|
|
36
|
+
from .scene import Scene, SceneObject
|
|
37
|
+
from .simulation import SimulationRunner
|
|
38
|
+
|
|
39
|
+
__all__ = ["Scene", "SceneObject", "SimulationRunner"]
|
lsurf/gui/core/scene.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
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
|
+
"""Scene state management for the GUI.
|
|
35
|
+
|
|
36
|
+
Manages the collection of surfaces, sources, and simulation results
|
|
37
|
+
that are displayed in the 3D viewport.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from enum import Enum, auto
|
|
42
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
43
|
+
|
|
44
|
+
import numpy as np
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from lsurf.geometry import Geometry
|
|
48
|
+
from lsurf.simulation import SimulationResult
|
|
49
|
+
from lsurf.sources import RaySource
|
|
50
|
+
from lsurf.surfaces import Surface
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ObjectType(Enum):
|
|
54
|
+
"""Type of object in the scene."""
|
|
55
|
+
|
|
56
|
+
SURFACE = auto()
|
|
57
|
+
DETECTOR = auto()
|
|
58
|
+
SOURCE = auto()
|
|
59
|
+
RAY_PATHS = auto()
|
|
60
|
+
DETECTIONS = auto()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class SceneObject:
|
|
65
|
+
"""A single object in the scene hierarchy."""
|
|
66
|
+
|
|
67
|
+
name: str
|
|
68
|
+
obj_type: ObjectType
|
|
69
|
+
data: Any
|
|
70
|
+
visible: bool = True
|
|
71
|
+
color: tuple[float, float, float, float] = (0.7, 0.7, 0.7, 1.0)
|
|
72
|
+
selected: bool = False
|
|
73
|
+
|
|
74
|
+
# Mesh data for rendering (computed by renderers)
|
|
75
|
+
vertices: np.ndarray | None = None
|
|
76
|
+
indices: np.ndarray | None = None
|
|
77
|
+
wireframe: bool = False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Scene:
|
|
82
|
+
"""Central state management for the GUI scene.
|
|
83
|
+
|
|
84
|
+
Holds all objects to be rendered and provides methods for
|
|
85
|
+
adding, removing, and querying objects.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
objects: dict[str, SceneObject] = field(default_factory=dict)
|
|
89
|
+
geometry: "Geometry | None" = None
|
|
90
|
+
source: "RaySource | None" = None
|
|
91
|
+
result: "SimulationResult | None" = None
|
|
92
|
+
|
|
93
|
+
# Camera state
|
|
94
|
+
camera_position: np.ndarray = field(
|
|
95
|
+
default_factory=lambda: np.array([0.0, -50.0, 25.0])
|
|
96
|
+
)
|
|
97
|
+
camera_target: np.ndarray = field(default_factory=lambda: np.array([0.0, 0.0, 0.0]))
|
|
98
|
+
camera_up: np.ndarray = field(default_factory=lambda: np.array([0.0, 0.0, 1.0]))
|
|
99
|
+
|
|
100
|
+
# Selection state
|
|
101
|
+
selected_object: str | None = None
|
|
102
|
+
|
|
103
|
+
# Callbacks for UI updates
|
|
104
|
+
_on_change_callbacks: list[Callable[[], None]] = field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
def add_object(self, obj: SceneObject) -> None:
|
|
107
|
+
"""Add an object to the scene."""
|
|
108
|
+
self.objects[obj.name] = obj
|
|
109
|
+
self._notify_change()
|
|
110
|
+
|
|
111
|
+
def remove_object(self, name: str) -> None:
|
|
112
|
+
"""Remove an object from the scene."""
|
|
113
|
+
if name in self.objects:
|
|
114
|
+
del self.objects[name]
|
|
115
|
+
if self.selected_object == name:
|
|
116
|
+
self.selected_object = None
|
|
117
|
+
self._notify_change()
|
|
118
|
+
|
|
119
|
+
def get_object(self, name: str) -> SceneObject | None:
|
|
120
|
+
"""Get an object by name."""
|
|
121
|
+
return self.objects.get(name)
|
|
122
|
+
|
|
123
|
+
def select_object(self, name: str | None) -> None:
|
|
124
|
+
"""Select an object (or deselect if None)."""
|
|
125
|
+
# Deselect previous
|
|
126
|
+
if self.selected_object and self.selected_object in self.objects:
|
|
127
|
+
self.objects[self.selected_object].selected = False
|
|
128
|
+
|
|
129
|
+
# Select new
|
|
130
|
+
self.selected_object = name
|
|
131
|
+
if name and name in self.objects:
|
|
132
|
+
self.objects[name].selected = True
|
|
133
|
+
|
|
134
|
+
self._notify_change()
|
|
135
|
+
|
|
136
|
+
def toggle_visibility(self, name: str) -> None:
|
|
137
|
+
"""Toggle visibility of an object."""
|
|
138
|
+
if name in self.objects:
|
|
139
|
+
self.objects[name].visible = not self.objects[name].visible
|
|
140
|
+
self._notify_change()
|
|
141
|
+
|
|
142
|
+
def clear(self) -> None:
|
|
143
|
+
"""Clear all objects from the scene."""
|
|
144
|
+
self.objects.clear()
|
|
145
|
+
self.geometry = None
|
|
146
|
+
self.source = None
|
|
147
|
+
self.result = None
|
|
148
|
+
self.selected_object = None
|
|
149
|
+
self._notify_change()
|
|
150
|
+
|
|
151
|
+
def get_objects_by_type(self, obj_type: ObjectType) -> list[SceneObject]:
|
|
152
|
+
"""Get all objects of a specific type."""
|
|
153
|
+
return [obj for obj in self.objects.values() if obj.obj_type == obj_type]
|
|
154
|
+
|
|
155
|
+
def get_visible_objects(self) -> list[SceneObject]:
|
|
156
|
+
"""Get all visible objects."""
|
|
157
|
+
return [obj for obj in self.objects.values() if obj.visible]
|
|
158
|
+
|
|
159
|
+
def get_bounds(self) -> tuple[np.ndarray, np.ndarray]:
|
|
160
|
+
"""Get the bounding box of all objects.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
(min_corner, max_corner) as numpy arrays
|
|
164
|
+
"""
|
|
165
|
+
all_vertices = []
|
|
166
|
+
for obj in self.objects.values():
|
|
167
|
+
if obj.visible and obj.vertices is not None and len(obj.vertices) > 0:
|
|
168
|
+
all_vertices.append(obj.vertices)
|
|
169
|
+
|
|
170
|
+
if not all_vertices:
|
|
171
|
+
# Default bounds if no objects
|
|
172
|
+
return np.array([-10.0, -10.0, -10.0]), np.array([10.0, 10.0, 10.0])
|
|
173
|
+
|
|
174
|
+
all_verts = np.vstack(all_vertices)
|
|
175
|
+
return all_verts.min(axis=0), all_verts.max(axis=0)
|
|
176
|
+
|
|
177
|
+
def fit_camera_to_scene(self) -> None:
|
|
178
|
+
"""Adjust camera to fit all objects in view."""
|
|
179
|
+
min_bounds, max_bounds = self.get_bounds()
|
|
180
|
+
center = (min_bounds + max_bounds) / 2
|
|
181
|
+
size = np.linalg.norm(max_bounds - min_bounds)
|
|
182
|
+
|
|
183
|
+
# Position camera at 45-degree angle looking at center
|
|
184
|
+
distance = max(size * 1.5, 10.0)
|
|
185
|
+
self.camera_target = center
|
|
186
|
+
self.camera_position = center + np.array(
|
|
187
|
+
[distance * 0.5, -distance * 0.7, distance * 0.5]
|
|
188
|
+
)
|
|
189
|
+
self._notify_change()
|
|
190
|
+
|
|
191
|
+
def on_change(self, callback: Callable[[], None]) -> None:
|
|
192
|
+
"""Register a callback to be called when the scene changes."""
|
|
193
|
+
self._on_change_callbacks.append(callback)
|
|
194
|
+
|
|
195
|
+
def _notify_change(self) -> None:
|
|
196
|
+
"""Notify all registered callbacks of a change."""
|
|
197
|
+
for callback in self._on_change_callbacks:
|
|
198
|
+
callback()
|
|
199
|
+
|
|
200
|
+
def load_geometry(self, geometry: "Geometry") -> None:
|
|
201
|
+
"""Load a Geometry object into the scene."""
|
|
202
|
+
from ..renderers import SurfaceRenderer
|
|
203
|
+
|
|
204
|
+
self.geometry = geometry
|
|
205
|
+
|
|
206
|
+
# Remove existing surfaces and detectors
|
|
207
|
+
to_remove = [
|
|
208
|
+
name
|
|
209
|
+
for name, obj in self.objects.items()
|
|
210
|
+
if obj.obj_type in (ObjectType.SURFACE, ObjectType.DETECTOR)
|
|
211
|
+
]
|
|
212
|
+
for name in to_remove:
|
|
213
|
+
del self.objects[name]
|
|
214
|
+
|
|
215
|
+
renderer = SurfaceRenderer()
|
|
216
|
+
|
|
217
|
+
# Add surfaces
|
|
218
|
+
for surface in geometry.surfaces:
|
|
219
|
+
mesh_data = renderer.render(surface)
|
|
220
|
+
obj = SceneObject(
|
|
221
|
+
name=surface.name,
|
|
222
|
+
obj_type=ObjectType.SURFACE,
|
|
223
|
+
data=surface,
|
|
224
|
+
color=_get_surface_color(surface),
|
|
225
|
+
vertices=mesh_data.get("vertices"),
|
|
226
|
+
indices=mesh_data.get("indices"),
|
|
227
|
+
wireframe=mesh_data.get("wireframe", False),
|
|
228
|
+
)
|
|
229
|
+
self.objects[surface.name] = obj
|
|
230
|
+
print(
|
|
231
|
+
f"[Scene] Added surface '{surface.name}': {type(surface).__name__}, vertices={mesh_data.get('vertices') is not None}, indices={mesh_data.get('indices') is not None}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Add detectors
|
|
235
|
+
for detector in geometry.detectors:
|
|
236
|
+
mesh_data = renderer.render(detector)
|
|
237
|
+
obj = SceneObject(
|
|
238
|
+
name=detector.name,
|
|
239
|
+
obj_type=ObjectType.DETECTOR,
|
|
240
|
+
data=detector,
|
|
241
|
+
color=(0.2, 0.8, 0.2, 0.8), # Green for detectors
|
|
242
|
+
vertices=mesh_data.get("vertices"),
|
|
243
|
+
indices=mesh_data.get("indices"),
|
|
244
|
+
wireframe=mesh_data.get("wireframe", False),
|
|
245
|
+
)
|
|
246
|
+
self.objects[detector.name] = obj
|
|
247
|
+
print(
|
|
248
|
+
f"[Scene] Added detector '{detector.name}': {type(detector).__name__}, vertices={mesh_data.get('vertices') is not None}, indices={mesh_data.get('indices') is not None}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
print(f"[Scene] Total objects: {len(self.objects)}")
|
|
252
|
+
self.fit_camera_to_scene()
|
|
253
|
+
self._notify_change()
|
|
254
|
+
|
|
255
|
+
def load_source(self, source: "RaySource") -> None:
|
|
256
|
+
"""Load a RaySource into the scene."""
|
|
257
|
+
from ..renderers import SourceRenderer
|
|
258
|
+
|
|
259
|
+
self.source = source
|
|
260
|
+
|
|
261
|
+
# Remove existing source
|
|
262
|
+
to_remove = [
|
|
263
|
+
name
|
|
264
|
+
for name, obj in self.objects.items()
|
|
265
|
+
if obj.obj_type == ObjectType.SOURCE
|
|
266
|
+
]
|
|
267
|
+
for name in to_remove:
|
|
268
|
+
del self.objects[name]
|
|
269
|
+
|
|
270
|
+
renderer = SourceRenderer()
|
|
271
|
+
mesh_data = renderer.render(source)
|
|
272
|
+
|
|
273
|
+
obj = SceneObject(
|
|
274
|
+
name="Source",
|
|
275
|
+
obj_type=ObjectType.SOURCE,
|
|
276
|
+
data=source,
|
|
277
|
+
color=(1.0, 0.8, 0.0, 1.0), # Yellow/gold for sources
|
|
278
|
+
vertices=mesh_data.get("vertices"),
|
|
279
|
+
indices=mesh_data.get("indices"),
|
|
280
|
+
wireframe=mesh_data.get("wireframe", False),
|
|
281
|
+
)
|
|
282
|
+
self.objects["Source"] = obj
|
|
283
|
+
self._notify_change()
|
|
284
|
+
|
|
285
|
+
def load_result(self, result: "SimulationResult", show_rays: bool = True) -> None:
|
|
286
|
+
"""Load simulation results into the scene."""
|
|
287
|
+
from ..renderers import RayRenderer
|
|
288
|
+
|
|
289
|
+
self.result = result
|
|
290
|
+
|
|
291
|
+
# Remove existing results
|
|
292
|
+
to_remove = [
|
|
293
|
+
name
|
|
294
|
+
for name, obj in self.objects.items()
|
|
295
|
+
if obj.obj_type in (ObjectType.RAY_PATHS, ObjectType.DETECTIONS)
|
|
296
|
+
]
|
|
297
|
+
for name in to_remove:
|
|
298
|
+
del self.objects[name]
|
|
299
|
+
|
|
300
|
+
renderer = RayRenderer()
|
|
301
|
+
|
|
302
|
+
# Add detection points
|
|
303
|
+
if result.detected is not None and result.num_detected > 0:
|
|
304
|
+
mesh_data = renderer.render_detections(result.detected)
|
|
305
|
+
obj = SceneObject(
|
|
306
|
+
name="Detections",
|
|
307
|
+
obj_type=ObjectType.DETECTIONS,
|
|
308
|
+
data=result.detected,
|
|
309
|
+
color=(1.0, 0.2, 0.2, 1.0), # Red for detections
|
|
310
|
+
vertices=mesh_data.get("vertices"),
|
|
311
|
+
indices=mesh_data.get("indices"),
|
|
312
|
+
wireframe=False,
|
|
313
|
+
)
|
|
314
|
+
self.objects["Detections"] = obj
|
|
315
|
+
|
|
316
|
+
# Add ray paths if available and requested
|
|
317
|
+
if show_rays and result.surface_hits:
|
|
318
|
+
mesh_data = renderer.render_ray_paths(result.surface_hits)
|
|
319
|
+
if mesh_data.get("vertices") is not None:
|
|
320
|
+
obj = SceneObject(
|
|
321
|
+
name="Ray Paths",
|
|
322
|
+
obj_type=ObjectType.RAY_PATHS,
|
|
323
|
+
data=result.surface_hits,
|
|
324
|
+
color=(0.5, 0.5, 1.0, 0.5), # Blue for rays
|
|
325
|
+
vertices=mesh_data.get("vertices"),
|
|
326
|
+
indices=mesh_data.get("indices"),
|
|
327
|
+
wireframe=True,
|
|
328
|
+
)
|
|
329
|
+
self.objects["Ray Paths"] = obj
|
|
330
|
+
|
|
331
|
+
self._notify_change()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _get_surface_color(surface: "Surface") -> tuple[float, float, float, float]:
|
|
335
|
+
"""Get a color for a surface based on its role."""
|
|
336
|
+
from lsurf.surfaces import SurfaceRole
|
|
337
|
+
|
|
338
|
+
if surface.role == SurfaceRole.DETECTOR:
|
|
339
|
+
return (0.2, 0.8, 0.2, 0.8) # Green
|
|
340
|
+
elif surface.role == SurfaceRole.ABSORBER:
|
|
341
|
+
return (0.3, 0.3, 0.3, 0.9) # Dark gray
|
|
342
|
+
else: # OPTICAL
|
|
343
|
+
return (0.4, 0.6, 0.9, 0.7) # Light blue
|
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
"""Background simulation runner for the GUI.
|
|
35
|
+
|
|
36
|
+
Runs simulations in a separate thread to keep the GUI responsive.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import threading
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from enum import Enum, auto
|
|
42
|
+
from typing import TYPE_CHECKING, Callable
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from lsurf.geometry import Geometry
|
|
46
|
+
from lsurf.simulation import SimulationConfig, SimulationResult
|
|
47
|
+
from lsurf.sources import RaySource
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SimulationState(Enum):
|
|
51
|
+
"""State of the simulation runner."""
|
|
52
|
+
|
|
53
|
+
IDLE = auto()
|
|
54
|
+
RUNNING = auto()
|
|
55
|
+
COMPLETED = auto()
|
|
56
|
+
ERROR = auto()
|
|
57
|
+
CANCELLED = auto()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class SimulationProgress:
|
|
62
|
+
"""Progress information for a running simulation."""
|
|
63
|
+
|
|
64
|
+
state: SimulationState
|
|
65
|
+
bounce: int = 0
|
|
66
|
+
max_bounces: int = 0
|
|
67
|
+
rays_active: int = 0
|
|
68
|
+
rays_detected: int = 0
|
|
69
|
+
message: str = ""
|
|
70
|
+
error: Exception | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SimulationRunner:
|
|
74
|
+
"""Runs simulations in a background thread.
|
|
75
|
+
|
|
76
|
+
Usage:
|
|
77
|
+
runner = SimulationRunner()
|
|
78
|
+
runner.on_progress(update_progress_bar)
|
|
79
|
+
runner.on_complete(handle_results)
|
|
80
|
+
runner.start(geometry, source, config)
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
self._thread: threading.Thread | None = None
|
|
85
|
+
self._cancel_flag = threading.Event()
|
|
86
|
+
self._progress = SimulationProgress(state=SimulationState.IDLE)
|
|
87
|
+
self._result: "SimulationResult | None" = None
|
|
88
|
+
|
|
89
|
+
# Callbacks
|
|
90
|
+
self._on_progress_callbacks: list[Callable[[SimulationProgress], None]] = []
|
|
91
|
+
self._on_complete_callbacks: list[
|
|
92
|
+
Callable[["SimulationResult | None"], None]
|
|
93
|
+
] = []
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def state(self) -> SimulationState:
|
|
97
|
+
"""Current state of the simulation."""
|
|
98
|
+
return self._progress.state
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def progress(self) -> SimulationProgress:
|
|
102
|
+
"""Current progress information."""
|
|
103
|
+
return self._progress
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def result(self) -> "SimulationResult | None":
|
|
107
|
+
"""The simulation result (if completed)."""
|
|
108
|
+
return self._result
|
|
109
|
+
|
|
110
|
+
def on_progress(self, callback: Callable[[SimulationProgress], None]) -> None:
|
|
111
|
+
"""Register a callback for progress updates."""
|
|
112
|
+
self._on_progress_callbacks.append(callback)
|
|
113
|
+
|
|
114
|
+
def on_complete(
|
|
115
|
+
self, callback: Callable[["SimulationResult | None"], None]
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Register a callback for completion."""
|
|
118
|
+
self._on_complete_callbacks.append(callback)
|
|
119
|
+
|
|
120
|
+
def start(
|
|
121
|
+
self,
|
|
122
|
+
geometry: "Geometry",
|
|
123
|
+
source: "RaySource",
|
|
124
|
+
config: "SimulationConfig | None" = None,
|
|
125
|
+
) -> bool:
|
|
126
|
+
"""Start a simulation in the background.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
geometry: The geometry to simulate
|
|
130
|
+
source: The ray source
|
|
131
|
+
config: Optional simulation configuration
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if started successfully, False if already running
|
|
135
|
+
"""
|
|
136
|
+
if self._thread is not None and self._thread.is_alive():
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
self._cancel_flag.clear()
|
|
140
|
+
self._result = None
|
|
141
|
+
self._progress = SimulationProgress(
|
|
142
|
+
state=SimulationState.RUNNING, message="Initializing..."
|
|
143
|
+
)
|
|
144
|
+
self._notify_progress()
|
|
145
|
+
|
|
146
|
+
self._thread = threading.Thread(
|
|
147
|
+
target=self._run_simulation,
|
|
148
|
+
args=(geometry, source, config),
|
|
149
|
+
daemon=True,
|
|
150
|
+
)
|
|
151
|
+
self._thread.start()
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
def cancel(self) -> None:
|
|
155
|
+
"""Request cancellation of the running simulation."""
|
|
156
|
+
self._cancel_flag.set()
|
|
157
|
+
|
|
158
|
+
def is_running(self) -> bool:
|
|
159
|
+
"""Check if a simulation is currently running."""
|
|
160
|
+
return self._thread is not None and self._thread.is_alive()
|
|
161
|
+
|
|
162
|
+
def _run_simulation(
|
|
163
|
+
self,
|
|
164
|
+
geometry: "Geometry",
|
|
165
|
+
source: "RaySource",
|
|
166
|
+
config: "SimulationConfig | None",
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Run the simulation (called in background thread)."""
|
|
169
|
+
try:
|
|
170
|
+
from lsurf.simulation import Simulation, SimulationConfig
|
|
171
|
+
|
|
172
|
+
# Create simulation
|
|
173
|
+
sim_config = config or SimulationConfig()
|
|
174
|
+
simulation = Simulation(geometry, sim_config)
|
|
175
|
+
|
|
176
|
+
self._progress.max_bounces = sim_config.max_bounces
|
|
177
|
+
self._progress.message = "Generating rays..."
|
|
178
|
+
self._notify_progress()
|
|
179
|
+
|
|
180
|
+
# Generate rays
|
|
181
|
+
rays = source.generate()
|
|
182
|
+
self._progress.rays_active = rays.num_rays
|
|
183
|
+
self._progress.message = "Running simulation..."
|
|
184
|
+
self._notify_progress()
|
|
185
|
+
|
|
186
|
+
# Check for cancellation
|
|
187
|
+
if self._cancel_flag.is_set():
|
|
188
|
+
self._progress.state = SimulationState.CANCELLED
|
|
189
|
+
self._progress.message = "Cancelled"
|
|
190
|
+
self._notify_progress()
|
|
191
|
+
self._notify_complete()
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Run simulation
|
|
195
|
+
# Note: For more detailed progress, we could use run_single_bounce
|
|
196
|
+
# in a loop, but the full run is more efficient
|
|
197
|
+
result = simulation.run(rays)
|
|
198
|
+
|
|
199
|
+
# Check for cancellation
|
|
200
|
+
if self._cancel_flag.is_set():
|
|
201
|
+
self._progress.state = SimulationState.CANCELLED
|
|
202
|
+
self._progress.message = "Cancelled"
|
|
203
|
+
self._notify_progress()
|
|
204
|
+
self._notify_complete()
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
# Success
|
|
208
|
+
self._result = result
|
|
209
|
+
self._progress.state = SimulationState.COMPLETED
|
|
210
|
+
self._progress.bounce = result.bounces
|
|
211
|
+
self._progress.rays_detected = result.num_detected
|
|
212
|
+
self._progress.rays_active = result.num_remaining
|
|
213
|
+
self._progress.message = f"Complete: {result.num_detected} rays detected"
|
|
214
|
+
self._notify_progress()
|
|
215
|
+
self._notify_complete()
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
self._progress.state = SimulationState.ERROR
|
|
219
|
+
self._progress.error = e
|
|
220
|
+
self._progress.message = f"Error: {e}"
|
|
221
|
+
self._notify_progress()
|
|
222
|
+
self._notify_complete()
|
|
223
|
+
|
|
224
|
+
def _notify_progress(self) -> None:
|
|
225
|
+
"""Notify progress callbacks."""
|
|
226
|
+
for callback in self._on_progress_callbacks:
|
|
227
|
+
try:
|
|
228
|
+
callback(self._progress)
|
|
229
|
+
except Exception:
|
|
230
|
+
pass # Don't let callback errors crash the simulation
|
|
231
|
+
|
|
232
|
+
def _notify_complete(self) -> None:
|
|
233
|
+
"""Notify completion callbacks."""
|
|
234
|
+
for callback in self._on_complete_callbacks:
|
|
235
|
+
try:
|
|
236
|
+
callback(self._result)
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def run_simulation_sync(
|
|
242
|
+
geometry: "Geometry",
|
|
243
|
+
source: "RaySource",
|
|
244
|
+
config: "SimulationConfig | None" = None,
|
|
245
|
+
) -> "SimulationResult":
|
|
246
|
+
"""Run a simulation synchronously (blocking).
|
|
247
|
+
|
|
248
|
+
This is a convenience function for running simulations without
|
|
249
|
+
the background thread infrastructure.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
geometry: The geometry to simulate
|
|
253
|
+
source: The ray source
|
|
254
|
+
config: Optional simulation configuration
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
The simulation result
|
|
258
|
+
"""
|
|
259
|
+
from lsurf.simulation import Simulation, SimulationConfig
|
|
260
|
+
|
|
261
|
+
sim_config = config or SimulationConfig()
|
|
262
|
+
simulation = Simulation(geometry, sim_config)
|
|
263
|
+
rays = source.generate()
|
|
264
|
+
return simulation.run(rays)
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
"""Renderers for converting lsurf objects to 3D visuals."""
|
|
35
|
+
|
|
36
|
+
from .ray_renderer import RayRenderer
|
|
37
|
+
from .source_renderer import SourceRenderer
|
|
38
|
+
from .surface_renderer import SurfaceRenderer
|
|
39
|
+
|
|
40
|
+
__all__ = ["SurfaceRenderer", "SourceRenderer", "RayRenderer"]
|