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 CHANGED
@@ -1,19 +1,27 @@
1
- from .object import Object
1
+ from .logic import Logic
2
2
  from .mesh import Mesh
3
- from .volume import Volume
3
+ from .object import Object
4
4
  from .scene import Scene
5
- from .logic import Logic
6
- from .ui import UI
7
5
  from .screenshot import Screenshot
6
+ from .segmentation import Segmentation
7
+ from .transfer_functions import (
8
+ list_available_presets,
9
+ load_preset,
10
+ )
11
+ from .ui import UI
12
+ from .volume import Volume
8
13
 
9
14
  __all__ = [
10
15
  "Object",
11
16
  "Mesh",
12
17
  "Volume",
18
+ "Segmentation",
13
19
  "Scene",
14
20
  "Screenshot",
15
21
  "UI",
16
22
  "Logic",
23
+ "load_preset",
24
+ "list_available_presets",
17
25
  ]
18
26
 
19
- __version__ = "2023.1.2"
27
+ __version__ = "2025.8.0"
cardio/app.py CHANGED
@@ -1,31 +1,62 @@
1
1
  #!/usr/bin/env python
2
2
 
3
- from trame.app import Server, get_server
3
+ # Third Party
4
+ import pydantic_settings as ps
5
+ import tomlkit as tk
6
+ import trame as tm
4
7
 
8
+ from . import __version__
5
9
  from .logic import Logic
10
+
11
+ # Internal
6
12
  from .scene import Scene
7
13
  from .ui import UI
8
14
 
9
15
 
10
- def main(server=None, **kwargs):
16
+ class CardioApp(tm.app.TrameApp):
17
+ def __init__(self, name=None):
18
+ super().__init__(server=name, client_type="vue2")
19
+
20
+ # Add config file argument to Trame's parser
21
+ self.server.cli.add_argument(
22
+ "--config", help="TOML configuration file.", dest="cfg_file", required=False
23
+ )
24
+
25
+ # Add version argument
26
+ self.server.cli.add_argument(
27
+ "--version", action="version", version=f"cardio {__version__}"
28
+ )
29
+
30
+ # Create CLI settings source with Trame's parser - enable argument parsing
31
+ cli_settings = ps.CliSettingsSource(
32
+ Scene, root_parser=self.server.cli, cli_parse_args=True
33
+ )
11
34
 
12
- if server is None:
13
- server = get_server()
35
+ # Parse arguments to get config file path (use parse_known_args to avoid conflicts)
36
+ args, unknown = self.server.cli.parse_known_args()
37
+ config_file = getattr(args, "cfg_file", None)
14
38
 
15
- if isinstance(server, str):
16
- server = get_server(server)
39
+ # Set the CLI source and config file on the Scene class temporarily
40
+ Scene._cli_source = cli_settings
41
+ Scene._config_file = config_file
17
42
 
18
- server.cli.add_argument(
19
- "--config", help="TOML configutation file.", dest="cfg_file", required=True
20
- )
21
- args = server.cli.parse_args()
43
+ try:
44
+ # Create Scene with CLI and config file support
45
+ scene = Scene()
46
+ finally:
47
+ # Clean up class attributes
48
+ if hasattr(Scene, "_cli_source"):
49
+ delattr(Scene, "_cli_source")
50
+ if hasattr(Scene, "_config_file"):
51
+ delattr(Scene, "_config_file")
22
52
 
23
- scene = Scene(args.cfg_file)
53
+ Logic(self.server, scene)
54
+ UI(self.server, scene)
24
55
 
25
- Logic(server, scene)
26
- UI(server, scene)
27
56
 
28
- server.start(open_browser=False)
57
+ def main():
58
+ app = CardioApp()
59
+ app.server.start(open_browser=False)
29
60
 
30
61
 
31
62
  if __name__ == "__main__":
