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/__init__.py +9 -7
- cardio/app.py +6 -13
- cardio/blend_transfer_functions.py +84 -0
- cardio/color_transfer_function.py +29 -0
- cardio/logic.py +549 -6
- cardio/mesh.py +2 -3
- cardio/object.py +9 -13
- cardio/orientation.py +215 -0
- cardio/piecewise_function.py +29 -0
- cardio/scene.py +190 -28
- cardio/segmentation.py +4 -2
- cardio/transfer_function_pair.py +25 -0
- cardio/ui.py +501 -66
- cardio/utils.py +4 -47
- cardio/volume.py +227 -5
- cardio/volume_property.py +46 -0
- cardio/volume_property_presets.py +53 -0
- cardio/window_level.py +35 -0
- {cardio-2025.8.1.dist-info → cardio-2025.10.0.dist-info}/METADATA +5 -3
- cardio-2025.10.0.dist-info/RECORD +29 -0
- cardio/transfer_functions.py +0 -272
- cardio-2025.8.1.dist-info/RECORD +0 -22
- {cardio-2025.8.1.dist-info → cardio-2025.10.0.dist-info}/WHEEL +0 -0
- {cardio-2025.8.1.dist-info → cardio-2025.10.0.dist-info}/entry_points.txt +0 -0
cardio/ui.py
CHANGED
|
@@ -1,22 +1,149 @@
|
|
|
1
|
-
|
|
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 .
|
|
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(
|
|
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=("
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
#
|
|
166
|
+
# Close button
|
|
51
167
|
vuetify.VCheckbox(
|
|
52
168
|
value=False,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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.
|
|
624
|
+
vuetify.VListSubheader("Appearance and Visibility")
|
|
197
625
|
|
|
198
626
|
if self.scene.meshes:
|
|
199
|
-
vuetify.
|
|
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.
|
|
237
|
-
with vuetify.
|
|
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.
|
|
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 =
|
|
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.
|
|
310
|
-
with vuetify.
|
|
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.
|
|
351
|
-
with vuetify.
|
|
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.
|
|
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.
|
|
435
|
-
with vuetify.
|
|
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()
|