cardio 2023.1.2__py3-none-any.whl → 2025.8.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.
- cardio/__init__.py +13 -5
- cardio/app.py +45 -14
- cardio/assets/bone.toml +42 -0
- cardio/assets/vascular_closed.toml +78 -0
- cardio/assets/vascular_open.toml +54 -0
- cardio/assets/xray.toml +42 -0
- cardio/logic.py +273 -10
- cardio/mesh.py +249 -29
- cardio/object.py +183 -10
- cardio/property_config.py +56 -0
- cardio/scene.py +204 -65
- cardio/screenshot.py +4 -5
- cardio/segmentation.py +178 -0
- cardio/transfer_functions.py +272 -0
- cardio/types.py +18 -0
- cardio/ui.py +359 -80
- cardio/utils.py +101 -0
- cardio/volume.py +41 -96
- cardio-2025.8.0.dist-info/METADATA +94 -0
- cardio-2025.8.0.dist-info/RECORD +22 -0
- cardio-2025.8.0.dist-info/WHEEL +4 -0
- {cardio-2023.1.2.dist-info → cardio-2025.8.0.dist-info}/entry_points.txt +1 -0
- __init__.py +0 -0
- cardio-2023.1.2.dist-info/LICENSE +0 -15
- cardio-2023.1.2.dist-info/METADATA +0 -69
- cardio-2023.1.2.dist-info/RECORD +0 -16
- cardio-2023.1.2.dist-info/WHEEL +0 -5
- cardio-2023.1.2.dist-info/top_level.txt +0 -2
cardio/mesh.py
CHANGED
@@ -1,39 +1,259 @@
|
|
1
|
+
# System
|
2
|
+
import enum
|
1
3
|
import logging
|
2
|
-
import os
|
3
4
|
|
4
|
-
|
5
|
-
|
5
|
+
# Third Party
|
6
|
+
import numpy as np
|
7
|
+
import pydantic as pc
|
8
|
+
import vtk
|
6
9
|
|
7
|
-
|
10
|
+
# Internal
|
11
|
+
from .object import Object
|
12
|
+
from .property_config import Representation, vtkPropertyConfig
|
13
|
+
|
14
|
+
|
15
|
+
class SurfaceType(enum.Enum):
|
16
|
+
"""Surface coloring types for mesh rendering."""
|
17
|
+
|
18
|
+
SOLID = "solid"
|
19
|
+
SQUEEZ = "squeez"
|
20
|
+
|
21
|
+
|
22
|
+
def apply_elementwise(
|
23
|
+
array1: vtk.vtkFloatArray, array2: vtk.vtkFloatArray, func, name: str = "Result"
|
24
|
+
) -> vtk.vtkFloatArray:
|
25
|
+
if array1.GetNumberOfTuples() != array2.GetNumberOfTuples():
|
26
|
+
raise ValueError(
|
27
|
+
f"Array lengths must match: {array1.GetNumberOfTuples()} != {array2.GetNumberOfTuples()}"
|
28
|
+
)
|
29
|
+
|
30
|
+
result_array = vtk.vtkFloatArray()
|
31
|
+
result_array.SetName(name)
|
32
|
+
result_array.SetNumberOfComponents(1)
|
33
|
+
|
34
|
+
for i in range(array1.GetNumberOfTuples()):
|
35
|
+
val1 = array1.GetValue(i)
|
36
|
+
val2 = array2.GetValue(i)
|
37
|
+
result = func(val1, val2)
|
38
|
+
result_array.InsertNextValue(result)
|
39
|
+
|
40
|
+
return result_array
|
41
|
+
|
42
|
+
|
43
|
+
def calculate_squeez(current_area: float, ref_area: float) -> float:
|
44
|
+
if ref_area > 0:
|
45
|
+
return (current_area / ref_area) ** 0.5
|
46
|
+
return 1.0
|
8
47
|
|
9
48
|
|
10
49
|
class Mesh(Object):
|
11
|
-
|
12
|
-
super().__init__(cfg, renderer)
|
13
|
-
self.actors: list[vtkActor] = []
|
50
|
+
"""Mesh object with subdivision support."""
|
14
51
|
|
15
|
-
|
16
|
-
|
17
|
-
|
52
|
+
pattern: str = pc.Field(
|
53
|
+
default="${frame}.obj", description="Filename pattern with $frame placeholder"
|
54
|
+
)
|
55
|
+
_actors: list[vtk.vtkActor] = pc.PrivateAttr(default_factory=list)
|
56
|
+
properties: vtkPropertyConfig = pc.Field(
|
57
|
+
default_factory=vtkPropertyConfig, description="Property configuration"
|
58
|
+
)
|
59
|
+
loop_subdivision_iterations: int = pc.Field(ge=0, le=5, default=0)
|
60
|
+
surface_type: SurfaceType = pc.Field(default=SurfaceType.SOLID)
|
61
|
+
ctf_min: float = pc.Field(ge=0.0, default=0.7)
|
62
|
+
ctf_max: float = pc.Field(ge=0.0, default=1.3)
|
18
63
|
|
19
|
-
|
20
|
-
|
64
|
+
@pc.model_validator(mode="after")
|
65
|
+
def initialize_mesh(self):
|
66
|
+
# Pass 1: Load all frames and determine topology consistency
|
67
|
+
frame_data = []
|
68
|
+
for frame, path in enumerate(self.path_list):
|
21
69
|
logging.info(f"{self.label}: Loading frame {frame}.")
|
22
|
-
reader = vtkOBJReader()
|
23
|
-
|
24
|
-
actor = vtkActor()
|
25
|
-
reader.SetFileName(self.path_for_frame(frame))
|
26
|
-
mapper.SetInputConnection(reader.GetOutputPort())
|
27
|
-
actor.SetMapper(mapper)
|
70
|
+
reader = vtk.vtkOBJReader()
|
71
|
+
reader.SetFileName(path)
|
28
72
|
reader.Update()
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
73
|
+
|
74
|
+
if self.loop_subdivision_iterations > 0:
|
75
|
+
subdivision_filter = vtk.vtkLoopSubdivisionFilter()
|
76
|
+
subdivision_filter.SetInputConnection(reader.GetOutputPort())
|
77
|
+
subdivision_filter.SetNumberOfSubdivisions(
|
78
|
+
self.loop_subdivision_iterations
|
79
|
+
)
|
80
|
+
subdivision_filter.Update()
|
81
|
+
polydata = subdivision_filter.GetOutput()
|
82
|
+
else:
|
83
|
+
polydata = reader.GetOutput()
|
84
|
+
|
85
|
+
if self.properties.representation == Representation.Surface:
|
86
|
+
polydata = self.calculate_mesh_areas(polydata)
|
87
|
+
|
88
|
+
frame_data.append(polydata)
|
89
|
+
|
90
|
+
consistent_topology = self._should_calculate_squeez(frame_data)
|
91
|
+
|
92
|
+
# Pass 2: Create actors with appropriate coloring
|
93
|
+
for frame, polydata in enumerate(frame_data):
|
94
|
+
mapper = self._setup_coloring(
|
95
|
+
polydata, frame_data, frame, consistent_topology
|
96
|
+
)
|
97
|
+
|
98
|
+
actor = vtk.vtkActor()
|
99
|
+
actor.SetMapper(mapper)
|
100
|
+
|
101
|
+
self._actors.append(actor)
|
102
|
+
|
103
|
+
return self
|
104
|
+
|
105
|
+
@property
|
106
|
+
def actors(self) -> list[vtk.vtkActor]:
|
107
|
+
return self._actors
|
108
|
+
|
109
|
+
def _setup_coloring(self, polydata, frame_data, frame, consistent_topology):
|
110
|
+
"""Setup mesh coloring based on surface_type with smart fallback."""
|
111
|
+
if self.surface_type == SurfaceType.SQUEEZ:
|
112
|
+
if self._can_use_squeez_coloring(consistent_topology):
|
113
|
+
return self._setup_squeez_coloring(polydata, frame_data, frame)
|
114
|
+
else:
|
115
|
+
self._log_squeez_fallback()
|
116
|
+
return self._setup_solid_coloring(polydata)
|
117
|
+
else: # SurfaceType.SOLID
|
118
|
+
return self._setup_solid_coloring(polydata)
|
119
|
+
|
120
|
+
def _can_use_squeez_coloring(self, consistent_topology):
|
121
|
+
"""Check if SQUEEZ coloring is possible."""
|
122
|
+
return (
|
123
|
+
self.properties.representation == Representation.Surface
|
124
|
+
and consistent_topology
|
125
|
+
)
|
126
|
+
|
127
|
+
def _log_squeez_fallback(self):
|
128
|
+
"""Log warning when falling back from SQUEEZ to solid coloring."""
|
129
|
+
if self.properties.representation != Representation.Surface:
|
130
|
+
logging.warning(
|
131
|
+
f"SQUEEZ coloring requested for {self.label} but representation is not Surface, falling back to solid coloring"
|
132
|
+
)
|
133
|
+
else:
|
134
|
+
logging.warning(
|
135
|
+
f"SQUEEZ coloring requested for {self.label} but topology inconsistent across frames, falling back to solid coloring"
|
136
|
+
)
|
137
|
+
|
138
|
+
def _setup_solid_coloring(self, polydata):
|
139
|
+
"""Setup solid coloring using VTK property colors."""
|
140
|
+
mapper = vtk.vtkPolyDataMapper()
|
141
|
+
mapper.SetInputData(polydata)
|
142
|
+
mapper.ScalarVisibilityOff()
|
143
|
+
return mapper
|
144
|
+
|
145
|
+
def _setup_squeez_coloring(self, polydata, frame_data, frame):
|
146
|
+
"""Setup SQUEEZ coloring with scalar data."""
|
147
|
+
ref_quality_array = frame_data[0].GetCellData().GetArray("Area")
|
148
|
+
if frame == 0:
|
149
|
+
ratio_array = apply_elementwise(
|
150
|
+
ref_quality_array, ref_quality_array, lambda x, y: 1.0, "SQUEEZ"
|
151
|
+
)
|
152
|
+
else:
|
153
|
+
current_quality_array = polydata.GetCellData().GetArray("Area")
|
154
|
+
ratio_array = apply_elementwise(
|
155
|
+
current_quality_array,
|
156
|
+
ref_quality_array,
|
157
|
+
calculate_squeez,
|
158
|
+
"SQUEEZ",
|
159
|
+
)
|
160
|
+
polydata.GetCellData().AddArray(ratio_array)
|
161
|
+
polydata.GetCellData().SetActiveScalars("SQUEEZ")
|
162
|
+
|
163
|
+
mapper = vtk.vtkPolyDataMapper()
|
164
|
+
mapper.SetInputData(polydata)
|
165
|
+
mapper = self.setup_scalar_coloring(mapper)
|
166
|
+
return mapper
|
167
|
+
|
168
|
+
def color_transfer_function(self):
|
169
|
+
ctf = vtk.vtkColorTransferFunction()
|
170
|
+
ctf.AddRGBPoint(0.7, 0.0, 0.0, 1.0)
|
171
|
+
ctf.AddRGBPoint(1.0, 1.0, 0.0, 0.0)
|
172
|
+
ctf.AddRGBPoint(1.3, 1.0, 1.0, 0.0)
|
173
|
+
return ctf
|
174
|
+
|
175
|
+
def setup_scalar_coloring(self, mapper):
|
176
|
+
mapper.SetColorModeToMapScalars()
|
177
|
+
mapper.SetScalarModeToUseCellData()
|
178
|
+
mapper.SetLookupTable(self.color_transfer_function())
|
179
|
+
mapper.SetScalarRange(self.ctf_min, self.ctf_max)
|
180
|
+
mapper.ScalarVisibilityOn()
|
181
|
+
mapper.SetInterpolateScalarsBeforeMapping(False)
|
182
|
+
return mapper
|
183
|
+
|
184
|
+
def calculate_mesh_areas(self, polydata):
|
185
|
+
"""Calculate area of each triangle/cell in the mesh using VTK mesh quality."""
|
186
|
+
# Ensure we have triangles
|
187
|
+
triangle_filter = vtk.vtkTriangleFilter()
|
188
|
+
triangle_filter.SetInputData(polydata)
|
189
|
+
triangle_filter.Update()
|
190
|
+
triangulated = triangle_filter.GetOutput()
|
191
|
+
|
192
|
+
# Use VTK mesh quality to calculate areas
|
193
|
+
mesh_quality = vtk.vtkMeshQuality()
|
194
|
+
mesh_quality.SetInputData(triangulated)
|
195
|
+
mesh_quality.SetTriangleQualityMeasureToArea()
|
196
|
+
mesh_quality.SetQuadQualityMeasureToArea()
|
197
|
+
mesh_quality.SaveCellQualityOn()
|
198
|
+
mesh_quality.Update()
|
199
|
+
|
200
|
+
quality_output = mesh_quality.GetOutput()
|
201
|
+
quality_array = quality_output.GetCellData().GetArray("Quality")
|
202
|
+
quality_array.SetName("Area")
|
203
|
+
|
204
|
+
return quality_output
|
205
|
+
|
206
|
+
def _should_calculate_squeez(self, frame_data: list) -> bool:
|
207
|
+
if (
|
208
|
+
self.properties.representation != Representation.Surface
|
209
|
+
or len(frame_data) <= 1
|
210
|
+
):
|
211
|
+
return False
|
212
|
+
|
213
|
+
reference_frame = frame_data[0]
|
214
|
+
ref_area_array = reference_frame.GetCellData().GetArray("Area")
|
215
|
+
|
216
|
+
if ref_area_array is None:
|
217
|
+
return False
|
218
|
+
|
219
|
+
ref_num_cells = reference_frame.GetNumberOfCells()
|
220
|
+
ref_num_points = reference_frame.GetNumberOfPoints()
|
221
|
+
ref_num_areas = ref_area_array.GetNumberOfTuples()
|
222
|
+
|
223
|
+
# Check all frames have identical topology
|
224
|
+
for polydata in frame_data[1:]:
|
225
|
+
area_array = polydata.GetCellData().GetArray("Area")
|
226
|
+
if (
|
227
|
+
area_array is None
|
228
|
+
or polydata.GetNumberOfCells() != ref_num_cells
|
229
|
+
or polydata.GetNumberOfPoints() != ref_num_points
|
230
|
+
or area_array.GetNumberOfTuples() != ref_num_areas
|
231
|
+
):
|
232
|
+
return False
|
233
|
+
|
234
|
+
return True
|
235
|
+
|
236
|
+
def configure_actors(self):
|
237
|
+
"""Configure actor properties without adding to renderer."""
|
238
|
+
for actor in self._actors:
|
239
|
+
actor.SetVisibility(False)
|
240
|
+
actor.SetProperty(self.properties.vtk_property)
|
241
|
+
|
242
|
+
# Apply flat shading for SQUEEZ coloring
|
243
|
+
if self._is_using_squeez_coloring():
|
244
|
+
for actor in self._actors:
|
245
|
+
actor.GetProperty().SetInterpolationToFlat()
|
246
|
+
|
247
|
+
def _is_using_squeez_coloring(self):
|
248
|
+
"""Check if SQUEEZ coloring is actually being used (not fell back to solid)."""
|
249
|
+
if self.surface_type != SurfaceType.SQUEEZ:
|
250
|
+
return False
|
251
|
+
|
252
|
+
# Check if any actor has SQUEEZ scalar data
|
253
|
+
for actor in self._actors:
|
254
|
+
mapper = actor.GetMapper()
|
255
|
+
if mapper.GetInput():
|
256
|
+
squeez_array = mapper.GetInput().GetCellData().GetArray("SQUEEZ")
|
257
|
+
if squeez_array is not None:
|
258
|
+
return True
|
259
|
+
return False
|
cardio/object.py
CHANGED
@@ -1,13 +1,186 @@
|
|
1
|
-
|
1
|
+
# System
|
2
|
+
import functools
|
3
|
+
import logging
|
4
|
+
import pathlib as pl
|
5
|
+
import re
|
6
|
+
import string
|
2
7
|
|
8
|
+
# Third Party
|
9
|
+
import pydantic as pc
|
10
|
+
import vtk
|
3
11
|
|
4
|
-
|
5
|
-
def __init__(self, cfg: str, renderer: vtkRenderer):
|
6
|
-
self.label: str = cfg["label"]
|
7
|
-
self.directory: str = cfg["directory"]
|
8
|
-
self.suffix: str = cfg["suffix"]
|
9
|
-
self.visible: bool = cfg["visible"]
|
10
|
-
self.renderer = renderer
|
12
|
+
from .utils import calculate_combined_bounds
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
+
|
15
|
+
class Object(pc.BaseModel):
|
16
|
+
"""Base class for renderable objects with validated configuration."""
|
17
|
+
|
18
|
+
model_config = pc.ConfigDict(arbitrary_types_allowed=True, frozen=True)
|
19
|
+
|
20
|
+
label: str = pc.Field(description="Object identifier (only [a-zA-Z0-9_] allowed)")
|
21
|
+
directory: pl.Path = pc.Field(description="Directory containing object files")
|
22
|
+
pattern: str | None = pc.Field(
|
23
|
+
default=None, description="Filename pattern with ${frame} placeholder"
|
24
|
+
)
|
25
|
+
file_paths: list[str] | None = pc.Field(
|
26
|
+
default=None, description="Static list of file paths relative to directory"
|
27
|
+
)
|
28
|
+
visible: bool = pc.Field(
|
29
|
+
default=True, description="Whether object is initially visible"
|
30
|
+
)
|
31
|
+
clipping_enabled: bool = pc.Field(default=False)
|
32
|
+
|
33
|
+
@pc.field_validator("label")
|
34
|
+
@classmethod
|
35
|
+
def validate_label(cls, v: str) -> str:
|
36
|
+
"""Validate that label contains only letters, numbers, and underscores."""
|
37
|
+
if not isinstance(v, str):
|
38
|
+
raise ValueError("label must be a string")
|
39
|
+
|
40
|
+
if not re.match(r"^[a-zA-Z0-9_]+$", v):
|
41
|
+
raise ValueError(
|
42
|
+
f"label '{v}' contains invalid characters. "
|
43
|
+
"Labels must contain only letters, numbers, and underscores."
|
44
|
+
)
|
45
|
+
|
46
|
+
return v
|
47
|
+
|
48
|
+
@pc.field_validator("pattern")
|
49
|
+
@classmethod
|
50
|
+
def validate_pattern(cls, v: str | None) -> str | None:
|
51
|
+
if v is None:
|
52
|
+
return v
|
53
|
+
|
54
|
+
if not isinstance(v, str):
|
55
|
+
raise ValueError("pattern must be a string")
|
56
|
+
|
57
|
+
if not re.match(r"^[a-zA-Z0-9_\-.${}]+$", v):
|
58
|
+
raise ValueError("Pattern contains unsafe characters")
|
59
|
+
|
60
|
+
if "${frame}" not in v and "$frame" not in v:
|
61
|
+
raise ValueError("Pattern must contain $frame placeholder")
|
62
|
+
|
63
|
+
return v
|
64
|
+
|
65
|
+
@pc.model_validator(mode="after")
|
66
|
+
def validate_pattern_or_file_paths(self):
|
67
|
+
if self.pattern is None and self.file_paths is None:
|
68
|
+
raise ValueError("Either pattern or file_paths must be provided")
|
69
|
+
if self.pattern is not None and self.file_paths is not None:
|
70
|
+
logging.info("Both pattern and file_paths specified; using file_paths.")
|
71
|
+
|
72
|
+
# Validate all paths for traversal attacks and file existence
|
73
|
+
for path in self.path_list:
|
74
|
+
if not path.resolve().is_relative_to(self.directory.resolve()):
|
75
|
+
raise ValueError(
|
76
|
+
f"Path {path} would access files outside base directory"
|
77
|
+
)
|
78
|
+
if not path.is_file():
|
79
|
+
raise ValueError(f"File does not exist: {path}")
|
80
|
+
|
81
|
+
return self
|
82
|
+
|
83
|
+
def path_for_frame(self, frame: int) -> pl.Path:
|
84
|
+
if self.pattern is None:
|
85
|
+
raise ValueError("Cannot use path_for_frame with static file_paths")
|
86
|
+
template = string.Template(self.pattern)
|
87
|
+
filename = template.safe_substitute(frame=frame)
|
88
|
+
return self.directory / filename
|
89
|
+
|
90
|
+
@functools.cached_property
|
91
|
+
def path_list(self) -> list[pl.Path]:
|
92
|
+
"""Return list of file paths, using static paths if provided, otherwise dynamic pattern-based paths."""
|
93
|
+
if self.file_paths is not None:
|
94
|
+
return [self.directory / path for path in self.file_paths]
|
95
|
+
|
96
|
+
paths = []
|
97
|
+
frame = 0
|
98
|
+
while True:
|
99
|
+
path = self.path_for_frame(frame)
|
100
|
+
if not path.is_file():
|
101
|
+
break
|
102
|
+
paths.append(path)
|
103
|
+
frame += 1
|
104
|
+
return paths
|
105
|
+
|
106
|
+
@property
|
107
|
+
def combined_bounds(self) -> list[float]:
|
108
|
+
"""Get combined bounds encompassing all actors."""
|
109
|
+
if not hasattr(self, "actors") or not self.actors:
|
110
|
+
return [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
111
|
+
|
112
|
+
return calculate_combined_bounds(self.actors)
|
113
|
+
|
114
|
+
@functools.cached_property
|
115
|
+
def clipping_planes(self) -> vtk.vtkPlanes:
|
116
|
+
"""Generate clipping planes based on combined bounds of all actors."""
|
117
|
+
if not hasattr(self, "actors") or not self.actors:
|
118
|
+
return None
|
119
|
+
|
120
|
+
bounds = self.combined_bounds
|
121
|
+
planes = vtk.vtkPlanes()
|
122
|
+
self._create_clipping_planes_from_bounds(planes, bounds)
|
123
|
+
return planes
|
124
|
+
|
125
|
+
def _create_clipping_planes_from_bounds(self, planes: vtk.vtkPlanes, bounds):
|
126
|
+
"""Create 6 clipping planes from box bounds."""
|
127
|
+
|
128
|
+
# Create 6 planes for the box faces
|
129
|
+
normals = [
|
130
|
+
[1, 0, 0],
|
131
|
+
[-1, 0, 0], # x-min, x-max
|
132
|
+
[0, 1, 0],
|
133
|
+
[0, -1, 0], # y-min, y-max
|
134
|
+
[0, 0, 1],
|
135
|
+
[0, 0, -1], # z-min, z-max
|
136
|
+
]
|
137
|
+
origins = [
|
138
|
+
[bounds[0], 0, 0],
|
139
|
+
[bounds[1], 0, 0], # x-min, x-max
|
140
|
+
[0, bounds[2], 0],
|
141
|
+
[0, bounds[3], 0], # y-min, y-max
|
142
|
+
[0, 0, bounds[4]],
|
143
|
+
[0, 0, bounds[5]], # z-min, z-max
|
144
|
+
]
|
145
|
+
|
146
|
+
points = vtk.vtkPoints()
|
147
|
+
norms = vtk.vtkDoubleArray()
|
148
|
+
norms.SetNumberOfComponents(3)
|
149
|
+
norms.SetName("Normals")
|
150
|
+
|
151
|
+
for normal, origin in zip(normals, origins):
|
152
|
+
points.InsertNextPoint(origin)
|
153
|
+
norms.InsertNextTuple(normal)
|
154
|
+
|
155
|
+
planes.SetPoints(points)
|
156
|
+
planes.SetNormals(norms)
|
157
|
+
|
158
|
+
def toggle_clipping(self, enabled: bool):
|
159
|
+
"""Enable or disable clipping for all actors."""
|
160
|
+
if not hasattr(self, "actors"):
|
161
|
+
return
|
162
|
+
|
163
|
+
if enabled and self.clipping_planes:
|
164
|
+
# Apply clipping to all actors
|
165
|
+
for actor in self.actors:
|
166
|
+
mapper = actor.GetMapper()
|
167
|
+
mapper.SetClippingPlanes(self.clipping_planes)
|
168
|
+
else:
|
169
|
+
# Remove clipping from all actors
|
170
|
+
for actor in self.actors:
|
171
|
+
mapper = actor.GetMapper()
|
172
|
+
mapper.RemoveAllClippingPlanes()
|
173
|
+
|
174
|
+
def update_clipping_bounds(self, bounds):
|
175
|
+
"""Update clipping bounds from UI controls."""
|
176
|
+
if not self.clipping_planes:
|
177
|
+
return
|
178
|
+
|
179
|
+
# Update clipping planes with new bounds
|
180
|
+
self._create_clipping_planes_from_bounds(self.clipping_planes, bounds)
|
181
|
+
|
182
|
+
# Apply to all actors if clipping is enabled
|
183
|
+
if self.clipping_enabled:
|
184
|
+
for actor in self.actors:
|
185
|
+
mapper = actor.GetMapper()
|
186
|
+
mapper.SetClippingPlanes(self.clipping_planes)
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# System
|
2
|
+
import enum
|
3
|
+
import functools
|
4
|
+
|
5
|
+
# Third Party
|
6
|
+
import pydantic as pc
|
7
|
+
import vtk
|
8
|
+
|
9
|
+
# Internal
|
10
|
+
from .types import RGBColor, ScalarComponent
|
11
|
+
|
12
|
+
|
13
|
+
class Representation(enum.IntEnum):
|
14
|
+
"""Mesh representation modes for VTK rendering."""
|
15
|
+
|
16
|
+
Points = 0
|
17
|
+
Wireframe = 1
|
18
|
+
Surface = 2
|
19
|
+
|
20
|
+
|
21
|
+
class Interpolation(enum.IntEnum):
|
22
|
+
"""Mesh interpolation modes for VTK rendering."""
|
23
|
+
|
24
|
+
Flat = 0
|
25
|
+
Gouraud = 1
|
26
|
+
Phong = 2
|
27
|
+
PBR = 3
|
28
|
+
|
29
|
+
|
30
|
+
class vtkPropertyConfig(pc.BaseModel):
|
31
|
+
"""Configuration for mesh rendering properties."""
|
32
|
+
|
33
|
+
representation: Representation = pc.Field(
|
34
|
+
default=Representation.Surface, description="Rendering representation mode"
|
35
|
+
)
|
36
|
+
color: RGBColor = (1.0, 1.0, 1.0)
|
37
|
+
edge_visibility: bool = pc.Field(default=False, description="Show edges")
|
38
|
+
vertex_visibility: bool = pc.Field(default=False, description="Show vertices")
|
39
|
+
shading: bool = pc.Field(default=True, description="Enable shading")
|
40
|
+
interpolation: Interpolation = pc.Field(
|
41
|
+
default=Interpolation.Gouraud, description="Interpolation mode"
|
42
|
+
)
|
43
|
+
opacity: ScalarComponent = 1.0
|
44
|
+
|
45
|
+
@functools.cached_property
|
46
|
+
def vtk_property(self) -> vtk.vtkProperty:
|
47
|
+
"""Create a fully configured VTK property from this configuration."""
|
48
|
+
_vtk_property = vtk.vtkProperty()
|
49
|
+
_vtk_property.SetRepresentation(self.representation.value)
|
50
|
+
_vtk_property.SetColor(*self.color)
|
51
|
+
_vtk_property.SetEdgeVisibility(self.edge_visibility)
|
52
|
+
_vtk_property.SetVertexVisibility(self.vertex_visibility)
|
53
|
+
_vtk_property.SetShading(self.shading)
|
54
|
+
_vtk_property.SetInterpolation(self.interpolation.value)
|
55
|
+
_vtk_property.SetOpacity(self.opacity)
|
56
|
+
return _vtk_property
|