@@ -0,0 +1,42 @@
1
+ name = "Bone"
2
+ description = "Bone"
3
+
4
+ ambient = 1.0
5
+ diffuse = 1.0
6
+ specular = 0.0
7
+
8
+ [[transfer_functions]]
9
+
10
+ [transfer_functions.opacity]
11
+ [[transfer_functions.opacity.points]]
12
+ x = -1024.0
13
+ y = 0.0
14
+
15
+ [[transfer_functions.opacity.points]]
16
+ x = 54.0
17
+ y = 0.0
18
+
19
+ [[transfer_functions.opacity.points]]
20
+ x = 536.0
21
+ y = 0.1
22
+
23
+ [[transfer_functions.opacity.points]]
24
+ x = 3071.0
25
+ y = 0.1
26
+
27
+ [transfer_functions.color]
28
+ [[transfer_functions.color.points]]
29
+ x = -1024.0
30
+ color = [0.0, 0.0, 0.0]
31
+
32
+ [[transfer_functions.color.points]]
33
+ x = 54.0
34
+ color = [0.0, 0.0, 0.0]
35
+
36
+ [[transfer_functions.color.points]]
37
+ x = 536.0
38
+ color = [1.0, 1.0, 1.0]
39
+
40
+ [[transfer_functions.color.points]]
41
+ x = 3071.0
42
+ color = [1.0, 1.0, 1.0]
@@ -0,0 +1,78 @@
1
+ name = "Vascular_Closed"
2
+ description = "Vascular (Closed)"
3
+
4
+ ambient = 0.7
5
+ diffuse = 0.4
6
+ specular = 1.0
7
+
8
+ [[transfer_functions]]
9
+
10
+ [transfer_functions.opacity]
11
+ [[transfer_functions.opacity.points]]
12
+ x = 150
13
+ y = 0.0
14
+
15
+ [[transfer_functions.opacity.points]]
16
+ x = 650
17
+ y = 1.0
18
+
19
+ [[transfer_functions.opacity.points]]
20
+ x = 3071
21
+ y = 1.0
22
+
23
+ [transfer_functions.color]
24
+ [[transfer_functions.color.points]]
25
+ x = 150
26
+ color = [1.0, 1.0, 1.0]
27
+
28
+ [[transfer_functions.color.points]]
29
+ x = 3071
30
+ color = [1.0, 1.0, 1.0]
31
+
32
+ [[transfer_functions]]
33
+
34
+ [transfer_functions.opacity]
35
+ [[transfer_functions.opacity.points]]
36
+ x = -96.5
37
+ y = 0.0
38
+
39
+ [[transfer_functions.opacity.points]]
40
+ x = 269.0
41
+ y = 0.05
42
+
43
+ [[transfer_functions.opacity.points]]
44
+ x = 474.5
45
+ y = 0.0
46
+
47
+ [transfer_functions.color]
48
+ [[transfer_functions.color.points]]
49
+ x = -96.5
50
+ color = [1.0, 1.0, 1.0]
51
+
52
+ [[transfer_functions.color.points]]
53
+ x = 474.5
54
+ color = [1.0, 1.0, 1.0]
55
+
56
+ [[transfer_functions]]
57
+
58
+ [transfer_functions.opacity]
59
+ [[transfer_functions.opacity.points]]
60
+ x = 120
61
+ y = 0.0
62
+
63
+ [[transfer_functions.opacity.points]]
64
+ x = 258.0
65
+ y = 0.8
66
+
67
+ [[transfer_functions.opacity.points]]
68
+ x = 320
69
+ y = 0.0
70
+
71
+ [transfer_functions.color]
72
+ [[transfer_functions.color.points]]
73
+ x = 120
74
+ color = [1.0, 0.0, 0.0]
75
+
76
+ [[transfer_functions.color.points]]
77
+ x = 320
78
+ color = [1.0, 1.0, 0.0]
@@ -0,0 +1,54 @@
1
+ name = "Vascular_Open"
2
+ description = "Vascular (Open)"
3
+
4
+ ambient = 0.7
5
+ diffuse = 0.4
6
+ specular = 1.0
7
+
8
+ [[transfer_functions]]
9
+
10
+ [transfer_functions.opacity]
11
+ [[transfer_functions.opacity.points]]
12
+ x = -96.5
13
+ y = 0.0
14
+
15
+ [[transfer_functions.opacity.points]]
16
+ x = 269.0
17
+ y = 0.05
18
+
19
+ [[transfer_functions.opacity.points]]
20
+ x = 474.5
21
+ y = 0.0
22
+
23
+ [transfer_functions.color]
24
+ [[transfer_functions.color.points]]
25
+ x = -96.5
26
+ color = [1.0, 1.0, 1.0]
27
+
28
+ [[transfer_functions.color.points]]
29
+ x = 474.5
30
+ color = [1.0, 1.0, 1.0]
31
+
32
+ [[transfer_functions]]
33
+
34
+ [transfer_functions.opacity]
35
+ [[transfer_functions.opacity.points]]
36
+ x = 117
37
+ y = 0.0
38
+
39
+ [[transfer_functions.opacity.points]]
40
+ x = 246.0
41
+ y = 0.87
42
+
43
+ [[transfer_functions.opacity.points]]
44
+ x = 319
45
+ y = 0.0
46
+
47
+ [transfer_functions.color]
48
+ [[transfer_functions.color.points]]
49
+ x = 117
50
+ color = [1.0, 0.0, 0.0]
51
+
52
+ [[transfer_functions.color.points]]
53
+ x = 319
54
+ color = [1.0, 1.0, 0.0]
@@ -0,0 +1,42 @@
1
+ name = "Radiograph"
2
+ description = "Radiograph"
3
+
4
+ ambient = 1.0
5
+ diffuse = 1.0
6
+ specular = 0.0
7
+
8
+ [[transfer_functions]]
9
+
10
+ [transfer_functions.opacity]
11
+ [[transfer_functions.opacity.points]]
12
+ x = -1024.0
13
+ y = 0.0
14
+
15
+ [[transfer_functions.opacity.points]]
16
+ x = -200.0
17
+ y = 0.0
18
+
19
+ [[transfer_functions.opacity.points]]
20
+ x = 300.0
21
+ y = 0.01
22
+
23
+ [[transfer_functions.opacity.points]]
24
+ x = 3071.0
25
+ y = 0.01
26
+
27
+ [transfer_functions.color]
28
+ [[transfer_functions.color.points]]
29
+ x = -1024.0
30
+ color = [0.0, 0.0, 0.0]
31
+
32
+ [[transfer_functions.color.points]]
33
+ x = -200.0
34
+ color = [0.0, 0.0, 0.0]
35
+
36
+ [[transfer_functions.color.points]]
37
+ x = 300.0
38
+ color = [1.0, 1.0, 1.0]
39
+
40
+ [[transfer_functions.color.points]]
41
+ x = 3071.0
42
+ color = [1.0, 1.0, 1.0]
cardio/logic.py CHANGED
@@ -1,8 +1,10 @@
1
1
  import asyncio
