cardio 2025.8.1__py3-none-any.whl → 2025.10.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/ui.py CHANGED
@@ -1,22 +1,149 @@
1
- from trame.ui.vuetify import SinglePageWithDrawerLayout
1
+ import functools as ft
2
+
3
+ from trame.ui.vuetify3 import SinglePageWithDrawerLayout
2
4
  from trame.widgets import vtk as vtk_widgets
3
- from trame.widgets import vuetify
5
+ from trame.widgets import vuetify3 as vuetify
4
6
 
5
7
  from .scene import Scene
6
- from .transfer_functions import list_available_presets
8
+ from .volume_property_presets import list_volume_property_presets
9
+ from .window_level import presets
7
10
 
8
11
 
9
12
  class UI:
13
+ @property
14
+ def handled_events(self):
15
+ return [
16
+ "MouseMove",
17
+ "LeftButtonPress",
18
+ "LeftButtonRelease",
19
+ "RightButtonPress",
20
+ "RightButtonRelease",
21
+ "KeyPress",
22
+ ]
23
+
24
+ def event_listeners_for_view(self, view_name):
25
+ """Create event listeners for a specific view."""
26
+ result = {}
27
+ for event in self.handled_events:
28
+ callback = ft.partial(self.on_event, view_name=view_name)
29
+ result[event] = (callback, "[utils.vtk.event($event)]")
30
+ return result
31
+
32
+ def on_event(self, *args, view_name=None, **kwargs):
33
+ if not args:
34
+ return
35
+
36
+ event = args[0]
37
+
38
+ match event["type"]:
39
+ case "KeyPress":
40
+ if event["key"].isdigit():
41
+ key_num = int(event["key"])
42
+ if key_num in presets.keys():
43
+ self.server.state.mpr_window_level_preset = key_num
44
+
45
+ case "LeftButtonPress":
46
+ self.left_dragging = True
47
+ self._store_mouse_position(view_name, event)
48
+
49
+ case "LeftButtonRelease":
50
+ self.left_dragging = False
51
+
52
+ case "RightButtonPress":
53
+ self.right_dragging = True
54
+ self._store_mouse_position(view_name, event)
55
+
56
+ case "RightButtonRelease":
57
+ self.right_dragging = False
58
+
59
+ case "MouseMove" if self.left_dragging:
60
+ if (
61
+ view_name in {"axial", "sagittal", "coronal"}
62
+ and view_name in self.last_mouse_pos
63
+ and "position" in event
64
+ ):
65
+ current_pos = [event["position"]["x"], event["position"]["y"]]
66
+ dx = current_pos[0] - self.last_mouse_pos[view_name][0]
67
+ dy = current_pos[1] - self.last_mouse_pos[view_name][1]
68
+ self.last_mouse_pos[view_name] = current_pos
69
+
70
+ current_window = getattr(self.server.state, "mpr_window", 400.0)
71
+ current_level = getattr(self.server.state, "mpr_level", 40.0)
72
+
73
+ window_delta = -dx * self.window_sensitivity
74
+ level_delta = -dy * self.level_sensitivity
75
+ new_window = max(1.0, current_window + window_delta)
76
+ new_level = current_level + level_delta
77
+
78
+ self.server.state.mpr_window = new_window
79
+ self.server.state.mpr_level = new_level
80
+
81
+ case "MouseMove" if self.right_dragging:
82
+ if (
83
+ view_name in {"axial", "sagittal", "coronal"}
84
+ and view_name in self.last_mouse_pos
85
+ and "position" in event
86
+ ):
87
+ current_pos = [event["position"]["x"], event["position"]["y"]]
88
+ dy = current_pos[1] - self.last_mouse_pos[view_name][1]
89
+ self.last_mouse_pos[view_name] = current_pos
90
+
91
+ base_slice_delta = dy * self.slice_sensitivity
92
+ self._handle_slice_scroll(view_name, base_slice_delta)
93
+
94
+ def _store_mouse_position(self, view_name, event):
95
+ """Store mouse position for drag operations."""
96
+ if view_name and "position" in event:
97
+ self.last_mouse_pos[view_name] = [
98
+ event["position"]["x"],
99
+ event["position"]["y"],
100
+ ]
101
+
102
+ def _handle_slice_scroll(self, view_name, base_slice_delta):
103
+ """Handle slice scrolling for a specific view."""
104
+ match view_name:
105
+ case "axial":
106
+ current_slice = getattr(self.server.state, "axial_slice", 0.0)
107
+ bounds = getattr(self.server.state, "axial_slice_bounds", [0.0, 100.0])
108
+ slice_attr = "axial_slice"
109
+ case "sagittal":
110
+ current_slice = getattr(self.server.state, "sagittal_slice", 0.0)
111
+ bounds = getattr(
112
+ self.server.state, "sagittal_slice_bounds", [0.0, 100.0]
113
+ )
114
+ slice_attr = "sagittal_slice"
115
+ case "coronal":
116
+ current_slice = getattr(self.server.state, "coronal_slice", 0.0)
117
+ bounds = getattr(
118
+ self.server.state, "coronal_slice_bounds", [0.0, 100.0]
119
+ )
120
+ slice_attr = "coronal_slice"
121
+ case _:
122
+ return
123
+
124
+ min_bound, max_bound = min(bounds), max(bounds)
125
+ slice_delta = base_slice_delta if bounds[1] > bounds[0] else -base_slice_delta
126
+ new_slice = max(min_bound, min(max_bound, current_slice + slice_delta))
127
+ setattr(self.server.state, slice_attr, new_slice)
128
+
10
129
  def __init__(self, server, scene: Scene):
