cardio 2025.8.0__py3-none-any.whl → 2025.9.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,9 +1,9 @@
1
- from trame.ui.vuetify import SinglePageWithDrawerLayout
1
+ from trame.ui.vuetify3 import SinglePageWithDrawerLayout
2
2
  from trame.widgets import vtk as vtk_widgets
3
- from trame.widgets import vuetify
3
+ from trame.widgets import vuetify3 as vuetify
4
4
 
5
5
  from .scene import Scene
6
- from .transfer_functions import list_available_presets
6
+ from .volume_property_presets import list_volume_property_presets
7
7
 
8
8
 
9
9
  class UI:
@@ -16,7 +16,9 @@ class UI:
16
16
  def setup(self):
17
17
  self.server.state.trame__title = self.scene.project_name
18
18
 
19
- with SinglePageWithDrawerLayout(self.server) as layout:
19
+ with SinglePageWithDrawerLayout(
20
+ self.server, theme=("theme_mode", "dark")
21
+ ) as layout:
20
22
  layout.icon.click = self.server.controller.view_reset_camera
21
23
  layout.title.set_text(self.scene.project_name)
22
24
 
@@ -25,36 +27,24 @@ class UI:
25
27
 
26
28
  vuetify.VSpacer()
27
29
 
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
30
  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",
31
+ v_model=("theme_mode", "dark"),
32
+ true_value="dark",
33
+ false_value="light",
34
+ label="Dark Mode",
35
+ true_icon="mdi-lightbulb-off-outline",
36
+ false_icon="mdi-lightbulb-outline",
37
+ density="compact",
38
+ style="max-width: 150px;",
48
39
  )
49
40
 
50
- # NOTE: Reset button should be VBtn, but using VCheckbox for consistent sizing/spacing
41
+ # Close button
51
42
  vuetify.VCheckbox(
52
43
  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,
44
+ true_icon="mdi-close-circle",
45
+ false_icon="mdi-close-circle",
46
+ label="Close Application",
47
+ click=self.server.controller.close_application,
58
48
  readonly=True,
59
49
  )
60
50
 
@@ -66,26 +56,261 @@ class UI:
66
56
  )
67
57
 
68
58
  with layout.content:
59
+ # Single VR view (default mode)
69
60
  with vuetify.VContainer(
61
+ v_if="!mpr_enabled",
70
62
  fluid=True,
71
63
  classes="pa-0 fill-height",
72
64
  ):
73
65
  view = vtk_widgets.VtkRemoteView(
74
66
  self.scene.renderWindow, interactive_ratio=1
75
67
  )
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
68
  self.server.controller.view_update = view.update
84
69
  self.server.controller.view_reset_camera = view.reset_camera
85
70
  self.server.controller.on_server_ready.add(view.update)
86
71
 
72
+ # Quad-view layout (MPR mode) - directly in content like app.py
73
+ with vuetify.VContainer(
74
+ v_if="mpr_enabled",
75
+ fluid=True,
76
+ classes="pa-0",
77
+ style="height: calc(100vh - 85px);",
78
+ ):
79
+ # Setup MPR render windows in Scene
80
+ self.scene.setup_mpr_render_windows()
81
+
82
+ # First row: Axial and Volume (50% height)
83
+ with vuetify.VRow(classes="ma-0", style="height: 50%;"):
84
+ with vuetify.VCol(
85
+ cols="6", classes="pa-1", style="height: 100%;"
86
+ ):
87
+ # Axial view
88
+ axial_view = vtk_widgets.VtkRemoteView(
89
+ self.scene.axial_renderWindow,
90
+ style="height: 100%; width: 100%;",
91
+ interactive_ratio=1,
92
+ )
93
+ with vuetify.VCol(
94
+ cols="6", classes="pa-1", style="height: 100%;"
95
+ ):
96
+ # Volume view (reuse existing render window)
97
+ volume_view = vtk_widgets.VtkRemoteView(
98
+ self.scene.renderWindow,
99
+ style="height: 100%; width: 100%;",
100
+ interactive_ratio=1,
101
+ )
102
+
103
+ # Second row: Coronal and Sagittal (50% height)
104
+ with vuetify.VRow(classes="ma-0", style="height: 50%;"):
105
+ with vuetify.VCol(
106
+ cols="6", classes="pa-1", style="height: 100%;"
107
+ ):
108
+ # Coronal view
109
+ coronal_view = vtk_widgets.VtkRemoteView(
110
+ self.scene.coronal_renderWindow,
111
+ style="height: 100%; width: 100%;",
112
+ interactive_ratio=1,
113
+ )
114
+ with vuetify.VCol(
115
+ cols="6", classes="pa-1", style="height: 100%;"
116
+ ):
117
+ # Sagittal view
118
+ sagittal_view = vtk_widgets.VtkRemoteView(
119
+ self.scene.sagittal_renderWindow,
120
+ style="height: 100%; width: 100%;",
121
+ interactive_ratio=1,
122
+ )
123
+
124
+ # Set up controller functions for MPR mode
125
+ self.server.controller.view_update = self._update_all_mpr_views
126
+ self.server.controller.view_reset_camera = volume_view.reset_camera
127
+ self.server.controller.on_server_ready.add(
128
+ self._update_all_mpr_views
129
+ )
130
+
131
+ # Store individual view update functions
132
+ self.server.controller.axial_update = axial_view.update
133
+ self.server.controller.coronal_update = coronal_view.update
134
+ self.server.controller.sagittal_update = sagittal_view.update
135
+ self.server.controller.volume_update = volume_view.update
136
+
87
137
  with layout.drawer:
88
- vuetify.VSubheader("Playback Controls")
138
+ # MPR Mode Toggle (only show when volumes are present)
139
+ if self.scene.volumes:
140
+ vuetify.VListSubheader("View Mode")
141
+ vuetify.VCheckbox(
142
+ v_model=("mpr_enabled", False),
143
+ label="Multi-Planar Reconstruction (MPR)",
144
+ title="Toggle between single 3D view and quad-view with slice planes",
145
+ dense=True,
146
+ hide_details=True,
147
+ )
148
+
149
+ # Volume selection dropdown (only show when MPR is enabled)
150
+ vuetify.VSelect(
151
+ v_if="mpr_enabled",
152
+ v_model=("active_volume_label", ""),
153
+ items=("volume_items", []),
154
+ item_title="text",
155
+ item_value="value",
156
+ title="Select which volume to use for MPR",
157
+ dense=True,
158
+ hide_details=True,
159
+ )
160
+
161
+ # Window/Level controls for MPR
162
+ vuetify.VListSubheader(
163
+ "Window/Level", v_if="mpr_enabled && active_volume_label"
164
+ )
165
+
166
+ vuetify.VSelect(
167
+ v_if="mpr_enabled && active_volume_label",
168
+ v_model=("mpr_window_level_preset", 7),
169
+ items=("mpr_presets", []),
170
+ item_title="text",
171
+ item_value="value",
172
+ label="Preset",
173
+ dense=True,
174
+ hide_details=True,
175
+ )
176
+
177
+ vuetify.VSlider(
178
+ v_if="mpr_enabled && active_volume_label",
179
+ v_model="mpr_window",
180
+ min=1.0,
181
+ max=2000.0,
182
+ step=1.0,
183
+ hint="Window",
184
+ persistent_hint=True,
185
+ dense=True,
186
+ hide_details=False,
187
+ thumb_label=True,
188
+ )
189
+
190
+ vuetify.VSlider(
191
+ v_if="mpr_enabled && active_volume_label",
192
+ v_model="mpr_level",
193
+ min=-1000.0,
194
+ max=1000.0,
195
+ step=1.0,
196
+ hint="Level",
197
+ persistent_hint=True,
198
+ dense=True,
199
+ hide_details=False,
200
+ thumb_label=True,
201
+ )
202
+
203
+ # Slice position controls (only show when MPR is enabled and volume is selected)
204
+ vuetify.VListSubheader(
205
+ "Slice Positions", v_if="mpr_enabled && active_volume_label"
206
+ )
207
+
208
+ vuetify.VSlider(
209
+ v_if="mpr_enabled && active_volume_label",
210
+ v_model=("axial_slice", 0.5),
211
+ min=0.0,
212
+ max=1.0,
213
+ step=0.01,
214
+ hint="A (I ↔ S)",
215
+ persistent_hint=True,
216
+ dense=True,
217
+ hide_details=False,
218
+ thumb_label=True,
219
+ )
220
+
221
+ vuetify.VSlider(
222
+ v_if="mpr_enabled && active_volume_label",
223
+ v_model=("sagittal_slice", 0.5),
224
+ min=0.0,
225
+ max=1.0,
226
+ step=0.01,
227
+ hint="S (R ↔ L)",
228
+ persistent_hint=True,
229
+ dense=True,
230
+ hide_details=False,
231
+ thumb_label=True,
232
+ )
233
+
234
+ vuetify.VSlider(
235
+ v_if="mpr_enabled && active_volume_label",
236
+ v_model=("coronal_slice", 0.5),
237
+ min=0.0,
238
+ max=1.0,
239
+ step=0.01,
240
+ hint="C (P ↔ A)",
241
+ persistent_hint=True,
242
+ dense=True,
243
+ hide_details=False,
244
+ thumb_label=True,
245
+ )
246
+
247
+ # MPR Rotation controls
248
+ vuetify.VListSubheader(
249
+ "Rotations", v_if="mpr_enabled && active_volume_label"
250
+ )
251
+
252
+ # Rotation buttons
253
+ with vuetify.VRow(
254
+ v_if="mpr_enabled && active_volume_label",
255
+ no_gutters=True,
256
+ classes="mb-2",
257
+ ):
258
+ with vuetify.VCol(cols="4"):
259
+ vuetify.VBtn(
260
+ "X",
261
+ click=self.server.controller.add_x_rotation,
262
+ small=True,
263
+ dense=True,
264
+ outlined=True,
265
+ )
266
+ with vuetify.VCol(cols="4"):
267
+ vuetify.VBtn(
268
+ "Y",
269
+ click=self.server.controller.add_y_rotation,
270
+ small=True,
271
+ dense=True,
272
+ outlined=True,
273
+ )
274
+ with vuetify.VCol(cols="4"):
275
+ vuetify.VBtn(
276
+ "Z",
277
+ click=self.server.controller.add_z_rotation,
278
+ small=True,
279
+ dense=True,
280
+ outlined=True,
281
+ )
282
+
283
+ # Reset rotations button
284
+ vuetify.VBtn(
285
+ "Reset",
286
+ v_if="mpr_enabled && active_volume_label && mpr_rotation_sequence && mpr_rotation_sequence.length > 0",
287
+ click=self.server.controller.reset_rotations,
288
+ small=True,
289
+ dense=True,
290
+ outlined=True,
291
+ color="warning",
292
+ block=True,
293
+ classes="mb-2",
294
+ )
295
+
296
+ # Individual rotation sliders (show up to 10)
297
+ for i in range(10):
298
+ vuetify.VSlider(
299
+ v_if=f"mpr_enabled && active_volume_label && mpr_rotation_sequence && mpr_rotation_sequence.length > {i}",
300
+ v_model=(f"mpr_rotation_angle_{i}", 0),
301
+ min=-180,
302
+ max=180,
303
+ step=1,
304
+ hint=(f"mpr_rotation_axis_{i}", f"Rotation {i + 1}"),
305
+ persistent_hint=True,
306
+ dense=True,
307
+ hide_details=False,
308
+ thumb_label=True,
309
+ )
310
+
311
+ vuetify.VDivider(classes="my-2")
312
+
313
+ vuetify.VListSubheader("Playback Controls")
89
314
 