2
+ import datetime as dt
2
3
 
3
4
  from trame.app import asynchronous
4
5
 
5
- from . import Scene
6
+ from .scene import Scene
7
+ from .screenshot import Screenshot
6
8
 
7
9
 
8
10
  class Logic:
@@ -12,21 +14,114 @@ class Logic:
12
14
 
13
15
  self.server.state.change("frame")(self.update_frame)
14
16
  self.server.state.change("playing")(self.play)
17
+ self.server.state.change("dark_mode")(self.sync_background_color)
18
+
19
+ # Initialize visibility state variables
20
+ for m in self.scene.meshes:
21
+ self.server.state[f"mesh_visibility_{m.label}"] = m.visible
22
+ for v in self.scene.volumes:
23
+ self.server.state[f"volume_visibility_{v.label}"] = v.visible
24
+ for s in self.scene.segmentations:
25
+ self.server.state[f"segmentation_visibility_{s.label}"] = s.visible
26
+
27
+ # Initialize preset state variables
28
+ for v in self.scene.volumes:
29
+ self.server.state[f"volume_preset_{v.label}"] = v.transfer_function_preset
30
+
31
+ # Initialize clipping state variables
32
+ for m in self.scene.meshes:
33
+ self.server.state[f"mesh_clipping_{m.label}"] = m.clipping_enabled
34
+ for v in self.scene.volumes:
35
+ self.server.state[f"volume_clipping_{v.label}"] = v.clipping_enabled
36
+ for s in self.scene.segmentations:
37
+ self.server.state[f"segmentation_clipping_{s.label}"] = s.clipping_enabled
15
38
  self.server.state.change(
16
39
  *[f"mesh_visibility_{m.label}" for m in self.scene.meshes]
17
40
  )(self.sync_mesh_visibility)
18
41
  self.server.state.change(
19
42
  *[f"volume_visibility_{v.label}" for v in self.scene.volumes]
20
43
  )(self.sync_volume_visibility)