11
130
  self.server = server
12
131
  self.scene: Scene = scene
132
+ self.left_dragging = False
133
+ self.right_dragging = False
134
+ self.last_mouse_pos = {}
135
+ self.window_sensitivity = 5.0
136
+ self.level_sensitivity = 2.0
137
+ self.slice_sensitivity = 1.0
13
138
 
14
139
  self.setup()
15
140
 
16
141
  def setup(self):
17
142
  self.server.state.trame__title = self.scene.project_name
18
143
 
19
- with SinglePageWithDrawerLayout(self.server) as layout:
144
+ with SinglePageWithDrawerLayout(
145
+ self.server, theme=("theme_mode", "dark")
146
+ ) as layout:
20
147
  layout.icon.click = self.server.controller.view_reset_camera
21
148
  layout.title.set_text(self.scene.project_name)
22
149
 
@@ -25,36 +152,24 @@ class UI:
25
152
 
26
153
  vuetify.VSpacer()
27
154
 
28
- # vuetify.VCheckbox(
29
- # v_model=("viewMode", "local"),
30
- # on_icon="mdi-lan-disconnect",
31
- # off_icon="mdi-lan-connect",
32
- # true_value="local",
33
- # false_value="remote",
34
- # classes="mx-1",
35
- # hide_details=True,
36
- # dense=True,
37
- # )
38
-
39
155
  vuetify.VCheckbox(
40
- v_model=("dark_mode", False),
41
- on_icon="mdi-lightbulb-outline",
42
- off_icon="mdi-lightbulb-off-outline",
43
- classes="mx-1",
44
- hide_details=True,
45
- dense=True,
46
- outlined=True,
47
- change="$vuetify.theme.dark = $event",
156
+ v_model=("theme_mode", "dark"),
157
+ true_value="dark",
158
+ false_value="light",
159
+ label="Dark Mode",
160
+ true_icon="mdi-lightbulb-off-outline",
161
+ false_icon="mdi-lightbulb-outline",
162
+ density="compact",
163
+ style="max-width: 150px;",
48
164
  )
49
165
 
50
- # NOTE: Reset button should be VBtn, but using VCheckbox for consistent sizing/spacing
166
+ # Close button
51
167
  vuetify.VCheckbox(
52
168
  value=False,
53
- on_icon="mdi-undo-variant",
54
- off_icon="mdi-undo-variant",
55
- hide_details=True,
56
- title="Reset All",
57
- click=self.server.controller.reset_all,
169
+ true_icon="mdi-close-circle",
170
+ false_icon="mdi-close-circle",
171
+ label="Close Application",
172
+ click=self.server.controller.close_application,
58
173
  readonly=True,
59
174
  )
60
175
 
@@ -66,26 +181,334 @@ class UI:
66
181
  )