90
315
  with vuetify.VToolbar(dense=True, flat=True):
91
316
  # NOTE: Previous/Next controls should be VBtn components, but we use
@@ -93,8 +318,8 @@ class UI:
93
318
  # This may be easier to fix in Vuetify 3.
94
319
  vuetify.VCheckbox(
95
320
  value=False,
96
- on_icon="mdi-skip-previous-circle",
97
- off_icon="mdi-skip-previous-circle",
321
+ true_icon="mdi-skip-previous-circle",
322
+ false_icon="mdi-skip-previous-circle",
98
323
  hide_details=True,
99
324
  title="Previous",
100
325
  click=self.server.controller.decrement_frame,
@@ -105,8 +330,8 @@ class UI:
105
330
 
106
331
  vuetify.VCheckbox(
107
332
  value=False,
108
- on_icon="mdi-skip-next-circle",
109
- off_icon="mdi-skip-next-circle",
333
+ true_icon="mdi-skip-next-circle",
334
+ false_icon="mdi-skip-next-circle",
110
335
  hide_details=True,
111
336
  title="Next",
112
337
  click=self.server.controller.increment_frame,
@@ -117,8 +342,8 @@ class UI:
117
342
 
118
343
  vuetify.VCheckbox(
119
344
  v_model=("playing", False),
120
- on_icon="mdi-pause-circle",
121
- off_icon="mdi-play-circle",
345
+ true_icon="mdi-pause-circle",
346
+ false_icon="mdi-play-circle",
122
347
  title="Play/Pause",
123
348
  hide_details=True,
124
349
  )
@@ -127,8 +352,8 @@ class UI:
127
352
 
128
353
  vuetify.VCheckbox(
129
354
  v_model=("incrementing", True),
130
- on_icon="mdi-movie-open-outline",
131
- off_icon="mdi-movie-open-off-outline",
355
+ true_icon="mdi-movie-open-outline",
356
+ false_icon="mdi-movie-open-off-outline",
132
357
  hide_details=True,
133
358
  title="Incrementing",
134
359
  )
@@ -137,8 +362,8 @@ class UI:
137
362
 
138
363
  vuetify.VCheckbox(
139
364
  v_model=("rotating", False),
140
- on_icon="mdi-autorenew",
141
- off_icon="mdi-autorenew-off",
365
+ true_icon="mdi-autorenew",
366
+ false_icon="mdi-autorenew-off",
142
367
  hide_details=True,
143
368
  title="Rotating",
144
369
  )
@@ -193,10 +418,10 @@ class UI:
193
418
  title=f"Capture cine to {self.scene.screenshot_directory}",
194
419
  )
195
420
 
196
- vuetify.VSubheader("Appearance and Visibility")
421
+ vuetify.VListSubheader("Appearance and Visibility")
197
422
 
198
423
  if self.scene.meshes:
199
- vuetify.VSubheader("Meshes", classes="text-caption pl-4")
424
+ vuetify.VListSubheader("Meshes", classes="text-caption pl-4")
200
425
  for i, m in enumerate(self.scene.meshes):
201
426
  vuetify.VCheckbox(
202
427
  v_model=f"mesh_visibility_{m.label}",
@@ -233,8 +458,8 @@ class UI:
233
458
  style="max-width: 270px;",
234
459
  ):
235
460
  with vuetify.VExpansionPanel():
236
- vuetify.VExpansionPanelHeader("Clip Bounds")
237
- with vuetify.VExpansionPanelContent():
461
+ vuetify.VExpansionPanelTitle("Clip Bounds")
462
+ with vuetify.VExpansionPanelText():
238
463
  # X bounds
239
464
  vuetify.VRangeSlider(
240
465
  v_model=(
@@ -279,7 +504,7 @@ class UI:
279
504
  )
280
505
 
281
506
  if self.scene.volumes:
282
- vuetify.VSubheader("Volumes", classes="text-caption pl-4")
507
+ vuetify.VListSubheader("Volumes", classes="text-caption pl-4")
283
508
  for i, v in enumerate(self.scene.volumes):
284
509
  vuetify.VCheckbox(
285
510
  v_model=f"volume_visibility_{v.label}",
@@ -292,7 +517,7 @@ class UI:
292
517
  )
293
518
 
294
519
  # Preset selection in collapsible panel
295
- available_presets = list_available_presets()
520
+ available_presets = list_volume_property_presets()
296
521
  current_preset = self.server.state[f"volume_preset_{v.label}"]
297
522
  current_desc = available_presets.get(
298
523
  current_preset, current_preset
@@ -306,8 +531,8 @@ class UI:
306
531
  style="max-width: 270px;",
307
532
  ):
308
533
  with vuetify.VExpansionPanel():
309
- vuetify.VExpansionPanelHeader("Transfer Function")
310
- with vuetify.VExpansionPanelContent():
534
+ vuetify.VExpansionPanelTitle("Transfer Function")
535
+ with vuetify.VExpansionPanelText():
311
536
  with vuetify.VRadioGroup(
312
537
  v_model=f"volume_preset_{v.label}",
313
538
  dense=True,
@@ -347,8 +572,8 @@ class UI:
347
572
  style="max-width: 270px;",
348
573
  ):
349
574
  with vuetify.VExpansionPanel():
350
- vuetify.VExpansionPanelHeader("Clip Bounds")
351
- with vuetify.VExpansionPanelContent():
575
+ vuetify.VExpansionPanelTitle("Clip Bounds")
576
+ with vuetify.VExpansionPanelText():
352
577
  # X bounds
353
578
  vuetify.VRangeSlider(
354
579
  v_model=(
@@ -393,7 +618,7 @@ class UI:
393
618
  )
394
619
 
395
620
  if self.scene.segmentations:
396
- vuetify.VSubheader("Segmentations", classes="text-caption pl-4")
621
+ vuetify.VListSubheader("Segmentations", classes="text-caption pl-4")
397
622
  for i, s in enumerate(self.scene.segmentations):
398
623
  vuetify.VCheckbox(
399
624
  v_model=f"segmentation_visibility_{s.label}",
@@ -431,8 +656,8 @@ class UI:
431
656
  style="max-width: 270px;",
432
657
  ):
433
658
  with vuetify.VExpansionPanel():
434
- vuetify.VExpansionPanelHeader("Clip Bounds")
435
- with vuetify.VExpansionPanelContent():
659
+ vuetify.VExpansionPanelTitle("Clip Bounds")
660
+ with vuetify.VExpansionPanelText():
436
661
  # X bounds
437
662
  vuetify.VRangeSlider(
438
663
  v_model=(
@@ -475,3 +700,14 @@ class UI:
475
700
  dense=True,
476
701
  thumb_label=False,
477
702
  )
703
+
704
+ def _update_all_mpr_views(self, **kwargs):
705
+ """Update all MPR views."""
706
+ if hasattr(self.server.controller, "axial_update"):
707
+ self.server.controller.axial_update()
708
+ if hasattr(self.server.controller, "coronal_update"):
709
+ self.server.controller.coronal_update()
710
+ if hasattr(self.server.controller, "sagittal_update"):
711
+ self.server.controller.sagittal_update()
712
+ if hasattr(self.server.controller, "volume_update"):
713
+ self.server.controller.volume_update()