44
+ self.server.state.change(
45
+ *[f"segmentation_visibility_{s.label}" for s in self.scene.segmentations]
46
+ )(self.sync_segmentation_visibility)
47
+ self.server.state.change(
48
+ *[f"volume_preset_{v.label}" for v in self.scene.volumes]
49
+ )(self.sync_volume_presets)
50
+
51
+ # Set up mesh clipping controls
52
+ mesh_clipping_controls = []
53
+ for m in self.scene.meshes:
54
+ mesh_clipping_controls.extend(
55
+ [
56
+ f"mesh_clipping_{m.label}",
57
+ f"clip_x_{m.label}",
58
+ f"clip_y_{m.label}",
59
+ f"clip_z_{m.label}",
60
+ ]
61
+ )
62
+ if mesh_clipping_controls:
63
+ self.server.state.change(*mesh_clipping_controls)(self.sync_mesh_clipping)
64
+
65
+ # Set up volume clipping controls
66
+ volume_clipping_controls = []
67
+ for v in self.scene.volumes:
68
+ volume_clipping_controls.extend(
69
+ [
70
+ f"volume_clipping_{v.label}",
71
+ f"clip_x_{v.label}",
72
+ f"clip_y_{v.label}",
73
+ f"clip_z_{v.label}",
74
+ ]
75
+ )
76
+ if volume_clipping_controls:
77
+ self.server.state.change(*volume_clipping_controls)(
78
+ self.sync_volume_clipping
79
+ )
80
+
81
+ # Set up segmentation clipping controls
82
+ segmentation_clipping_controls = []
83
+ for s in self.scene.segmentations:
84
+ segmentation_clipping_controls.extend(
85
+ [
86
+ f"segmentation_clipping_{s.label}",
87
+ f"clip_x_{s.label}",
88
+ f"clip_y_{s.label}",
89
+ f"clip_z_{s.label}",
90
+ ]
91
+ )
92
+ if segmentation_clipping_controls:
93
+ self.server.state.change(*segmentation_clipping_controls)(
94
+ self.sync_segmentation_clipping
95
+ )
21
96
 
22
97
  self.server.controller.increment_frame = self.increment_frame
23
98
  self.server.controller.decrement_frame = self.decrement_frame
24
99
  self.server.controller.screenshot = self.screenshot
25
100
  self.server.controller.reset_all = self.reset_all
26
101
 
102
+ # Initialize clipping state variables
103
+ self._initialize_clipping_state()
104
+
27
105
  def update_frame(self, frame, **kwargs):
28
106
  self.scene.hide_all_frames()
29
- self.scene.show_frame(frame)
107
+
108
+ # Show frame with server state visibility
109
+ for mesh in self.scene.meshes:
110
+ visible = self.server.state[f"mesh_visibility_{mesh.label}"]
111
+ if visible:
112
+ mesh.actors[frame % len(mesh.actors)].SetVisibility(True)
113
+
114
+ for volume in self.scene.volumes:
115
+ visible = self.server.state[f"volume_visibility_{volume.label}"]
116
+ if visible:
117
+ volume.actors[frame % len(volume.actors)].SetVisibility(True)
118
+
119
+ for segmentation in self.scene.segmentations:
120
+ visible = self.server.state[f"segmentation_visibility_{segmentation.label}"]
121
+ if visible:
122
+ actor = segmentation.actors[frame % len(segmentation.actors)]
123
+ actor.SetVisibility(True)
124
+
30
125
  self.server.controller.view_update()
31
126
 
32
127
  @asynchronous.task
@@ -45,14 +140,113 @@ class Logic:
45
140
 
46
141
  def sync_mesh_visibility(self, **kwargs):
47
142
  for m in self.scene.meshes:
48
- m.visible = self.server.state[f"mesh_visibility_{m.label}"]
49
- m.actors[self.server.state.frame].SetVisibility(m.visible)
143
+ visible = self.server.state[f"mesh_visibility_{m.label}"]
144
+ m.actors[self.server.state.frame % len(m.actors)].SetVisibility(visible)
50
145
  self.server.controller.view_update()
51
146
 
52
147
  def sync_volume_visibility(self, **kwargs):
53
148
  for v in self.scene.volumes:
