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/__init__.py
CHANGED
cardio/app.py
CHANGED
cardio/logic.py
CHANGED
|
@@ -8,6 +8,26 @@ from .screenshot import Screenshot
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Logic:
|
|
11
|
+
def _get_visible_rotation_data(self):
|
|
12
|
+
"""Get rotation sequence and angles for visible rotations only."""
|
|
13
|
+
rotation_data = getattr(
|
|
14
|
+
self.server.state, "mpr_rotation_data", {"angles_list": []}
|
|
15
|
+
)
|
|
16
|
+
angles_list = rotation_data.get("angles_list", [])
|
|
17
|
+
|
|
18
|
+
# Build rotation_sequence (list of {"axis": ...}) for visible rotations
|
|
19
|
+
rotation_sequence = []
|
|
20
|
+
rotation_angles = {}
|
|
21
|
+
|
|
22
|
+
visible_index = 0
|
|
23
|
+
for rotation in angles_list:
|
|
24
|
+
if rotation.get("visible", True):
|
|
25
|
+
rotation_sequence.append({"axis": rotation["axes"]})
|
|
26
|
+
rotation_angles[visible_index] = rotation["angles"][0]
|
|
27
|
+
visible_index += 1
|
|
28
|
+
|
|
29
|
+
return rotation_sequence, rotation_angles
|
|
30
|
+
|
|
11
31
|
def __init__(self, server, scene: Scene):
|
|
12
32
|
self.server = server
|
|
13
33
|
self.scene = scene
|
|
@@ -27,34 +47,31 @@ class Logic:
|
|
|
27
47
|
{"text": "Radians", "value": "radians"},
|
|
28
48
|
]
|
|
29
49
|
|
|
30
|
-
# Initialize
|
|
31
|
-
self.server.state.
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
# Initialize axis convention items for dropdown
|
|
51
|
+
self.server.state.axis_convention_items = [
|
|
52
|
+
{"text": "ITK (X=L, Y=P, Z=S)", "value": "itk"},
|
|
53
|
+
{"text": "Roma (X=S, Y=P, Z=L)", "value": "roma"},
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Initialize MPR origin (will be updated when active volume changes)
|
|
57
|
+
self.server.state.mpr_origin = [0.0, 0.0, 0.0]
|
|
58
|
+
self.server.state.mpr_crosshairs_enabled = self.scene.mpr_crosshairs_enabled
|
|
34
59
|
|
|
35
60
|
self.server.state.change("frame")(self.update_frame)
|
|
36
61
|
self.server.state.change("playing")(self.play)
|
|
37
62
|
self.server.state.change("theme_mode")(self.sync_background_color)
|
|
38
|
-
self.server.state.change("mpr_enabled")(self.sync_mpr_mode)
|
|
39
63
|
self.server.state.change("active_volume_label")(self.sync_active_volume)
|
|
40
|
-
self.server.state.change("
|
|
41
|
-
|
|
64
|
+
self.server.state.change("mpr_origin")(self.update_slice_positions)
|
|
65
|
+
self.server.state.change("mpr_crosshairs_enabled")(
|
|
66
|
+
self.sync_crosshairs_visibility
|
|
42
67
|
)
|
|
43
68
|
self.server.state.change("mpr_window", "mpr_level")(
|
|
44
69
|
self.update_mpr_window_level
|
|
45
70
|
)
|
|
46
71
|
self.server.state.change("mpr_window_level_preset")(self.update_mpr_preset)
|
|
47
|
-
self.server.state.change("
|
|
72
|
+
self.server.state.change("mpr_rotation_data")(self.update_mpr_rotation)
|
|
48
73
|
self.server.state.change("angle_units")(self.sync_angle_units)
|
|
49
|
-
|
|
50
|
-
# Add handlers for individual rotation angles and visibility
|
|
51
|
-
for i in range(self.scene.max_mpr_rotations):
|
|
52
|
-
self.server.state.change(f"mpr_rotation_angle_{i}")(
|
|
53
|
-
self.update_mpr_rotation
|
|
54
|
-
)
|
|
55
|
-
self.server.state.change(f"mpr_rotation_visible_{i}")(
|
|
56
|
-
self.update_mpr_rotation
|
|
57
|
-
)
|
|
74
|
+
self.server.state.change("axis_convention")(self.sync_axis_convention)
|
|
58
75
|
|
|
59
76
|
# Initialize visibility state variables
|
|
60
77
|
for m in self.scene.meshes:
|
|
@@ -140,25 +157,40 @@ class Logic:
|
|
|
140
157
|
self.server.controller.save_rotation_angles = self.save_rotation_angles
|
|
141
158
|
self.server.controller.reset_all = self.reset_all
|
|
142
159
|
self.server.controller.close_application = self.close_application
|
|
160
|
+
self.server.controller.finalize_mpr_initialization = (
|
|
161
|
+
self.finalize_mpr_initialization
|
|
162
|
+
)
|
|
143
163
|
|
|
144
164
|
# MPR rotation controllers
|
|
145
165
|
self.server.controller.add_x_rotation = lambda: self.add_mpr_rotation("X")
|
|
146
166
|
self.server.controller.add_y_rotation = lambda: self.add_mpr_rotation("Y")
|
|
147
167
|
self.server.controller.add_z_rotation = lambda: self.add_mpr_rotation("Z")
|
|
148
168
|
self.server.controller.remove_rotation_event = self.remove_mpr_rotation
|
|
169
|
+
self.server.controller.reset_rotation_angle = self.reset_rotation_angle
|
|
149
170
|
self.server.controller.reset_rotations = self.reset_mpr_rotations
|
|
150
171
|
|
|
151
172
|
# Initialize MPR state
|
|
152
173
|
self.server.state.mpr_enabled = self.scene.mpr_enabled
|
|
153
|
-
|
|
154
|
-
self.server.state.
|
|
155
|
-
|
|
156
|
-
self.
|
|
174
|
+
# Initialize active_volume_label to empty string (actual value set after UI ready)
|
|
175
|
+
self.server.state.active_volume_label = ""
|
|
176
|
+
# Store the actual volume label to set later, avoiding race condition
|
|
177
|
+
self._pending_active_volume = (
|
|
178
|
+
self.scene.volumes[0].label
|
|
179
|
+
if self.scene.volumes and not self.scene.active_volume_label
|
|
180
|
+
else self.scene.active_volume_label
|
|
181
|
+
)
|
|
182
|
+
self.server.state.mpr_origin = list(self.scene.mpr_origin)
|
|
157
183
|
self.server.state.mpr_window = self.scene.mpr_window
|
|
158
184
|
self.server.state.mpr_level = self.scene.mpr_level
|
|
159
185
|
self.server.state.mpr_window_level_preset = self.scene.mpr_window_level_preset
|
|
160
|
-
|
|
186
|
+
|
|
187
|
+
# Initialize rotation data from RotationSequence
|
|
188
|
+
self.server.state.mpr_rotation_data = (
|
|
189
|
+
self.scene.mpr_rotation_sequence.to_dict_for_ui()
|
|
190
|
+
)
|
|
191
|
+
|
|
161
192
|
self.server.state.angle_units = self.scene.angle_units.value
|
|
193
|
+
self.server.state.axis_convention = self.scene.axis_convention.value
|
|
162
194
|
|
|
163
195
|
# Initialize MPR presets data
|
|
164
196
|
try:
|
|
@@ -171,12 +203,6 @@ class Logic:
|
|
|
171
203
|
print(f"Error initializing MPR presets: {e}")
|
|
172
204
|
self.server.state.mpr_presets = []
|
|
173
205
|
|
|
174
|
-
# Initialize rotation angle states
|
|
175
|
-
for i in range(self.scene.max_mpr_rotations):
|
|
176
|
-
setattr(self.server.state, f"mpr_rotation_angle_{i}", 0)
|
|
177
|
-
setattr(self.server.state, f"mpr_rotation_axis_{i}", f"Rotation {i + 1}")
|
|
178
|
-
setattr(self.server.state, f"mpr_rotation_visible_{i}", True)
|
|
179
|
-
|
|
180
206
|
# Apply initial preset to ensure window/level values are set correctly
|
|
181
207
|
# Only update state values, don't call update methods yet since MPR may not be enabled
|
|
182
208
|
from .window_level import presets
|
|
@@ -236,6 +262,10 @@ class Logic:
|
|
|
236
262
|
# Get or create MPR actors for the new frame
|
|
237
263
|
mpr_actors = active_volume.get_mpr_actors_for_frame(frame)
|
|
238
264
|
|
|
265
|
+
# Get crosshair visibility state
|
|
266
|
+
crosshairs_visible = getattr(self.server.state, "mpr_crosshairs_enabled", True)
|
|
267
|
+
crosshairs = active_volume.crosshair_actors
|
|
268
|
+
|
|
239
269
|
# Update each MPR renderer with the new frame's actors
|
|
240
270
|
if self.scene.axial_renderWindow:
|
|
241
271
|
axial_renderer = (
|
|
@@ -245,8 +275,11 @@ class Logic:
|
|
|
245
275
|
axial_renderer.RemoveAllViewProps()
|
|
246
276
|
axial_renderer.AddActor(mpr_actors["axial"]["actor"])
|
|
247
277
|
mpr_actors["axial"]["actor"].SetVisibility(True)
|
|
248
|
-
#
|
|
249
|
-
|
|
278
|
+
# Re-add crosshairs
|
|
279
|
+
if crosshairs and "axial" in crosshairs:
|
|
280
|
+
for line_data in crosshairs["axial"].values():
|
|
281
|
+
axial_renderer.AddActor2D(line_data["actor"])
|
|
282
|
+
line_data["actor"].SetVisibility(crosshairs_visible)
|
|
250
283
|
axial_renderer.ResetCamera()
|
|
251
284
|
|
|
252
285
|
if self.scene.coronal_renderWindow:
|
|
@@ -257,6 +290,11 @@ class Logic:
|
|
|
257
290
|
coronal_renderer.RemoveAllViewProps()
|
|
258
291
|
coronal_renderer.AddActor(mpr_actors["coronal"]["actor"])
|
|
259
292
|
mpr_actors["coronal"]["actor"].SetVisibility(True)
|
|
293
|
+
# Re-add crosshairs
|
|
294
|
+
if crosshairs and "coronal" in crosshairs:
|
|
295
|
+
for line_data in crosshairs["coronal"].values():
|
|
296
|
+
coronal_renderer.AddActor2D(line_data["actor"])
|
|
297
|
+
line_data["actor"].SetVisibility(crosshairs_visible)
|
|
260
298
|
coronal_renderer.ResetCamera()
|
|
261
299
|
|
|
262
300
|
if self.scene.sagittal_renderWindow:
|
|
@@ -267,30 +305,28 @@ class Logic:
|
|
|
267
305
|
sagittal_renderer.RemoveAllViewProps()
|
|
268
306
|
sagittal_renderer.AddActor(mpr_actors["sagittal"]["actor"])
|
|
269
307
|
mpr_actors["sagittal"]["actor"].SetVisibility(True)
|
|
308
|
+
# Re-add crosshairs
|
|
309
|
+
if crosshairs and "sagittal" in crosshairs:
|
|
310
|
+
for line_data in crosshairs["sagittal"].values():
|
|
311
|
+
sagittal_renderer.AddActor2D(line_data["actor"])
|
|
312
|
+
line_data["actor"].SetVisibility(crosshairs_visible)
|
|
270
313
|
sagittal_renderer.ResetCamera()
|
|
271
314
|
|
|
315
|
+
# Apply current slice position and window/level to all views
|
|
316
|
+
self._apply_current_mpr_settings(active_volume, frame)
|
|
317
|
+
|
|
272
318
|
def _apply_current_mpr_settings(self, active_volume, frame):
|
|
273
319
|
"""Apply current slice positions and window/level to MPR actors."""
|
|
274
320
|
# Apply slice positions
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
coronal_slice = getattr(self.server.state, "coronal_slice", 0.5)
|
|
278
|
-
|
|
279
|
-
# Get rotation data
|
|
280
|
-
rotation_sequence = getattr(self.server.state, "mpr_rotation_sequence", [])
|
|
281
|
-
rotation_angles = {}
|
|
282
|
-
for i in range(len(rotation_sequence)):
|
|
283
|
-
rotation_angles[i] = getattr(
|
|
284
|
-
self.server.state, f"mpr_rotation_angle_{i}", 0
|
|
285
|
-
)
|
|
321
|
+
origin = getattr(self.server.state, "mpr_origin", [0.0, 0.0, 0.0])
|
|
322
|
+
rotation_sequence, rotation_angles = self._get_visible_rotation_data()
|
|
286
323
|
|
|
287
324
|
active_volume.update_slice_positions(
|
|
288
325
|
frame,
|
|
289
|
-
|
|
290
|
-
sagittal_slice,
|
|
291
|
-
coronal_slice,
|
|
326
|
+
origin,
|
|
292
327
|
rotation_sequence,
|
|
293
328
|
rotation_angles,
|
|
329
|
+
self.scene.angle_units,
|
|
294
330
|
)
|
|
295
331
|
|
|
296
332
|
# Apply window/level
|
|
@@ -462,62 +498,35 @@ class Logic:
|
|
|
462
498
|
|
|
463
499
|
@asynchronous.task
|
|
464
500
|
async def save_rotation_angles(self):
|
|
465
|
-
"""Save current rotation angles to
|
|
466
|
-
|
|
501
|
+
"""Save current rotation angles to TOML file."""
|
|
502
|
+
from .rotation import RotationSequence
|
|
467
503
|
|
|
468
|
-
# Get current timestamp
|
|
469
504
|
timestamp = dt.datetime.now()
|
|
470
505
|
timestamp_str = timestamp.strftime(self.scene.timestamp_format)
|
|
471
|
-
iso_timestamp = timestamp.isoformat()
|
|
472
|
-
|
|
473
|
-
# Get active volume label
|
|
474
506
|
active_volume_label = getattr(self.server.state, "active_volume_label", "")
|
|
507
|
+
|
|
475
508
|
if not active_volume_label:
|
|
476
|
-
print("Warning: No active volume selected
|
|
509
|
+
print("Warning: No active volume selected")
|
|
477
510
|
return
|
|
478
511
|
|
|
479
|
-
# Create directory structure
|
|
480
512
|
save_dir = self.scene.rotations_directory / active_volume_label
|
|
481
513
|
save_dir.mkdir(parents=True, exist_ok=True)
|
|
482
514
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
metadata = tk.table()
|
|
488
|
-
metadata["coordinate_system"] = self.scene.coordinate_system
|
|
489
|
-
metadata["units"] = self.scene.angle_units.value
|
|
490
|
-
metadata["timestamp"] = iso_timestamp
|
|
491
|
-
metadata["volume_label"] = active_volume_label
|
|
492
|
-
doc["metadata"] = metadata
|
|
493
|
-
|
|
494
|
-
# Slice positions section
|
|
495
|
-
slice_positions = tk.table()
|
|
496
|
-
slice_positions["axial"] = getattr(self.server.state, "axial_slice", 0.5)
|
|
497
|
-
slice_positions["sagittal"] = getattr(self.server.state, "sagittal_slice", 0.5)
|
|
498
|
-
slice_positions["coronal"] = getattr(self.server.state, "coronal_slice", 0.5)
|
|
499
|
-
doc["slice_positions"] = slice_positions
|
|
500
|
-
|
|
501
|
-
# Rotations section (array of tables)
|
|
502
|
-
rotation_sequence = getattr(self.server.state, "mpr_rotation_sequence", [])
|
|
503
|
-
rotations_array = tk.aot()
|
|
504
|
-
|
|
505
|
-
for i, rotation_def in enumerate(rotation_sequence):
|
|
506
|
-
rotation = tk.table()
|
|
507
|
-
rotation["axis"] = rotation_def["axis"]
|
|
508
|
-
angle = getattr(self.server.state, f"mpr_rotation_angle_{i}", 0)
|
|
509
|
-
rotation["angle"] = float(angle)
|
|
510
|
-
rotation["visible"] = getattr(
|
|
511
|
-
self.server.state, f"mpr_rotation_visible_{i}", True
|
|
512
|
-
)
|
|
513
|
-
rotations_array.append(rotation)
|
|
515
|
+
rotation_data = getattr(
|
|
516
|
+
self.server.state, "mpr_rotation_data", {"angles_list": []}
|
|
517
|
+
)
|
|
518
|
+
rotation_seq = RotationSequence.from_ui_dict(rotation_data, active_volume_label)
|
|
514
519
|
|
|
515
|
-
|
|
520
|
+
rotation_seq.metadata.timestamp = timestamp.isoformat()
|
|
521
|
+
rotation_seq.metadata.volume_label = active_volume_label
|
|
522
|
+
rotation_seq.metadata.coordinate_system = self.scene.coordinate_system
|
|
516
523
|
|
|
517
|
-
# Save to file
|
|
518
524
|
output_path = save_dir / f"{timestamp_str}.toml"
|
|
519
|
-
|
|
520
|
-
|
|
525
|
+
rotation_seq.to_file(
|
|
526
|
+
output_path,
|
|
527
|
+
target_convention=self.scene.axis_convention,
|
|
528
|
+
target_units=self.scene.angle_units,
|
|
529
|
+
)
|
|
521
530
|
|
|
522
531
|
def reset_all(self):
|
|
523
532
|
self.server.state.frame = 0
|
|
@@ -544,13 +553,55 @@ class Logic:
|
|
|
544
553
|
|
|
545
554
|
def sync_angle_units(self, angle_units, **kwargs):
|
|
546
555
|
"""Sync angle units selection - updates the scene configuration."""
|
|
547
|
-
|
|
556
|
+
import copy
|
|
557
|
+
|
|
558
|
+
import numpy as np
|
|
559
|
+
|
|
560
|
+
from .orientation import AngleUnits
|
|
561
|
+
|
|
562
|
+
# Get current units before changing
|
|
563
|
+
old_units = self.scene.angle_units
|
|
548
564
|
|
|
549
565
|
# Update the scene's angle_units field based on UI selection
|
|
566
|
+
new_units = None
|
|
550
567
|
if angle_units == "degrees":
|
|
551
|
-
|
|
568
|
+
new_units = AngleUnits.DEGREES
|
|
552
569
|
elif angle_units == "radians":
|
|
553
|
-
|
|
570
|
+
new_units = AngleUnits.RADIANS
|
|
571
|
+
|
|
572
|
+
if new_units is None or old_units == new_units:
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
# Convert all existing rotation angles
|
|
576
|
+
rotation_data = getattr(
|
|
577
|
+
self.server.state, "mpr_rotation_data", {"angles_list": []}
|
|
578
|
+
)
|
|
579
|
+
if rotation_data.get("angles_list"):
|
|
580
|
+
updated_data = copy.deepcopy(rotation_data)
|
|
581
|
+
|
|
582
|
+
for rotation in updated_data["angles_list"]:
|
|
583
|
+
current_angle = rotation.get("angles", [0])[0]
|
|
584
|
+
|
|
585
|
+
# Convert based on old -> new units
|
|
586
|
+
if old_units == AngleUnits.DEGREES and new_units == AngleUnits.RADIANS:
|
|
587
|
+
rotation["angles"][0] = np.radians(current_angle)
|
|
588
|
+
elif (
|
|
589
|
+
old_units == AngleUnits.RADIANS and new_units == AngleUnits.DEGREES
|
|
590
|
+
):
|
|
591
|
+
rotation["angles"][0] = np.degrees(current_angle)
|
|
592
|
+
|
|
593
|
+
self.server.state.mpr_rotation_data = updated_data
|
|
594
|
+
|
|
595
|
+
self.scene.angle_units = new_units
|
|
596
|
+
|
|
597
|
+
def sync_axis_convention(self, axis_convention, **kwargs):
|
|
598
|
+
"""Sync axis convention selection - updates the scene configuration."""
|
|
599
|
+
from .orientation import AxisConvention
|
|
600
|
+
|
|
601
|
+
if axis_convention == "itk":
|
|
602
|
+
self.scene.axis_convention = AxisConvention.ITK
|
|
603
|
+
elif axis_convention == "roma":
|
|
604
|
+
self.scene.axis_convention = AxisConvention.ROMA
|
|
554
605
|
|
|
555
606
|
def _initialize_clipping_state(self):
|
|
556
607
|
"""Initialize clipping state variables for all objects."""
|
|
@@ -633,32 +684,32 @@ class Logic:
|
|
|
633
684
|
if not active_volume:
|
|
634
685
|
return
|
|
635
686
|
|
|
636
|
-
#
|
|
687
|
+
# Initialize origin to volume center (in LPS coordinates)
|
|
637
688
|
try:
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
#
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
self.server.state.sagittal_slice = (
|
|
648
|
-
bounds[0] + bounds[1]
|
|
649
|
-
) / 2 # X center
|
|
650
|
-
if self.server.state.coronal_slice == 0.0:
|
|
651
|
-
self.server.state.coronal_slice = (
|
|
652
|
-
bounds[2] + bounds[3]
|
|
653
|
-
) / 2 # Y center
|
|
689
|
+
current_frame = getattr(self.server.state, "frame", 0)
|
|
690
|
+
volume_actor = active_volume.actors[current_frame]
|
|
691
|
+
image_data = volume_actor.GetMapper().GetInput()
|
|
692
|
+
center = image_data.GetCenter()
|
|
693
|
+
|
|
694
|
+
# Set origin to volume center if it's at default [0,0,0]
|
|
695
|
+
current_origin = getattr(self.server.state, "mpr_origin", [0.0, 0.0, 0.0])
|
|
696
|
+
if current_origin == [0.0, 0.0, 0.0]:
|
|
697
|
+
self.server.state.mpr_origin = list(center)
|
|
654
698
|
except (RuntimeError, IndexError) as e:
|
|
655
|
-
print(f"Error: Cannot get
|
|
699
|
+
print(f"Error: Cannot get center for volume '{active_volume_label}': {e}")
|
|
656
700
|
return
|
|
657
701
|
|
|
658
702
|
# Create MPR actors for current frame
|
|
659
703
|
current_frame = getattr(self.server.state, "frame", 0)
|
|
660
704
|
mpr_actors = active_volume.get_mpr_actors_for_frame(current_frame)
|
|
661
705
|
|
|
706
|
+
# Create crosshair actors
|
|
707
|
+
crosshairs = active_volume.create_crosshair_actors(
|
|
708
|
+
colors=self.scene.mpr_crosshair_colors,
|
|
709
|
+
line_width=self.scene.mpr_crosshair_width,
|
|
710
|
+
)
|
|
711
|
+
crosshairs_visible = getattr(self.server.state, "mpr_crosshairs_enabled", True)
|
|
712
|
+
|
|
662
713
|
# Add MPR actors to their respective renderers
|
|
663
714
|
if self.scene.axial_renderWindow:
|
|
664
715
|
axial_renderer = (
|
|
@@ -668,6 +719,10 @@ class Logic:
|
|
|
668
719
|
axial_renderer.RemoveAllViewProps() # Clear existing actors
|
|
669
720
|
axial_renderer.AddActor(mpr_actors["axial"]["actor"])
|
|
670
721
|
mpr_actors["axial"]["actor"].SetVisibility(True)
|
|
722
|
+
# Add 2D crosshair overlay actors
|
|
723
|
+
for line_data in crosshairs["axial"].values():
|
|
724
|
+
axial_renderer.AddActor2D(line_data["actor"])
|
|
725
|
+
line_data["actor"].SetVisibility(crosshairs_visible)
|
|
671
726
|
axial_renderer.ResetCamera()
|
|
672
727
|
|
|
673
728
|
if self.scene.coronal_renderWindow:
|
|
@@ -678,6 +733,10 @@ class Logic:
|
|
|
678
733
|
coronal_renderer.RemoveAllViewProps()
|
|
679
734
|
coronal_renderer.AddActor(mpr_actors["coronal"]["actor"])
|
|
680
735
|
mpr_actors["coronal"]["actor"].SetVisibility(True)
|
|
736
|
+
# Add 2D crosshair overlay actors
|
|
737
|
+
for line_data in crosshairs["coronal"].values():
|
|
738
|
+
coronal_renderer.AddActor2D(line_data["actor"])
|
|
739
|
+
line_data["actor"].SetVisibility(crosshairs_visible)
|
|
681
740
|
coronal_renderer.ResetCamera()
|
|
682
741
|
|
|
683
742
|
if self.scene.sagittal_renderWindow:
|
|
@@ -688,6 +747,10 @@ class Logic:
|
|
|
688
747
|
sagittal_renderer.RemoveAllViewProps()
|
|
689
748
|
sagittal_renderer.AddActor(mpr_actors["sagittal"]["actor"])
|
|
690
749
|
mpr_actors["sagittal"]["actor"].SetVisibility(True)
|
|
750
|
+
# Add 2D crosshair overlay actors
|
|
751
|
+
for line_data in crosshairs["sagittal"].values():
|
|
752
|
+
sagittal_renderer.AddActor2D(line_data["actor"])
|
|
753
|
+
line_data["actor"].SetVisibility(crosshairs_visible)
|
|
691
754
|
sagittal_renderer.ResetCamera()
|
|
692
755
|
|
|
693
756
|
# Apply current window/level settings to the MPR actors
|
|
@@ -718,28 +781,19 @@ class Logic:
|
|
|
718
781
|
if not active_volume:
|
|
719
782
|
return
|
|
720
783
|
|
|
721
|
-
# Get current
|
|
722
|
-
|
|
723
|
-
sagittal_slice = getattr(self.server.state, "sagittal_slice", 0.5)
|
|
724
|
-
coronal_slice = getattr(self.server.state, "coronal_slice", 0.5)
|
|
784
|
+
# Get current origin and frame
|
|
785
|
+
origin = getattr(self.server.state, "mpr_origin", [0.0, 0.0, 0.0])
|
|
725
786
|
current_frame = getattr(self.server.state, "frame", 0)
|
|
726
787
|
|
|
727
|
-
|
|
728
|
-
rotation_sequence = getattr(self.server.state, "mpr_rotation_sequence", [])
|
|
729
|
-
rotation_angles = {}
|
|
730
|
-
for i in range(len(rotation_sequence)):
|
|
731
|
-
rotation_angles[i] = getattr(
|
|
732
|
-
self.server.state, f"mpr_rotation_angle_{i}", 0
|
|
733
|
-
)
|
|
788
|
+
rotation_sequence, rotation_angles = self._get_visible_rotation_data()
|
|
734
789
|
|
|
735
790
|
# Update slice positions with rotation
|
|
736
791
|
active_volume.update_slice_positions(
|
|
737
792
|
current_frame,
|
|
738
|
-
|
|
739
|
-
sagittal_slice,
|
|
740
|
-
coronal_slice,
|
|
793
|
+
origin,
|
|
741
794
|
rotation_sequence,
|
|
742
795
|
rotation_angles,
|
|
796
|
+
self.scene.angle_units,
|
|
743
797
|
)
|
|
744
798
|
|
|
745
799
|
# Update all views
|
|
@@ -826,81 +880,109 @@ class Logic:
|
|
|
826
880
|
if not active_volume:
|
|
827
881
|
return
|
|
828
882
|
|
|
829
|
-
# Get current
|
|
830
|
-
|
|
831
|
-
sagittal_slice = getattr(self.server.state, "sagittal_slice", 0.5)
|
|
832
|
-
coronal_slice = getattr(self.server.state, "coronal_slice", 0.5)
|
|
883
|
+
# Get current origin and frame
|
|
884
|
+
origin = getattr(self.server.state, "mpr_origin", [0.0, 0.0, 0.0])
|
|
833
885
|
current_frame = getattr(self.server.state, "frame", 0)
|
|
834
886
|
|
|
835
|
-
|
|
836
|
-
rotation_sequence = getattr(self.server.state, "mpr_rotation_sequence", [])
|
|
837
|
-
rotation_angles = {}
|
|
838
|
-
|
|
839
|
-
# Include all visible rotations regardless of position
|
|
840
|
-
for i in range(len(rotation_sequence)):
|
|
841
|
-
is_visible = getattr(self.server.state, f"mpr_rotation_visible_{i}", True)
|
|
842
|
-
if is_visible:
|
|
843
|
-
rotation_angles[i] = getattr(
|
|
844
|
-
self.server.state, f"mpr_rotation_angle_{i}", 0
|
|
845
|
-
)
|
|
887
|
+
rotation_sequence, rotation_angles = self._get_visible_rotation_data()
|
|
846
888
|
|
|
847
889
|
# Update slice positions with rotation
|
|
848
890
|
active_volume.update_slice_positions(
|
|
849
891
|
current_frame,
|
|
850
|
-
|
|
851
|
-
sagittal_slice,
|
|
852
|
-
coronal_slice,
|
|
892
|
+
origin,
|
|
853
893
|
rotation_sequence,
|
|
854
894
|
rotation_angles,
|
|
895
|
+
self.scene.angle_units,
|
|
855
896
|
)
|
|
856
897
|
|
|
857
898
|
# Update all views
|
|
858
899
|
self.server.controller.view_update()
|
|
859
900
|
|
|
901
|
+
def sync_crosshairs_visibility(self, **kwargs):
|
|
902
|
+
"""Toggle crosshair visibility on all MPR views."""
|
|
903
|
+
if not getattr(self.server.state, "mpr_enabled", False):
|
|
904
|
+
return
|
|
905
|
+
|
|
906
|
+
active_volume_label = getattr(self.server.state, "active_volume_label", "")
|
|
907
|
+
if not active_volume_label:
|
|
908
|
+
return
|
|
909
|
+
|
|
910
|
+
# Find the active volume
|
|
911
|
+
active_volume = None
|
|
912
|
+
for volume in self.scene.volumes:
|
|
913
|
+
if volume.label == active_volume_label:
|
|
914
|
+
active_volume = volume
|
|
915
|
+
break
|
|
916
|
+
|
|
917
|
+
if not active_volume:
|
|
918
|
+
return
|
|
919
|
+
|
|
920
|
+
visible = getattr(self.server.state, "mpr_crosshairs_enabled", True)
|
|
921
|
+
active_volume.set_crosshairs_visible(visible)
|
|
922
|
+
self.server.controller.view_update()
|
|
923
|
+
|
|
860
924
|
def add_mpr_rotation(self, axis):
|
|
861
925
|
"""Add a new rotation to the MPR rotation sequence."""
|
|
862
926
|
import copy
|
|
863
927
|
|
|
864
|
-
|
|
865
|
-
getattr(self.server.state, "
|
|
928
|
+
current_data = copy.deepcopy(
|
|
929
|
+
getattr(self.server.state, "mpr_rotation_data", {"angles_list": []})
|
|
930
|
+
)
|
|
931
|
+
angles_list = current_data["angles_list"]
|
|
932
|
+
new_index = len(angles_list)
|
|
933
|
+
|
|
934
|
+
angles_list.append(
|
|
935
|
+
{
|
|
936
|
+
"axes": axis,
|
|
937
|
+
"angles": [0],
|
|
938
|
+
"visible": True,
|
|
939
|
+
"name": "",
|
|
940
|
+
"name_editable": True,
|
|
941
|
+
"deletable": True,
|
|
942
|
+
}
|
|
866
943
|
)
|
|
867
|
-
|
|
868
|
-
self.server.state.
|
|
869
|
-
self.update_mpr_rotation_labels()
|
|
944
|
+
|
|
945
|
+
self.server.state.mpr_rotation_data = current_data
|
|
870
946
|
|
|
871
947
|
def remove_mpr_rotation(self, index):
|
|
872
|
-
"""Remove a rotation at given index
|
|
873
|
-
|
|
874
|
-
if 0 <= index < len(sequence):
|
|
875
|
-
sequence = sequence[:index]
|
|
876
|
-
self.server.state.mpr_rotation_sequence = sequence
|
|
948
|
+
"""Remove a rotation at given index."""
|
|
949
|
+
import copy
|
|
877
950
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
951
|
+
current_data = copy.deepcopy(
|
|
952
|
+
getattr(self.server.state, "mpr_rotation_data", {"angles_list": []})
|
|
953
|
+
)
|
|
954
|
+
angles_list = current_data["angles_list"]
|
|
881
955
|
|
|
882
|
-
|
|
956
|
+
if 0 <= index < len(angles_list):
|
|
957
|
+
angles_list.pop(index)
|
|
958
|
+
current_data["angles_list"] = angles_list
|
|
959
|
+
self.server.state.mpr_rotation_data = current_data
|
|
960
|
+
|
|
961
|
+
def reset_rotation_angle(self, index):
|
|
962
|
+
"""Reset the angle of a rotation at given index to zero."""
|
|
963
|
+
import copy
|
|
964
|
+
|
|
965
|
+
current_data = copy.deepcopy(
|
|
966
|
+
getattr(self.server.state, "mpr_rotation_data", {"angles_list": []})
|
|
967
|
+
)
|
|
968
|
+
angles_list = current_data["angles_list"]
|
|
969
|
+
|
|
970
|
+
if 0 <= index < len(angles_list):
|
|
971
|
+
angles_list[index]["angles"][0] = 0
|
|
972
|
+
current_data["angles_list"] = angles_list
|
|
973
|
+
self.server.state.mpr_rotation_data = current_data
|
|
883
974
|
|
|
884
975
|
def reset_mpr_rotations(self):
|
|
885
976
|
"""Reset all MPR rotations."""
|
|
886
|
-
self.server.state.
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
self.
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
for i, rotation in enumerate(rotation_sequence):
|
|
896
|
-
setattr(
|
|
897
|
-
self.server.state,
|
|
898
|
-
f"mpr_rotation_axis_{i}",
|
|
899
|
-
f"{rotation['axis']} ({i + 1})",
|
|
900
|
-
)
|
|
901
|
-
# Clear unused labels
|
|
902
|
-
for i in range(len(rotation_sequence), self.scene.max_mpr_rotations):
|
|
903
|
-
setattr(self.server.state, f"mpr_rotation_axis_{i}", f"Rotation {i + 1}")
|
|
977
|
+
self.server.state.mpr_rotation_data = {"angles_list": []}
|
|
978
|
+
|
|
979
|
+
def finalize_mpr_initialization(self, **kwargs):
|
|
980
|
+
"""Set the active volume label after UI is ready to avoid race condition."""
|
|
981
|
+
if hasattr(self, "_pending_active_volume") and self._pending_active_volume:
|
|
982
|
+
self.server.state.active_volume_label = self._pending_active_volume
|
|
983
|
+
# Manually trigger sync_active_volume since state change may not fire during on_server_ready
|
|
984
|
+
self.sync_active_volume(self._pending_active_volume)
|
|
985
|
+
delattr(self, "_pending_active_volume")
|
|
904
986
|
|
|
905
987
|
@asynchronous.task
|
|
906
988
|
async def close_application(self):
|