67
182
 
68
183
  with layout.content:
184
+ # Single VR view (default mode)
69
185
  with vuetify.VContainer(
186
+ v_if="!mpr_enabled",
70
187
  fluid=True,
71
188
  classes="pa-0 fill-height",
72
189
  ):
73
190
  view = vtk_widgets.VtkRemoteView(
74
- self.scene.renderWindow, interactive_ratio=1
191
+ self.scene.renderWindow,
192
+ interactor_events=("event_types", self.handled_events),
193
+ **self.event_listeners_for_view("volume"),
194
+ interactive_ratio=1,
75
195
  )
76
- # view = vtk_widgets.VtkLocalView(renderWindow)
77
- # view = vtk_widgets.VtkRemoteLocalView(
78
- # scene.renderWindow,
79
- # namespace="view",
80
- # mode="local",
81
- # interactive_ratio=1,
82
- # )
83
196
  self.server.controller.view_update = view.update
84
197
  self.server.controller.view_reset_camera = view.reset_camera
85
198
  self.server.controller.on_server_ready.add(view.update)
86
199
 
200
+ # Quad-view layout (MPR mode) - directly in content like app.py
201
+ with vuetify.VContainer(
202
+ v_if="mpr_enabled",
203
+ fluid=True,
204
+ classes="pa-0",
205
+ style="height: calc(100vh - 85px);",
206
+ ):
207
+ # Setup MPR render windows in Scene
208
+ self.scene.setup_mpr_render_windows()
209
+
210
+ # First row: Axial and Volume (50% height)
211
+ with vuetify.VRow(classes="ma-0", style="height: 50%;"):
212
+ with vuetify.VCol(
213
+ cols="6", classes="pa-1", style="height: 100%;"
214
+ ):
215
+ # Axial view
216
+ axial_view = vtk_widgets.VtkRemoteView(
217
+ self.scene.axial_renderWindow,
218
+ style="height: 100%; width: 100%;",
219
+ interactor_events=("event_types", self.handled_events),
220
+ **self.event_listeners_for_view("axial"),
221
+ interactive_ratio=1,
222
+ )
223
+ with vuetify.VCol(
224
+ cols="6", classes="pa-1", style="height: 100%;"
225
+ ):
226
+ # Volume view
227
+ volume_view = vtk_widgets.VtkRemoteView(
228
+ self.scene.renderWindow,
229
+ style="height: 100%; width: 100%;",
230
+ interactor_events=("event_types", self.handled_events),
231
+ **self.event_listeners_for_view("volume_mpr"),
232
+ interactive_ratio=1,
233
+ )
234
+
235
+ # Second row: Coronal and Sagittal (50% height)
236
+ with vuetify.VRow(classes="ma-0", style="height: 50%;"):
237
+ with vuetify.VCol(
238
+ cols="6", classes="pa-1", style="height: 100%;"
239
+ ):
240
+ # Coronal view
241
+ coronal_view = vtk_widgets.VtkRemoteView(
242
+ self.scene.coronal_renderWindow,
243
+ style="height: 100%; width: 100%;",
244
+ interactor_events=("event_types", self.handled_events),
245
+ **self.event_listeners_for_view("coronal"),
246
+ interactive_ratio=1,
247
+ )
248
+ with vuetify.VCol(
249
+ cols="6", classes="pa-1", style="height: 100%;"
250
+ ):
251
+ # Sagittal view
252
+ sagittal_view = vtk_widgets.VtkRemoteView(
253
+ self.scene.sagittal_renderWindow,
254
+ style="height: 100%; width: 100%;",
255
+ interactor_events=("event_types", self.handled_events),
256
+ **self.event_listeners_for_view("sagittal"),
257
+ interactive_ratio=1,
258
+ )
259
+
260
+ # Set up controller functions for MPR mode
261
+ self.server.controller.view_update = self._update_all_mpr_views
262
+ self.server.controller.view_reset_camera = volume_view.reset_camera
263
+ self.server.controller.on_server_ready.add(
264
+ self._update_all_mpr_views
265
+ )
266
+
267
+ # Store individual view update functions
268
+ self.server.controller.axial_update = axial_view.update
269
+ self.server.controller.coronal_update = coronal_view.update
270
+ self.server.controller.sagittal_update = sagittal_view.update
271
+ self.server.controller.volume_update = volume_view.update
272
+
87
273
  with layout.drawer:
88
- vuetify.VSubheader("Playback Controls")
274
+ # MPR Mode Toggle (only show when volumes are present)
275
+ if self.scene.volumes:
276
+ vuetify.VListSubheader("View Mode")
277
+ vuetify.VCheckbox(
278
+ v_model=("mpr_enabled", False),
279
+ label="Multi-Planar Reconstruction (MPR)",
280
+ title="Toggle between single 3D view and quad-view with slice planes",
281
+ dense=True,
282
+ hide_details=True,
283
+ )
284
+
285
+ # Volume selection dropdown (only show when MPR is enabled)
286
+ vuetify.VSelect(
287
+ v_if="mpr_enabled",
288
+ v_model=("active_volume_label", ""),
289
+ items=("volume_items", []),
290
+ item_title="text",
291
+ item_value="value",
292
+ title="Select which volume to use for MPR",
293
+ dense=True,
294
+ hide_details=True,
295
+ )
296
+
297
+ # Window/Level controls for MPR
298
+ vuetify.VListSubheader(
299
+ "Window/Level", v_if="mpr_enabled && active_volume_label"
300
+ )
301
+
302
+ vuetify.VSelect(
303
+ v_if="mpr_enabled && active_volume_label",
304
+ v_model=("mpr_window_level_preset", 7),
305
+ items=("mpr_presets", []),
306
+ item_title="text",
307
+ item_value="value",
308
+ label="Preset",
309
+ dense=True,
310
+ hide_details=True,
311
+ )
312
+
313
+ vuetify.VSlider(
314
+ v_if="mpr_enabled && active_volume_label",
315
+ v_model="mpr_window",
316
+ min=1.0,
317
+ max=2000.0,
318
+ step=1.0,
319
+ hint="Window",
320
+ persistent_hint=True,
321
+ dense=True,
322
+ hide_details=False,
323
+ thumb_label=True,
324
+ )
325
+
326
+ vuetify.VSlider(
327
+ v_if="mpr_enabled && active_volume_label",
328
+ v_model="mpr_level",
329
+ min=-1000.0,
330
+ max=1000.0,
331
+ step=1.0,
332
+ hint="Level",
333
+ persistent_hint=True,
334
+ dense=True,
335
+ hide_details=False,
336
+ thumb_label=True,
337
+ )
338
+
339
+ # Slice position controls (only show when MPR is enabled and volume is selected)
340
+ vuetify.VListSubheader(
341
+ "Slice Positions", v_if="mpr_enabled && active_volume_label"
342
+ )
343
+
344
+ vuetify.VSlider(
345
+ v_if="mpr_enabled && active_volume_label",
346
+ v_model=("axial_slice", 0.0),
347
+ min=("axial_slice_bounds[0]", 0.0),
348
+ max=("axial_slice_bounds[1]", 100.0),
349
+ hint="A (I ↔ S)",
350
+ persistent_hint=True,
351
+ dense=True,
352
+ hide_details=False,
353
+ thumb_label=True,
354
+ )
355
+
356
+ vuetify.VSlider(
357
+ v_if="mpr_enabled && active_volume_label",
358
+ v_model=("sagittal_slice", 0.0),
359
+ min=("sagittal_slice_bounds[0]", 0.0),
360
+ max=("sagittal_slice_bounds[1]", 100.0),
361
+ hint="S (R ↔ L)",
362
+ persistent_hint=True,
363
+ dense=True,
364
+ hide_details=False,
365
+ thumb_label=True,
366
+ )
367
+
368
+ vuetify.VSlider(
369
+ v_if="mpr_enabled && active_volume_label",
370
+ v_model=("coronal_slice", 0.0),
371
+ min=("coronal_slice_bounds[0]", 0.0),
372
+ max=("coronal_slice_bounds[1]", 100.0),
373
+ hint="C (P ↔ A)",
374
+ persistent_hint=True,
375
+ dense=True,
376
+ hide_details=False,
377
+ thumb_label=True,
378
+ )
379
+
380
+ # MPR Rotation controls
381
+ vuetify.VListSubheader(
382
+ "Rotations", v_if="mpr_enabled && active_volume_label"
383
+ )
384
+
385
+ # Rotation buttons
386
+ with vuetify.VRow(
387
+ v_if="mpr_enabled && active_volume_label",
388
+ no_gutters=True,
389
+ classes="mb-2",
390
+ ):
391
+ with vuetify.VCol(cols="4"):
392
+ vuetify.VBtn(
393
+ "X",
394
+ click=self.server.controller.add_x_rotation,
395
+ small=True,
396
+ dense=True,
397
+ outlined=True,
398
+ color="primary",
399
+ )
400
+ with vuetify.VCol(cols="4"):
401
+ vuetify.VBtn(
402
+ "Y",
403
+ click=self.server.controller.add_y_rotation,
404
+ small=True,
405
+ dense=True,
406
+ outlined=True,
407
+ color="primary",
408
+ )
409
+ with vuetify.VCol(cols="4"):
410
+ vuetify.VBtn(
411
+ "Z",
412
+ click=self.server.controller.add_z_rotation,
413
+ small=True,
414
+ dense=True,
415
+ outlined=True,
416
+ color="primary",
417
+ )
418
+
419
+ # Reset rotations button
420
+ vuetify.VBtn(
421
+ "Reset",
422
+ v_if="mpr_enabled && active_volume_label && mpr_rotation_sequence && mpr_rotation_sequence.length > 0",
423
+ click=self.server.controller.reset_rotations,
424
+ small=True,
425
+ dense=True,
426
+ outlined=True,
427
+ color="warning",
428
+ block=True,
429
+ classes="mb-2",
430
+ prepend_icon="mdi-refresh",
431
+ )
432
+
433
+ # Individual rotation sliders
434
+ for i in range(self.scene.max_mpr_rotations):
435
+ with vuetify.VRow(
436
+ v_if=f"mpr_enabled && active_volume_label && mpr_rotation_sequence && mpr_rotation_sequence.length > {i}",
437
+ no_gutters=True,
438
+ classes="align-center mb-1",
439
+ ):
440
+ with vuetify.VCol(cols="8"):
441
+ vuetify.VSlider(
442
+ v_model=(f"mpr_rotation_angle_{i}", 0),
443
+ min=-180,
444
+ max=180,
445
+ step=1,
446
+ hint=(
447
+ f"mpr_rotation_axis_{i}",
448
+ f"Rotation {i + 1}",
449
+ ),
450
+ persistent_hint=True,
451
+ dense=True,
452
+ hide_details=False,
453
+ thumb_label=True,
454
+ )
455
+ with vuetify.VCol(cols="2"):
456
+ vuetify.VCheckbox(
457
+ v_model=(f"mpr_rotation_visible_{i}", True),
458
+ true_icon="mdi-eye",
459
+ false_icon="mdi-eye-off",
460
+ hide_details=True,
461
+ dense=True,
462
+ title="Toggle this rotation and all subsequent ones",
463
+ )
464
+ with vuetify.VCol(cols="2"):
465
+ vuetify.VBtn(
466
+ icon="mdi-delete",
467
+ click=ft.partial(
468
+ self.server.controller.remove_rotation_event, i
469
+ ),
470
+ small=True,
471
+ dense=True,
472
+ color="error",
473
+ title="Remove this rotation and all subsequent ones",
474
+ )
475
+
476
+ # Angle units selector
477
+ with vuetify.VRow(
478
+ v_if="mpr_enabled && active_volume_label",
479
+ no_gutters=True,
480
+ classes="align-center mb-2 mt-2",
481
+ ):
482
+ with vuetify.VCol(cols="4"):
483
+ vuetify.VLabel("Units:")
484
+ with vuetify.VCol(cols="8"):
485
+ vuetify.VSelect(
486
+ v_model=("angle_units", "degrees"),
487
+ items=("angle_units_items", []),
488
+ item_title="text",
489
+ item_value="value",
490
+ dense=True,
491
+ hide_details=True,
492
+ outlined=True,
493
+ )
494
+
495
+ # Save rotations button
496
+ vuetify.VBtn(
497
+ "Save Rotations",
498
+ v_if="mpr_enabled && active_volume_label && mpr_rotation_sequence && mpr_rotation_sequence.length > 0",
499
+ click=self.server.controller.save_rotation_angles,
500
+ small=True,
501
+ dense=True,
502
+ outlined=True,
503
+ color="success",
504
+ block=True,
505
+ classes="mb-2",
506
+ prepend_icon="mdi-content-save",
507
+ )
508
+
509
+ vuetify.VDivider(classes="my-2")
510
+
511
+ vuetify.VListSubheader("Playback Controls")
89
512
 