54
- v.visible = self.server.state[f"volume_visibility_{v.label}"]
55
- v.actors[self.server.state.frame].SetVisibility(v.visible)
149
+ visible = self.server.state[f"volume_visibility_{v.label}"]
150
+ v.actors[self.server.state.frame % len(v.actors)].SetVisibility(visible)
151
+ self.server.controller.view_update()
152
+
153
+ def sync_segmentation_visibility(self, **kwargs):
154
+ for s in self.scene.segmentations:
155
+ visible = self.server.state[f"segmentation_visibility_{s.label}"]
156
+ actor = s.actors[self.server.state.frame % len(s.actors)]
157
+ actor.SetVisibility(visible)
158
+ self.server.controller.view_update()
159
+
160
+ def sync_volume_presets(self, **kwargs):
161
+ """Update volume transfer function presets based on UI selection."""
162
+ from .transfer_functions import load_preset
163
+
164
+ for v in self.scene.volumes:
165
+ preset_name = self.server.state[f"volume_preset_{v.label}"]
166
+ preset = load_preset(preset_name)
167
+
168
+ # Apply preset to all actors
169
+ for actor in v.actors:
170
+ actor.SetProperty(preset.vtk_property)
171
+
172
+ self.server.controller.view_update()
173
+
174
+ def sync_mesh_clipping(self, **kwargs):
175
+ """Update mesh clipping based on UI controls."""
176
+ for m in self.scene.meshes:
177
+ # Toggle clipping on/off
178
+ clipping_enabled = self.server.state[f"mesh_clipping_{m.label}"]
179
+ m.toggle_clipping(clipping_enabled)
180
+
181
+ # Update clipping bounds from sliders
182
+ if clipping_enabled and hasattr(self.server.state, f"clip_x_{m.label}"):
183
+ x_bounds = getattr(self.server.state, f"clip_x_{m.label}")
184
+ y_bounds = getattr(self.server.state, f"clip_y_{m.label}")
185
+ z_bounds = getattr(self.server.state, f"clip_z_{m.label}")
186
+
187
+ bounds = [
188
+ x_bounds[0],
189
+ x_bounds[1], # x_min, x_max
190
+ y_bounds[0],
191
+ y_bounds[1], # y_min, y_max
192
+ z_bounds[0],
193
+ z_bounds[1], # z_min, z_max
194
+ ]
195
+
196
+ m.update_clipping_bounds(bounds)
197
+
198
+ self.server.controller.view_update()
199
+
200
+ def sync_volume_clipping(self, **kwargs):
201
+ """Update volume clipping based on UI controls."""
202
+ for v in self.scene.volumes:
203
+ # Toggle clipping on/off
204
+ clipping_enabled = self.server.state[f"volume_clipping_{v.label}"]
205
+ v.toggle_clipping(clipping_enabled)
206
+
207
+ # Update clipping bounds if enabled
208
+ if clipping_enabled:
209
+ x_range = self.server.state[f"clip_x_{v.label}"]
210
+ y_range = self.server.state[f"clip_y_{v.label}"]
211
+ z_range = self.server.state[f"clip_z_{v.label}"]
212
+
213
+ if x_range and y_range and z_range:
214
+ bounds = [
215
+ x_range[0],
216
+ x_range[1],
217
+ y_range[0],
218
+ y_range[1],
219
+ z_range[0],
220
+ z_range[1],
221
+ ]
222
+ v.update_clipping_bounds(bounds)
223
+
224
+ self.server.controller.view_update()
225
+
226
+ def sync_segmentation_clipping(self, **kwargs):
227
+ """Update segmentation clipping based on UI controls."""
228
+ for s in self.scene.segmentations:
229
+ # Toggle clipping on/off
230
+ clipping_enabled = self.server.state[f"segmentation_clipping_{s.label}"]
231
+ s.toggle_clipping(clipping_enabled)
232
+
233
+ # Update clipping bounds if enabled
234
+ if clipping_enabled:
235
+ x_range = self.server.state[f"clip_x_{s.label}"]
236
+ y_range = self.server.state[f"clip_y_{s.label}"]
237
+ z_range = self.server.state[f"clip_z_{s.label}"]
238
+
239
+ if x_range and y_range and z_range:
240
+ bounds = [
241
+ x_range[0],
242
+ x_range[1],
243
+ y_range[0],
244
+ y_range[1],
245
+ z_range[0],
246
+ z_range[1],
247
+ ]
248
+ s.update_clipping_bounds(bounds)
249
+
56
250
  self.server.controller.view_update()
57
251
 
58
252
  def increment_frame(self):
@@ -68,11 +262,11 @@ class Logic:
68
262
  @asynchronous.task
69
263
  async def screenshot(self):
70
264
  dr = dt.datetime.now().strftime(self.scene.screenshot_subdirectory_format)
