cardio 2025.10.1__py3-none-any.whl → 2026.1.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 +1 -1
- cardio/app.py +0 -2
- cardio/blend_transfer_functions.py +0 -3
- cardio/color_transfer_function.py +0 -2
- cardio/logic.py +254 -172
- cardio/mesh.py +0 -3
- cardio/object.py +0 -2
- cardio/orientation.py +6 -3
- cardio/piecewise_function.py +0 -2
- cardio/property_config.py +0 -3
- cardio/rotation.py +211 -0
- cardio/scene.py +51 -22
- cardio/segmentation.py +1 -4
- cardio/transfer_function_pair.py +0 -2
- cardio/types.py +0 -2
- cardio/ui.py +350 -173
- cardio/utils.py +0 -7
- cardio/volume.py +174 -44
- cardio/volume_property.py +0 -2
- cardio/volume_property_presets.py +0 -3
- cardio/window_level.py +0 -2
- {cardio-2025.10.1.dist-info → cardio-2026.1.0.dist-info}/METADATA +7 -8
- cardio-2026.1.0.dist-info/RECORD +30 -0
- cardio-2025.10.1.dist-info/RECORD +0 -29
- {cardio-2025.10.1.dist-info → cardio-2026.1.0.dist-info}/WHEEL +0 -0
- {cardio-2025.10.1.dist-info → cardio-2026.1.0.dist-info}/entry_points.txt +0 -0
cardio/ui.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import functools as ft
|
|
2
|
+
import time
|
|
2
3
|
|
|
4
|
+
import numpy as np
|
|
3
5
|
from trame.ui.vuetify3 import SinglePageWithDrawerLayout
|
|
6
|
+
from trame.widgets import client
|
|
7
|
+
from trame.widgets import html
|
|
4
8
|
from trame.widgets import vtk as vtk_widgets
|
|
5
9
|
from trame.widgets import vuetify3 as vuetify
|
|
6
10
|
|
|
11
|
+
from .orientation import AngleUnits, EulerAxis, euler_angle_to_rotation_matrix
|
|
7
12
|
from .scene import Scene
|
|
8
13
|
from .volume_property_presets import list_volume_property_presets
|
|
9
14
|
from .window_level import presets
|
|
@@ -37,10 +42,46 @@ class UI:
|
|
|
37
42
|
|
|
38
43
|
match event["type"]:
|
|
39
44
|
case "KeyPress":
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
key = event["key"]
|
|
46
|
+
current_time = time.time() * 1000
|
|
47
|
+
|
|
48
|
+
if key in self.last_keypress_time:
|
|
49
|
+
time_since_last = current_time - self.last_keypress_time[key]
|
|
50
|
+
if time_since_last < self.keypress_debounce_ms:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
self.last_keypress_time[key] = current_time
|
|
54
|
+
|
|
55
|
+
if key.isdigit():
|
|
56
|
+
key_num = int(key)
|
|
42
57
|
if key_num in presets.keys():
|
|
43
58
|
self.server.state.mpr_window_level_preset = key_num
|
|
59
|
+
elif key == "l":
|
|
60
|
+
current = getattr(self.server.state, "mpr_crosshairs_enabled", True)
|
|
61
|
+
self.server.state.mpr_crosshairs_enabled = not current
|
|
62
|
+
elif key == "h":
|
|
63
|
+
current = getattr(self.server.state, "help_overlay_visible", False)
|
|
64
|
+
self.server.state.help_overlay_visible = not current
|
|
65
|
+
elif key == "v":
|
|
66
|
+
current = getattr(self.server.state, "maximized_view", "")
|
|
67
|
+
self.server.state.maximized_view = (
|
|
68
|
+
"" if current == "volume" else "volume"
|
|
69
|
+
)
|
|
70
|
+
elif key == "a":
|
|
71
|
+
current = getattr(self.server.state, "maximized_view", "")
|
|
72
|
+
self.server.state.maximized_view = (
|
|
73
|
+
"" if current == "axial" else "axial"
|
|
74
|
+
)
|
|
75
|
+
elif key == "c":
|
|
76
|
+
current = getattr(self.server.state, "maximized_view", "")
|
|
77
|
+
self.server.state.maximized_view = (
|
|
78
|
+
"" if current == "coronal" else "coronal"
|
|
79
|
+
)
|
|
80
|
+
elif key == "s":
|
|
81
|
+
current = getattr(self.server.state, "maximized_view", "")
|
|
82
|
+
self.server.state.maximized_view = (
|
|
83
|
+
"" if current == "sagittal" else "sagittal"
|
|
84
|
+
)
|
|
44
85
|
|
|
45
86
|
case "LeftButtonPress":
|
|
46
87
|
self.left_dragging = True
|
|
@@ -99,32 +140,57 @@ class UI:
|
|
|
99
140
|
event["position"]["y"],
|
|
100
141
|
]
|
|
101
142
|
|
|
102
|
-
def
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
143
|
+
def _get_scroll_vector(self, view_name):
|
|
144
|
+
"""Get the current normal vector for a view after rotation."""
|
|
145
|
+
base_normals = {
|
|
146
|
+
"axial": np.array([0.0, 0.0, 1.0]),
|
|
147
|
+
"sagittal": np.array([1.0, 0.0, 0.0]),
|
|
148
|
+
"coronal": np.array([0.0, 1.0, 0.0]),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if view_name not in base_normals:
|
|
152
|
+
return np.array([0.0, 0.0, 1.0])
|
|
153
|
+
|
|
154
|
+
# Build cumulative rotation from visible rotations
|
|
155
|
+
rotation_data = getattr(
|
|
156
|
+
self.server.state, "mpr_rotation_data", {"angles_list": []}
|
|
157
|
+
)
|
|
158
|
+
angles_list = rotation_data.get("angles_list", [])
|
|
159
|
+
cumulative_rotation = np.eye(3)
|
|
160
|
+
|
|
161
|
+
# Get current angle units
|
|
162
|
+
angle_units_str = getattr(self.server.state, "angle_units", "degrees")
|
|
163
|
+
angle_units = AngleUnits(angle_units_str)
|
|
164
|
+
|
|
165
|
+
for rotation in angles_list:
|
|
166
|
+
if rotation.get("visible", True):
|
|
167
|
+
angle = rotation.get("angles", [0])[0]
|
|
168
|
+
rotation_matrix = euler_angle_to_rotation_matrix(
|
|
169
|
+
EulerAxis(rotation["axes"]), angle, angle_units
|
|
119
170
|
)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
171
|
+
cumulative_rotation = cumulative_rotation @ rotation_matrix
|
|
172
|
+
|
|
173
|
+
return cumulative_rotation @ base_normals[view_name]
|
|
174
|
+
|
|
175
|
+
def _handle_slice_scroll(self, view_name, base_slice_delta):
|
|
176
|
+
"""Handle slice scrolling for a specific view using rotated scroll vectors."""
|
|
177
|
+
if view_name not in {"axial", "sagittal", "coronal"}:
|
|
178
|
+
return
|
|
123
179
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
180
|
+
# Get current origin
|
|
181
|
+
origin = getattr(self.server.state, "mpr_origin", [0.0, 0.0, 0.0])
|
|
182
|
+
|
|
183
|
+
# Get scroll vector for this view (considering rotation)
|
|
184
|
+
scroll_vector = self._get_scroll_vector(view_name)
|
|
185
|
+
|
|
186
|
+
# Update origin along the rotated scroll direction
|
|
187
|
+
new_origin = [
|
|
188
|
+
origin[0] + base_slice_delta * scroll_vector[0],
|
|
189
|
+
origin[1] + base_slice_delta * scroll_vector[1],
|
|
190
|
+
origin[2] + base_slice_delta * scroll_vector[2],
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
self.server.state.mpr_origin = new_origin
|
|
128
194
|
|
|
129
195
|
def __init__(self, server, scene: Scene):
|
|
130
196
|
self.server = server
|
|
@@ -135,6 +201,10 @@ class UI:
|
|
|
135
201
|
self.window_sensitivity = 5.0
|
|
136
202
|
self.level_sensitivity = 2.0
|
|
137
203
|
self.slice_sensitivity = 1.0
|
|
204
|
+
self.last_keypress_time = {}
|
|
205
|
+
self.keypress_debounce_ms = 100
|
|
206
|
+
self.server.state.help_overlay_visible = False
|
|
207
|
+
self.server.state.maximized_view = ""
|
|
138
208
|
|
|
139
209
|
self.setup()
|
|
140
210
|
|
|
@@ -181,9 +251,9 @@ class UI:
|
|
|
181
251
|
)
|
|
182
252
|
|
|
183
253
|
with layout.content:
|
|
184
|
-
# Single VR view (
|
|
254
|
+
# Single VR view (volume maximized)
|
|
185
255
|
with vuetify.VContainer(
|
|
186
|
-
v_if="
|
|
256
|
+
v_if="maximized_view === 'volume'",
|
|
187
257
|
fluid=True,
|
|
188
258
|
classes="pa-0 fill-height",
|
|
189
259
|
):
|
|
@@ -197,9 +267,9 @@ class UI:
|
|
|
197
267
|
self.server.controller.view_reset_camera = view.reset_camera
|
|
198
268
|
self.server.controller.on_server_ready.add(view.update)
|
|
199
269
|
|
|
200
|
-
# Quad-view layout (MPR mode
|
|
270
|
+
# Quad-view layout (MPR mode - default)
|
|
201
271
|
with vuetify.VContainer(
|
|
202
|
-
v_if="
|
|
272
|
+
v_if="!maximized_view",
|
|
203
273
|
fluid=True,
|
|
204
274
|
classes="pa-0",
|
|
205
275
|
style="height: calc(100vh - 85px);",
|
|
@@ -263,6 +333,10 @@ class UI:
|
|
|
263
333
|
self.server.controller.on_server_ready.add(
|
|
264
334
|
self._update_all_mpr_views
|
|
265
335
|
)
|
|
336
|
+
# Finalize MPR initialization after UI is ready to avoid race condition
|
|
337
|
+
self.server.controller.on_server_ready.add(
|
|
338
|
+
self.server.controller.finalize_mpr_initialization
|
|
339
|
+
)
|
|
266
340
|
|
|
267
341
|
# Store individual view update functions
|
|
268
342
|
self.server.controller.axial_update = axial_view.update
|
|
@@ -270,121 +344,148 @@ class UI:
|
|
|
270
344
|
self.server.controller.sagittal_update = sagittal_view.update
|
|
271
345
|
self.server.controller.volume_update = volume_view.update
|
|
272
346
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
347
|
+
# Maximized axial view
|
|
348
|
+
with vuetify.VContainer(
|
|
349
|
+
v_if="maximized_view === 'axial'",
|
|
350
|
+
fluid=True,
|
|
351
|
+
classes="pa-0 fill-height",
|
|
352
|
+
):
|
|
353
|
+
axial_maximized_view = vtk_widgets.VtkRemoteView(
|
|
354
|
+
self.scene.axial_renderWindow,
|
|
355
|
+
interactor_events=("event_types", self.handled_events),
|
|
356
|
+
**self.event_listeners_for_view("axial"),
|
|
357
|
+
interactive_ratio=1,
|
|
283
358
|
)
|
|
284
359
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
360
|
+
# Maximized coronal view
|
|
361
|
+
with vuetify.VContainer(
|
|
362
|
+
v_if="maximized_view === 'coronal'",
|
|
363
|
+
fluid=True,
|
|
364
|
+
classes="pa-0 fill-height",
|
|
365
|
+
):
|
|
366
|
+
coronal_maximized_view = vtk_widgets.VtkRemoteView(
|
|
367
|
+
self.scene.coronal_renderWindow,
|
|
368
|
+
interactor_events=("event_types", self.handled_events),
|
|
369
|
+
**self.event_listeners_for_view("coronal"),
|
|
370
|
+
interactive_ratio=1,
|
|
295
371
|
)
|
|
296
372
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
373
|
+
# Maximized sagittal view
|
|
374
|
+
with vuetify.VContainer(
|
|
375
|
+
v_if="maximized_view === 'sagittal'",
|
|
376
|
+
fluid=True,
|
|
377
|
+
classes="pa-0 fill-height",
|
|
378
|
+
):
|
|
379
|
+
sagittal_maximized_view = vtk_widgets.VtkRemoteView(
|
|
380
|
+
self.scene.sagittal_renderWindow,
|
|
381
|
+
interactor_events=("event_types", self.handled_events),
|
|
382
|
+
**self.event_listeners_for_view("sagittal"),
|
|
383
|
+
interactive_ratio=1,
|
|
300
384
|
)
|
|
301
385
|
|
|
386
|
+
with vuetify.VDialog(
|
|
387
|
+
v_model=("help_overlay_visible", False),
|
|
388
|
+
max_width="700px",
|
|
389
|
+
scrim="rgba(0, 0, 0, 0.7)",
|
|
390
|
+
):
|
|
391
|
+
with vuetify.VCard(
|
|
392
|
+
classes="pa-6",
|
|
393
|
+
style="background: rgba(33, 33, 33, 0.95);",
|
|
394
|
+
):
|
|
395
|
+
vuetify.VCardTitle(
|
|
396
|
+
"Keyboard Shortcuts & Controls", classes="text-h5 mb-4"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
with vuetify.VCardText():
|
|
400
|
+
html.H3("Keyboard Shortcuts", classes="text-h6 mb-3")
|
|
401
|
+
with vuetify.VTable(density="compact", classes="mb-4"):
|
|
402
|
+
with html.Thead():
|
|
403
|
+
with html.Tr():
|
|
404
|
+
html.Th("Key")
|
|
405
|
+
html.Th("Action")
|
|
406
|
+
with html.Tbody():
|
|
407
|
+
with html.Tr():
|
|
408
|
+
html.Td("h")
|
|
409
|
+
html.Td("Toggle this help window")
|
|
410
|
+
with html.Tr():
|
|
411
|
+
html.Td("v")
|
|
412
|
+
html.Td("Toggle 3D volume view")
|
|
413
|
+
with html.Tr():
|
|
414
|
+
html.Td("a")
|
|
415
|
+
html.Td("Toggle axial view")
|
|
416
|
+
with html.Tr():
|
|
417
|
+
html.Td("c")
|
|
418
|
+
html.Td("Toggle coronal view")
|
|
419
|
+
with html.Tr():
|
|
420
|
+
html.Td("s")
|
|
421
|
+
html.Td("Toggle sagittal view")
|
|
422
|
+
with html.Tr():
|
|
423
|
+
html.Td("l")
|
|
424
|
+
html.Td("Toggle crosshairs")
|
|
425
|
+
|
|
426
|
+
html.H3("Window/Level Presets", classes="text-h6 mb-3")
|
|
427
|
+
with vuetify.VTable(density="compact", classes="mb-4"):
|
|
428
|
+
with html.Thead():
|
|
429
|
+
with html.Tr():
|
|
430
|
+
html.Th("Key")
|
|
431
|
+
html.Th("Preset")
|
|
432
|
+
html.Th("Window")
|
|
433
|
+
html.Th("Level")
|
|
434
|
+
with html.Tbody():
|
|
435
|
+
for key, preset in presets.items():
|
|
436
|
+
with html.Tr():
|
|
437
|
+
html.Td(str(key))
|
|
438
|
+
html.Td(preset.name)
|
|
439
|
+
html.Td(str(preset.window))
|
|
440
|
+
html.Td(str(preset.level))
|
|
441
|
+
|
|
442
|
+
html.H3("Mouse Controls (MPR Mode)", classes="text-h6 mb-3")
|
|
443
|
+
with vuetify.VTable(density="compact"):
|
|
444
|
+
with html.Thead():
|
|
445
|
+
with html.Tr():
|
|
446
|
+
html.Th("Action")
|
|
447
|
+
html.Th("Effect")
|
|
448
|
+
with html.Tbody():
|
|
449
|
+
with html.Tr():
|
|
450
|
+
html.Td("Left Drag ←/→")
|
|
451
|
+
html.Td("Narrow/widen window")
|
|
452
|
+
with html.Tr():
|
|
453
|
+
html.Td("Left Drag ↑/↓")
|
|
454
|
+
html.Td("Increase/decrease level")
|
|
455
|
+
with html.Tr():
|
|
456
|
+
html.Td("Right Drag ↑/↓")
|
|
457
|
+
html.Td("Scroll through slices")
|
|
458
|
+
|
|
459
|
+
with vuetify.VCardActions():
|
|
460
|
+
vuetify.VSpacer()
|
|
461
|
+
vuetify.VBtn(
|
|
462
|
+
"Close",
|
|
463
|
+
click="help_overlay_visible = false",
|
|
464
|
+
variant="text",
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
with layout.drawer:
|
|
468
|
+
# Volume selection dropdown
|
|
469
|
+
if self.scene.volumes:
|
|
302
470
|
vuetify.VSelect(
|
|
303
|
-
v_if="
|
|
304
|
-
v_model=("
|
|
305
|
-
items=("
|
|
471
|
+
v_if="(!maximized_view || maximized_view === 'volume') && volume_items.length >= 2",
|
|
472
|
+
v_model=("active_volume_label", ""),
|
|
473
|
+
items=("volume_items", []),
|
|
306
474
|
item_title="text",
|
|
307
475
|
item_value="value",
|
|
308
|
-
|
|
476
|
+
title="Select which volume to use for MPR",
|
|
309
477
|
dense=True,
|
|
310
478
|
hide_details=True,
|
|
311
479
|
)
|
|
312
480
|
|
|
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
481
|
# MPR Rotation controls
|
|
381
482
|
vuetify.VListSubheader(
|
|
382
|
-
"Rotations", v_if="
|
|
483
|
+
"Rotations", v_if="!maximized_view && active_volume_label"
|
|
383
484
|
)
|
|
384
485
|
|
|
385
486
|
# Rotation buttons
|
|
386
487
|
with vuetify.VRow(
|
|
387
|
-
v_if="
|
|
488
|
+
v_if="!maximized_view && active_volume_label",
|
|
388
489
|
no_gutters=True,
|
|
389
490
|
classes="mb-2",
|
|
390
491
|
):
|
|
@@ -418,8 +519,8 @@ class UI:
|
|
|
418
519
|
|
|
419
520
|
# Reset rotations button
|
|
420
521
|
vuetify.VBtn(
|
|
421
|
-
"
|
|
422
|
-
v_if="
|
|
522
|
+
"Remove All Rotations",
|
|
523
|
+
v_if="!maximized_view && active_volume_label && mpr_rotation_data.angles_list && mpr_rotation_data.angles_list.length > 0",
|
|
423
524
|
click=self.server.controller.reset_rotations,
|
|
424
525
|
small=True,
|
|
425
526
|
dense=True,
|
|
@@ -430,52 +531,109 @@ class UI:
|
|
|
430
531
|
prepend_icon="mdi-refresh",
|
|
431
532
|
)
|
|
432
533
|
|
|
433
|
-
# Individual rotation sliders
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
vuetify.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
534
|
+
# Individual rotation sliders with DeepReactive
|
|
535
|
+
with client.DeepReactive("mpr_rotation_data"):
|
|
536
|
+
for i in range(self.scene.max_mpr_rotations):
|
|
537
|
+
with vuetify.VContainer(
|
|
538
|
+
v_if=f"!maximized_view && active_volume_label && mpr_rotation_data.angles_list && mpr_rotation_data.angles_list.length > {i}",
|
|
539
|
+
fluid=True,
|
|
540
|
+
classes="pa-0 mb-2",
|
|
541
|
+
):
|
|
542
|
+
with vuetify.VRow(no_gutters=True):
|
|
543
|
+
with vuetify.VCol(cols="12"):
|
|
544
|
+
vuetify.VTextField(
|
|
545
|
+
v_model=(
|
|
546
|
+
f"mpr_rotation_data.angles_list[{i}].name",
|
|
547
|
+
),
|
|
548
|
+
placeholder="Name",
|
|
549
|
+
dense=True,
|
|
550
|
+
hide_details=True,
|
|
551
|
+
readonly=(
|
|
552
|
+
f"!mpr_rotation_data.angles_list[{i}].name_editable",
|
|
553
|
+
),
|
|
554
|
+
__events=["keydown", "keyup", "keypress"],
|
|
555
|
+
keydown="$event.stopPropagation(); $event.stopImmediatePropagation();",
|
|
556
|
+
keyup="$event.stopPropagation(); $event.stopImmediatePropagation();",
|
|
557
|
+
keypress="$event.stopPropagation(); $event.stopImmediatePropagation();",
|
|
558
|
+
)
|
|
559
|
+
with vuetify.VRow(no_gutters=True):
|
|
560
|
+
with vuetify.VCol(cols="12"):
|
|
561
|
+
vuetify.VSlider(
|
|
562
|
+
v_model=(
|
|
563
|
+
f"mpr_rotation_data.angles_list[{i}].angles[0]",
|
|
564
|
+
),
|
|
565
|
+
min=(
|
|
566
|
+
"angle_units === 'radians' ? -Math.PI : -180",
|
|
567
|
+
),
|
|
568
|
+
max=(
|
|
569
|
+
"angle_units === 'radians' ? Math.PI : 180",
|
|
570
|
+
),
|
|
571
|
+
step=(
|
|
572
|
+
"angle_units === 'radians' ? 0.01 : 1",
|
|
573
|
+
),
|
|
574
|
+
dense=True,
|
|
575
|
+
hide_details=True,
|
|
576
|
+
thumb_label=True,
|
|
577
|
+
)
|
|
578
|
+
with vuetify.VRow(
|
|
579
|
+
no_gutters=True, classes="align-center"
|
|
580
|
+
):
|
|
581
|
+
vuetify.VSpacer()
|
|
582
|
+
with vuetify.VCol(cols="4"):
|
|
583
|
+
vuetify.VSelect(
|
|
584
|
+
v_model=(
|
|
585
|
+
f"mpr_rotation_data.angles_list[{i}].axes",
|
|
586
|
+
),
|
|
587
|
+
items=(["X", "Y", "Z"],),
|
|
588
|
+
dense=True,
|
|
589
|
+
hide_details=True,
|
|
590
|
+
label="Axis",
|
|
591
|
+
)
|
|
592
|
+
vuetify.VSpacer()
|
|
593
|
+
with vuetify.VCol(cols="auto"):
|
|
594
|
+
vuetify.VCheckbox(
|
|
595
|
+
v_model=(
|
|
596
|
+
f"mpr_rotation_data.angles_list[{i}].visible",
|
|
597
|
+
),
|
|
598
|
+
true_icon="mdi-eye",
|
|
599
|
+
false_icon="mdi-eye-off",
|
|
600
|
+
hide_details=True,
|
|
601
|
+
dense=True,
|
|
602
|
+
title="Toggle this rotation",
|
|
603
|
+
)
|
|
604
|
+
vuetify.VSpacer()
|
|
605
|
+
with vuetify.VCol(cols="auto"):
|
|
606
|
+
vuetify.VBtn(
|
|
607
|
+
icon="mdi-restore",
|
|
608
|
+
click=ft.partial(
|
|
609
|
+
self.server.controller.reset_rotation_angle,
|
|
610
|
+
i,
|
|
611
|
+
),
|
|
612
|
+
small=True,
|
|
613
|
+
dense=True,
|
|
614
|
+
title="Reset angle to zero",
|
|
615
|
+
)
|
|
616
|
+
vuetify.VSpacer()
|
|
617
|
+
with vuetify.VCol(cols="auto"):
|
|
618
|
+
vuetify.VBtn(
|
|
619
|
+
icon="mdi-delete",
|
|
620
|
+
click=ft.partial(
|
|
621
|
+
self.server.controller.remove_rotation_event,
|
|
622
|
+
i,
|
|
623
|
+
),
|
|
624
|
+
small=True,
|
|
625
|
+
dense=True,
|
|
626
|
+
color="error",
|
|
627
|
+
title="Remove this rotation",
|
|
628
|
+
disabled=(
|
|
629
|
+
f"!mpr_rotation_data.angles_list[{i}].deletable",
|
|
630
|
+
),
|
|
631
|
+
)
|
|
632
|
+
vuetify.VSpacer()
|
|
475
633
|
|
|
476
634
|
# Angle units selector
|
|
477
635
|
with vuetify.VRow(
|
|
478
|
-
v_if="
|
|
636
|
+
v_if="!maximized_view && active_volume_label",
|
|
479
637
|
no_gutters=True,
|
|
480
638
|
classes="align-center mb-2 mt-2",
|
|
481
639
|
):
|
|
@@ -483,7 +641,7 @@ class UI:
|
|
|
483
641
|
vuetify.VLabel("Units:")
|
|
484
642
|
with vuetify.VCol(cols="8"):
|
|
485
643
|
vuetify.VSelect(
|
|
486
|
-
v_model=("angle_units", "
|
|
644
|
+
v_model=("angle_units", "radians"),
|
|
487
645
|
items=("angle_units_items", []),
|
|
488
646
|
item_title="text",
|
|
489
647
|
item_value="value",
|
|
@@ -492,10 +650,29 @@ class UI:
|
|
|
492
650
|
outlined=True,
|
|
493
651
|
)
|
|
494
652
|
|
|
653
|
+
# Axis convention selector
|
|
654
|
+
with vuetify.VRow(
|
|
655
|
+
v_if="!maximized_view && active_volume_label",
|
|
656
|
+
no_gutters=True,
|
|
657
|
+
classes="align-center mb-2",
|
|
658
|
+
):
|
|
659
|
+
with vuetify.VCol(cols="4"):
|
|
660
|
+
vuetify.VLabel("Convention:")
|
|
661
|
+
with vuetify.VCol(cols="8"):
|
|
662
|
+
vuetify.VSelect(
|
|
663
|
+
v_model=("axis_convention", "itk"),
|
|
664
|
+
items=("axis_convention_items", []),
|
|
665
|
+
item_title="text",
|
|
666
|
+
item_value="value",
|
|
667
|
+
dense=True,
|
|
668
|
+
hide_details=True,
|
|
669
|
+
outlined=True,
|
|
670
|
+
)
|
|
671
|
+
|
|
495
672
|
# Save rotations button
|
|
496
673
|
vuetify.VBtn(
|
|
497
674
|
"Save Rotations",
|
|
498
|
-
v_if="
|
|
675
|
+
v_if="!maximized_view && active_volume_label && mpr_rotation_data.angles_list && mpr_rotation_data.angles_list.length > 0",
|
|
499
676
|
click=self.server.controller.save_rotation_angles,
|
|
500
677
|
small=True,
|
|
501
678
|
dense=True,
|
cardio/utils.py
CHANGED
|
@@ -10,13 +10,6 @@ class InterpolatorType(enum.Enum):
|
|
|
10
10
|
NEAREST = "nearest"
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class AngleUnit(enum.Enum):
|
|
14
|
-
"""Units for angle measurements."""
|
|
15
|
-
|
|
16
|
-
DEGREES = "degrees"
|
|
17
|
-
RADIANS = "radians"
|
|
18
|
-
|
|
19
|
-
|
|
20
13
|
def calculate_combined_bounds(actors):
|
|
21
14
|
"""Calculate combined bounds encompassing all VTK actors.
|
|
22
15
|
|