90
513
  with vuetify.VToolbar(dense=True, flat=True):
91
514
  # NOTE: Previous/Next controls should be VBtn components, but we use
@@ -93,8 +516,8 @@ class UI:
93
516
  # This may be easier to fix in Vuetify 3.
94
517
  vuetify.VCheckbox(
95
518
  value=False,
96
- on_icon="mdi-skip-previous-circle",
97
- off_icon="mdi-skip-previous-circle",
519
+ true_icon="mdi-skip-previous-circle",
520
+ false_icon="mdi-skip-previous-circle",
98
521
  hide_details=True,
99
522
  title="Previous",
100
523
  click=self.server.controller.decrement_frame,
@@ -105,8 +528,8 @@ class UI:
105
528
 
106
529
  vuetify.VCheckbox(
107
530
  value=False,
108
- on_icon="mdi-skip-next-circle",
109
- off_icon="mdi-skip-next-circle",
531
+ true_icon="mdi-skip-next-circle",
532
+ false_icon="mdi-skip-next-circle",
110
533
  hide_details=True,
111
534
  title="Next",
112
535
  click=self.server.controller.increment_frame,
@@ -117,8 +540,8 @@ class UI:
117
540
 
118
541
  vuetify.VCheckbox(
119
542
  v_model=("playing", False),
120
- on_icon="mdi-pause-circle",
121
- off_icon="mdi-play-circle",
543
+ true_icon="mdi-pause-circle",
544
+ false_icon="mdi-play-circle",
122
545
  title="Play/Pause",
123
546
  hide_details=True,
124
547
  )
@@ -127,8 +550,8 @@ class UI:
127
550
 
128
551
  vuetify.VCheckbox(
129
552
  v_model=("incrementing", True),
130
- on_icon="mdi-movie-open-outline",
131
- off_icon="mdi-movie-open-off-outline",
553
+ true_icon="mdi-movie-open-outline",
554
+ false_icon="mdi-movie-open-off-outline",
132
555
  hide_details=True,
133
556
  title="Incrementing",
134
557
  )
@@ -137,8 +560,8 @@ class UI:
137
560
 
138
561
  vuetify.VCheckbox(
139
562
  v_model=("rotating", False),
140
- on_icon="mdi-autorenew",
141
- off_icon="mdi-autorenew-off",
563
+ true_icon="mdi-autorenew",
564
+ false_icon="mdi-autorenew-off",
142
565
  hide_details=True,
143
566
  title="Rotating",
144
567
  )
@@ -187,16 +610,21 @@ class UI:
187
610
 
188
611
  with vuetify.VRow(justify="center", classes="my-3"):
189
612
  vuetify.VBtn(
190
- f"Capture Cine",
613
+ "Capture Cine",
191
614
  small=True,
615
+ dense=True,
616
+ outlined=True,
617
+ color="info",
618
+ block=True,
192
619
  click=self.server.controller.screenshot,
193
620
  title=f"Capture cine to {self.scene.screenshot_directory}",
621
+ prepend_icon="mdi-video",
194
622
  )
195
623
 
196
- vuetify.VSubheader("Appearance and Visibility")
624
+ vuetify.VListSubheader("Appearance and Visibility")
197
625
 
198
626
  if self.scene.meshes:
199
- vuetify.VSubheader("Meshes", classes="text-caption pl-4")
627
+ vuetify.VListSubheader("Meshes", classes="text-caption pl-4")
200
628
  for i, m in enumerate(self.scene.meshes):
201
629
  vuetify.VCheckbox(
202
630
  v_model=f"mesh_visibility_{m.label}",
@@ -233,8 +661,8 @@ class UI:
233
661
  style="max-width: 270px;",
234
662
  ):
235
663
  with vuetify.VExpansionPanel():
236
- vuetify.VExpansionPanelHeader("Clip Bounds")
237
- with vuetify.VExpansionPanelContent():
664
+ vuetify.VExpansionPanelTitle("Clip Bounds")
665
+ with vuetify.VExpansionPanelText():
238
666
  # X bounds
239
667
  vuetify.VRangeSlider(
240
668
  v_model=(
@@ -279,7 +707,7 @@ class UI:
279
707
  )
280
708
 
281
709
  if self.scene.volumes:
282
- vuetify.VSubheader("Volumes", classes="text-caption pl-4")
710
+ vuetify.VListSubheader("Volumes", classes="text-caption pl-4")
283
711
  for i, v in enumerate(self.scene.volumes):
284
712
  vuetify.VCheckbox(
285
713
  v_model=f"volume_visibility_{v.label}",
@@ -292,11 +720,7 @@ class UI:
292
720
  )
293
721
 
294
722
  # Preset selection in collapsible panel
295
- available_presets = list_available_presets()
296
- current_preset = self.server.state[f"volume_preset_{v.label}"]
297
- current_desc = available_presets.get(
298
- current_preset, current_preset
299
- )
723
+ available_presets = list_volume_property_presets()
300
724
 
301
725
  with vuetify.VExpansionPanels(
302
726
  v_model=f"preset_panel_{v.label}",
@@ -306,8 +730,8 @@ class UI:
306
730
  style="max-width: 270px;",
307
731
  ):
308
732
  with vuetify.VExpansionPanel():
309
- vuetify.VExpansionPanelHeader("Transfer Function")
310
- with vuetify.VExpansionPanelContent():
733
+ vuetify.VExpansionPanelTitle("Transfer Function")
734
+ with vuetify.VExpansionPanelText():
311
735
  with vuetify.VRadioGroup(
312
736
  v_model=f"volume_preset_{v.label}",
313
737
  dense=True,
@@ -347,8 +771,8 @@ class UI:
347
771
  style="max-width: 270px;",
348
772
  ):
349
773
  with vuetify.VExpansionPanel():
350
- vuetify.VExpansionPanelHeader("Clip Bounds")
351
- with vuetify.VExpansionPanelContent():
774
+ vuetify.VExpansionPanelTitle("Clip Bounds")
775
+ with vuetify.VExpansionPanelText():
352
776
  # X bounds
353
777
  vuetify.VRangeSlider(
354
778
  v_model=(
@@ -393,7 +817,7 @@ class UI:
393
817
  )
394
818
 
395
819
  if self.scene.segmentations:
396
- vuetify.VSubheader("Segmentations", classes="text-caption pl-4")
820
+ vuetify.VListSubheader("Segmentations", classes="text-caption pl-4")
397
821
  for i, s in enumerate(self.scene.segmentations):
398
822
  vuetify.VCheckbox(
399
823
  v_model=f"segmentation_visibility_{s.label}",
@@ -431,8 +855,8 @@ class UI:
431
855
  style="max-width: 270px;",
432
856
  ):
433
857
  with vuetify.VExpansionPanel():
434
- vuetify.VExpansionPanelHeader("Clip Bounds")
435
- with vuetify.VExpansionPanelContent():
858
+ vuetify.VExpansionPanelTitle("Clip Bounds")
859
+ with vuetify.VExpansionPanelText():
436
860
  # X bounds
437
861
  vuetify.VRangeSlider(
438
862
  v_model=(
@@ -475,3 +899,14 @@ class UI:
475
899
  dense=True,
476
900
  thumb_label=False,
477
901
  )
902
+
903
+ def _update_all_mpr_views(self, **kwargs):
904
+ """Update all MPR views."""
905
+ if hasattr(self.server.controller, "axial_update"):
906
+ self.server.controller.axial_update()
907
+ if hasattr(self.server.controller, "coronal_update"):
908
+ self.server.controller.coronal_update()
909
+ if hasattr(self.server.controller, "sagittal_update"):
910
+ self.server.controller.sagittal_update()
911
+ if hasattr(self.server.controller, "volume_update"):
912
+ self.server.controller.volume_update()