71
- dr = f"{self.scene.screenshot_directory}/{dr}"
72
- os.makedirs(dr)
265
+ dr = self.scene.screenshot_directory / dr
266
+ dr.mkdir(parents=True, exist_ok=True)
73
267
 
74
268
  if not (self.server.state.incrementing or self.server.state.rotating):
75
- ss = cy.Screenshot(self.scene.renderWindow)
269
+ ss = Screenshot(self.scene.renderWindow)
76
270
  ss.save(f"{dr}/0.png")
77
271
  else:
78
272
  n = self.scene.nframes
@@ -86,7 +280,7 @@ class Logic:
86
280
  if self.server.state.incrementing:
87
281
  self.increment_frame()
88
282
  self.server.controller.view_update()
89
- ss = cy.Screenshot(self.scene.renderWindow)
283
+ ss = Screenshot(self.scene.renderWindow)
90
284
  ss.save(f"{dr}/{i}.png")
91
285
  await asyncio.sleep(
92
286
  1 / self.server.state.bpm * 60 / self.scene.nframes
@@ -100,3 +294,72 @@ class Logic:
100
294
  self.server.state.bpm = 60
101
295
  self.server.state.bpr = 5
102
296
  self.server.controller.view_update()
297
+
298
+ def sync_background_color(self, dark_mode, **kwargs):
299
+ """Sync VTK renderer background with dark mode."""
300
+ if dark_mode:
301
+ # Dark mode: use dark background from config
302
+ self.scene.renderer.SetBackground(
303
+ *self.scene.background.dark,
304
+ )
305
+ else:
306
+ # Light mode: use light background from config
307
+ self.scene.renderer.SetBackground(
308
+ *self.scene.background.light,
309
+ )
310
+ self.server.controller.view_update()
311
+
312
+ def _initialize_clipping_state(self):
313
+ """Initialize clipping state variables for all objects."""
314
+ # Initialize mesh clipping state
315
+ for m in self.scene.meshes:
316
+ # Initialize panel state
317
+ setattr(self.server.state, f"clip_panel_{m.label}", [])
318
+
319
+ # Initialize range sliders with mesh bounds if available
320
+ if m.actors:
321
+ bounds = m.combined_bounds
322
+ setattr(self.server.state, f"clip_x_{m.label}", [bounds[0], bounds[1]])
323
+ setattr(self.server.state, f"clip_y_{m.label}", [bounds[2], bounds[3]])
324
+ setattr(self.server.state, f"clip_z_{m.label}", [bounds[4], bounds[5]])
325
+
326
+ # Initialize volume clipping state
327
+ for v in self.scene.volumes:
328
+ preset_key = getattr(v, "transfer_function_preset", "cardiac")
329
+ setattr(self.server.state, f"volume_preset_{v.label}", preset_key)
330
+
331
+ # Initialize preset panel state (collapsed by default)
332
+ setattr(self.server.state, f"preset_panel_{v.label}", [])
333
+ if hasattr(v, "clipping_enabled"):
334
+ # Initialize clipping checkbox state
335
+ setattr(
336
+ self.server.state, f"volume_clipping_{v.label}", v.clipping_enabled
337
+ )
338
+
339
+ # Initialize panel state
340
+ setattr(self.server.state, f"clip_panel_{v.label}", [])
341
+
342
+ # Initialize range sliders with volume bounds if available
343
+ if v.actors:
344
+ bounds = v.combined_bounds
345
+ setattr(
346
+ self.server.state, f"clip_x_{v.label}", [bounds[0], bounds[1]]
347
+ )
348
+ setattr(
349
+ self.server.state, f"clip_y_{v.label}", [bounds[2], bounds[3]]
350
+ )
351
+ setattr(
352
+ self.server.state, f"clip_z_{v.label}", [bounds[4], bounds[5]]
353
+ )
354
+
355
+ # Initialize segmentation clipping state
356
+ for s in self.scene.segmentations:
357
+ # Initialize panel state
358
+ setattr(self.server.state, f"clip_panel_{s.label}", [])
359
+
360
+ # Initialize range sliders with segmentation bounds if available
361
+ if s.actors:
362
+ bounds = s.combined_bounds
363
+ setattr(self.server.state, f"clip_x_{s.label}", [bounds[0], bounds[1]])
364
+ setattr(self.server.state, f"clip_y_{s.label}", [bounds[2], bounds[3]])
365
+ setattr(self.server.state, f"clip_z_{s.label}", [bounds[4], bounds[5]])