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/logic.py CHANGED
@@ -12,9 +12,49 @@ class Logic:
12
12
  self.server = server
13
13
  self.scene = scene
14
14
 
15
+ # Initialize mpr_presets early to avoid undefined errors
16
+ self.server.state.mpr_presets = []
17
+
18
+ # Initialize volume items for MPR dropdown
19
+ self.server.state.volume_items = [
20
+ {"text": volume.label, "value": volume.label}
21
+ for volume in self.scene.volumes
22
+ ]
23
+
24
+ # Initialize angle units items for dropdown
25
+ self.server.state.angle_units_items = [
26
+ {"text": "Degrees", "value": "degrees"},
27
+ {"text": "Radians", "value": "radians"},
28
+ ]
29
+
30
+ # Initialize slice bounds (will be updated when active volume changes)
31
+ self.server.state.axial_slice_bounds = [0.0, 100.0]
32
+ self.server.state.sagittal_slice_bounds = [0.0, 100.0]
33
+ self.server.state.coronal_slice_bounds = [0.0, 100.0]
34
+
15
35
  self.server.state.change("frame")(self.update_frame)
16
36
  self.server.state.change("playing")(self.play)
17
- self.server.state.change("dark_mode")(self.sync_background_color)
37
+ self.server.state.change("theme_mode")(self.sync_background_color)
38
+ self.server.state.change("mpr_enabled")(self.sync_mpr_mode)
39
+ self.server.state.change("active_volume_label")(self.sync_active_volume)
40
+ self.server.state.change("axial_slice", "sagittal_slice", "coronal_slice")(
41
+ self.update_slice_positions
42
+ )
43
+ self.server.state.change("mpr_window", "mpr_level")(
44
+ self.update_mpr_window_level
45
+ )
46
+ self.server.state.change("mpr_window_level_preset")(self.update_mpr_preset)
47
+ self.server.state.change("mpr_rotation_sequence")(self.update_mpr_rotation)
48
+ 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
+ )
18
58
 
19
59
  # Initialize visibility state variables
20
60
  for m in self.scene.meshes:
@@ -97,7 +137,54 @@ class Logic:
97
137
  self.server.controller.increment_frame = self.increment_frame
98
138
  self.server.controller.decrement_frame = self.decrement_frame
99
139
  self.server.controller.screenshot = self.screenshot
140
+ self.server.controller.save_rotation_angles = self.save_rotation_angles
100
141
  self.server.controller.reset_all = self.reset_all
142
+ self.server.controller.close_application = self.close_application
143
+
144
+ # MPR rotation controllers
145
+ self.server.controller.add_x_rotation = lambda: self.add_mpr_rotation("X")
146
+ self.server.controller.add_y_rotation = lambda: self.add_mpr_rotation("Y")
147
+ self.server.controller.add_z_rotation = lambda: self.add_mpr_rotation("Z")
148
+ self.server.controller.remove_rotation_event = self.remove_mpr_rotation
149
+ self.server.controller.reset_rotations = self.reset_mpr_rotations
150
+
151
+ # Initialize MPR state
152
+ self.server.state.mpr_enabled = self.scene.mpr_enabled
153
+ self.server.state.active_volume_label = self.scene.active_volume_label
154
+ self.server.state.axial_slice = self.scene.axial_slice
155
+ self.server.state.sagittal_slice = self.scene.sagittal_slice
156
+ self.server.state.coronal_slice = self.scene.coronal_slice
157
+ self.server.state.mpr_window = self.scene.mpr_window
158
+ self.server.state.mpr_level = self.scene.mpr_level
159
+ self.server.state.mpr_window_level_preset = self.scene.mpr_window_level_preset
160
+ self.server.state.mpr_rotation_sequence = self.scene.mpr_rotation_sequence
161
+ self.server.state.angle_units = self.scene.angle_units.value
162
+
163
+ # Initialize MPR presets data
164
+ try:
165
+ from .window_level import presets
166
+
167
+ self.server.state.mpr_presets = [
168
+ {"text": "Select W/L...", "value": None}
169
+ ] + [{"text": preset.name, "value": key} for key, preset in presets.items()]
170
+ except Exception as e:
171
+ print(f"Error initializing MPR presets: {e}")
172
+ self.server.state.mpr_presets = []
173
+
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
+ # Apply initial preset to ensure window/level values are set correctly
181
+ # Only update state values, don't call update methods yet since MPR may not be enabled
182
+ from .window_level import presets
183
+
184
+ if self.scene.mpr_window_level_preset in presets:
185
+ preset = presets[self.scene.mpr_window_level_preset]
186
+ self.server.state.mpr_window = preset.window
187
+ self.server.state.mpr_level = preset.level
101
188
 
102
189
  # Initialize clipping state variables
103
190
  self._initialize_clipping_state()
@@ -122,8 +209,95 @@ class Logic:
122
209
  actor = segmentation.actors[frame % len(segmentation.actors)]
123
210
  actor.SetVisibility(True)
124
211
 
212
+ # Update MPR views if MPR is enabled
213
+ self.update_mpr_frame(frame)
214
+
125
215
  self.server.controller.view_update()
126
216
 
217
+ def update_mpr_frame(self, frame):
218
+ """Update MPR views to show the specified frame."""
219
+ if not getattr(self.server.state, "mpr_enabled", False):
220
+ return
221
+
222
+ active_volume_label = getattr(self.server.state, "active_volume_label", "")
223
+ if not active_volume_label:
224
+ return
225
+
226
+ # Find the active volume
227
+ active_volume = None
228
+ for volume in self.scene.volumes:
229
+ if volume.label == active_volume_label:
230
+ active_volume = volume
231
+ break
232
+
233
+ if not active_volume:
234
+ return
235
+
236
+ # Get or create MPR actors for the new frame
237
+ mpr_actors = active_volume.get_mpr_actors_for_frame(frame)
238
+
239
+ # Update each MPR renderer with the new frame's actors
240
+ if self.scene.axial_renderWindow:
241
+ axial_renderer = (
242
+ self.scene.axial_renderWindow.GetRenderers().GetFirstRenderer()
243
+ )
244
+ if axial_renderer:
245
+ axial_renderer.RemoveAllViewProps()
246
+ axial_renderer.AddActor(mpr_actors["axial"]["actor"])
247
+ mpr_actors["axial"]["actor"].SetVisibility(True)
248
+ # Apply current slice position and window/level
249
+ self._apply_current_mpr_settings(active_volume, frame)
250
+ axial_renderer.ResetCamera()
251
+
252
+ if self.scene.coronal_renderWindow:
253
+ coronal_renderer = (
254
+ self.scene.coronal_renderWindow.GetRenderers().GetFirstRenderer()
255
+ )
256
+ if coronal_renderer:
257
+ coronal_renderer.RemoveAllViewProps()
258
+ coronal_renderer.AddActor(mpr_actors["coronal"]["actor"])
259
+ mpr_actors["coronal"]["actor"].SetVisibility(True)
260
+ coronal_renderer.ResetCamera()
261
+
262
+ if self.scene.sagittal_renderWindow:
263
+ sagittal_renderer = (
264
+ self.scene.sagittal_renderWindow.GetRenderers().GetFirstRenderer()
265
+ )
266
+ if sagittal_renderer:
267
+ sagittal_renderer.RemoveAllViewProps()
268
+ sagittal_renderer.AddActor(mpr_actors["sagittal"]["actor"])
269
+ mpr_actors["sagittal"]["actor"].SetVisibility(True)
270
+ sagittal_renderer.ResetCamera()
271
+
272
+ def _apply_current_mpr_settings(self, active_volume, frame):
273
+ """Apply current slice positions and window/level to MPR actors."""
274
+ # Apply slice positions
275
+ axial_slice = getattr(self.server.state, "axial_slice", 0.5)
276
+ sagittal_slice = getattr(self.server.state, "sagittal_slice", 0.5)
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
+ )
286
+
287
+ active_volume.update_slice_positions(
288
+ frame,
289
+ axial_slice,
290
+ sagittal_slice,
291
+ coronal_slice,
292
+ rotation_sequence,
293
+ rotation_angles,
294
+ )
295
+
296
+ # Apply window/level
297
+ window = getattr(self.server.state, "mpr_window", 400.0)
298
+ level = getattr(self.server.state, "mpr_level", 40.0)
299
+ active_volume.update_mpr_window_level(frame, window, level)
300
+
127
301
  @asynchronous.task
128
302
  async def play(self, playing, **kwargs):
129
303
  if not (self.server.state.incrementing or self.server.state.rotating):
@@ -159,11 +333,11 @@ class Logic:
159
333
 
160
334
  def sync_volume_presets(self, **kwargs):
161
335
  """Update volume transfer function presets based on UI selection."""
162
- from .transfer_functions import load_preset
336
+ from .volume_property_presets import load_volume_property_preset
163
337
 
164
338
  for v in self.scene.volumes:
165
339
  preset_name = self.server.state[f"volume_preset_{v.label}"]
166
- preset = load_preset(preset_name)
340
+ preset = load_volume_property_preset(preset_name)
167
341
 
168
342
  # Apply preset to all actors
169
343
  for actor in v.actors:
@@ -261,7 +435,7 @@ class Logic:
261
435
 
262
436
  @asynchronous.task
263
437
  async def screenshot(self):
264
- dr = dt.datetime.now().strftime(self.scene.screenshot_subdirectory_format)
438
+ dr = dt.datetime.now().strftime(self.scene.timestamp_format)
265
439
  dr = self.scene.screenshot_directory / dr
266
440
  dr.mkdir(parents=True, exist_ok=True)
267
441
 
@@ -286,6 +460,65 @@ class Logic:
286
460
  1 / self.server.state.bpm * 60 / self.scene.nframes
287
461
  )
288
462
 
463
+ @asynchronous.task
464
+ async def save_rotation_angles(self):
465
+ """Save current rotation angles to a TOML file."""
466
+ import tomlkit as tk
467
+
468
+ # Get current timestamp
469
+ timestamp = dt.datetime.now()
470
+ timestamp_str = timestamp.strftime(self.scene.timestamp_format)
471
+ iso_timestamp = timestamp.isoformat()
472
+
473
+ # Get active volume label
474
+ active_volume_label = getattr(self.server.state, "active_volume_label", "")
475
+ if not active_volume_label:
476
+ print("Warning: No active volume selected for rotation saving")
477
+ return
478
+
479
+ # Create directory structure
480
+ save_dir = self.scene.rotations_directory / active_volume_label
481
+ save_dir.mkdir(parents=True, exist_ok=True)
482
+
483
+ # Create TOML structure
484
+ doc = tk.document()
485
+
486
+ # Metadata section
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)
514
+
515
+ doc["rotations"] = rotations_array
516
+
517
+ # Save to file
518
+ output_path = save_dir / f"{timestamp_str}.toml"
519
+ with open(output_path, "w") as f:
520
+ f.write(tk.dumps(doc))
521
+
289
522
  def reset_all(self):
290
523
  self.server.state.frame = 0
291
524
  self.server.state.playing = False
@@ -295,9 +528,9 @@ class Logic:
295
528
  self.server.state.bpr = 5
296
529
  self.server.controller.view_update()
297
530
 
298
- def sync_background_color(self, dark_mode, **kwargs):
531
+ def sync_background_color(self, theme_mode, **kwargs):
299
532
  """Sync VTK renderer background with dark mode."""
300
- if dark_mode:
533
+ if theme_mode == "dark":
301
534
  # Dark mode: use dark background from config
302
535
  self.scene.renderer.SetBackground(
303
536
  *self.scene.background.dark,
@@ -309,6 +542,16 @@ class Logic:
309
542
  )
310
543
  self.server.controller.view_update()
311
544
 
545
+ def sync_angle_units(self, angle_units, **kwargs):
546
+ """Sync angle units selection - updates the scene configuration."""
547
+ from .utils import AngleUnit
548
+
549
+ # Update the scene's angle_units field based on UI selection
550
+ if angle_units == "degrees":
551
+ self.scene.angle_units = AngleUnit.DEGREES
552
+ elif angle_units == "radians":
553
+ self.scene.angle_units = AngleUnit.RADIANS
554
+
312
555
  def _initialize_clipping_state(self):
313
556
  """Initialize clipping state variables for all objects."""
314
557
  # Initialize mesh clipping state
@@ -363,3 +606,303 @@ class Logic:
363
606
  setattr(self.server.state, f"clip_x_{s.label}", [bounds[0], bounds[1]])
364
607
  setattr(self.server.state, f"clip_y_{s.label}", [bounds[2], bounds[3]])
365
608
  setattr(self.server.state, f"clip_z_{s.label}", [bounds[4], bounds[5]])
609
+
610
+ def sync_mpr_mode(self, mpr_enabled, **kwargs):
611
+ """Handle MPR mode toggle."""
612
+ if (
613
+ mpr_enabled
614
+ and self.scene.volumes
615
+ and not self.server.state.active_volume_label
616
+ ):
617
+ # Auto-select first volume when MPR is enabled and no volume is selected
618
+ self.server.state.active_volume_label = self.scene.volumes[0].label
619
+
620
+ def sync_active_volume(self, active_volume_label, **kwargs):
621
+ """Handle active volume selection for MPR."""
622
+
623
+ if not active_volume_label or not self.server.state.mpr_enabled:
624
+ return
625
+
626
+ # Find the selected volume
627
+ active_volume = None
628
+ for volume in self.scene.volumes:
629
+ if volume.label == active_volume_label:
630
+ active_volume = volume
631
+ break
632
+
633
+ if not active_volume:
634
+ return
635
+
636
+ # Update slice bounds based on active volume
637
+ try:
638
+ bounds = active_volume.get_physical_bounds()
639
+ self.server.state.axial_slice_bounds = [bounds[4], bounds[5]] # Z bounds
640
+ self.server.state.sagittal_slice_bounds = [bounds[0], bounds[1]] # X bounds
641
+ self.server.state.coronal_slice_bounds = [bounds[2], bounds[3]] # Y bounds
642
+
643
+ # Initialize slice positions to volume center if they are currently 0.0 (scene defaults)
644
+ if self.server.state.axial_slice == 0.0:
645
+ self.server.state.axial_slice = (bounds[4] + bounds[5]) / 2 # Z center
646
+ if self.server.state.sagittal_slice == 0.0:
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
654
+ except (RuntimeError, IndexError) as e:
655
+ print(f"Error: Cannot get bounds for volume '{active_volume_label}': {e}")
656
+ return
657
+
658
+ # Create MPR actors for current frame
659
+ current_frame = getattr(self.server.state, "frame", 0)
660
+ mpr_actors = active_volume.get_mpr_actors_for_frame(current_frame)
661
+
662
+ # Add MPR actors to their respective renderers
663
+ if self.scene.axial_renderWindow:
664
+ axial_renderer = (
665
+ self.scene.axial_renderWindow.GetRenderers().GetFirstRenderer()
666
+ )
667
+ if axial_renderer:
668
+ axial_renderer.RemoveAllViewProps() # Clear existing actors
669
+ axial_renderer.AddActor(mpr_actors["axial"]["actor"])
670
+ mpr_actors["axial"]["actor"].SetVisibility(True)
671
+ axial_renderer.ResetCamera()
672
+
673
+ if self.scene.coronal_renderWindow:
674
+ coronal_renderer = (
675
+ self.scene.coronal_renderWindow.GetRenderers().GetFirstRenderer()
676
+ )
677
+ if coronal_renderer:
678
+ coronal_renderer.RemoveAllViewProps()
679
+ coronal_renderer.AddActor(mpr_actors["coronal"]["actor"])
680
+ mpr_actors["coronal"]["actor"].SetVisibility(True)
681
+ coronal_renderer.ResetCamera()
682
+
683
+ if self.scene.sagittal_renderWindow:
684
+ sagittal_renderer = (
685
+ self.scene.sagittal_renderWindow.GetRenderers().GetFirstRenderer()
686
+ )
687
+ if sagittal_renderer:
688
+ sagittal_renderer.RemoveAllViewProps()
689
+ sagittal_renderer.AddActor(mpr_actors["sagittal"]["actor"])
690
+ mpr_actors["sagittal"]["actor"].SetVisibility(True)
691
+ sagittal_renderer.ResetCamera()
692
+
693
+ # Apply current window/level settings to the MPR actors
694
+ window = getattr(self.server.state, "mpr_window", 800.0)
695
+ level = getattr(self.server.state, "mpr_level", 200.0)
696
+ active_volume.update_mpr_window_level(current_frame, window, level)
697
+
698
+ # Update all views
699
+ self.server.controller.view_update()
700
+
701
+ def update_slice_positions(self, **kwargs):
702
+ """Update MPR slice positions when sliders change."""
703
+
704
+ if not getattr(self.server.state, "mpr_enabled", False):
705
+ return
706
+
707
+ active_volume_label = getattr(self.server.state, "active_volume_label", "")
708
+ if not active_volume_label:
709
+ return
710
+
711
+ # Find the active volume
712
+ active_volume = None
713
+ for volume in self.scene.volumes:
714
+ if volume.label == active_volume_label:
715
+ active_volume = volume
716
+ break
717
+
718
+ if not active_volume:
719
+ return
720
+
721
+ # Get current slice positions
722
+ axial_slice = getattr(self.server.state, "axial_slice", 0.5)
723
+ sagittal_slice = getattr(self.server.state, "sagittal_slice", 0.5)
724
+ coronal_slice = getattr(self.server.state, "coronal_slice", 0.5)
725
+ current_frame = getattr(self.server.state, "frame", 0)
726
+
727
+ # Get rotation data
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
+ )
734
+
735
+ # Update slice positions with rotation
736
+ active_volume.update_slice_positions(
737
+ current_frame,
738
+ axial_slice,
739
+ sagittal_slice,
740
+ coronal_slice,
741
+ rotation_sequence,
742
+ rotation_angles,
743
+ )
744
+
745
+ # Update all views
746
+ self.server.controller.view_update()
747
+
748
+ def update_mpr_window_level(self, **kwargs):
749
+ """Update MPR window/level when sliders change."""
750
+
751
+ if not getattr(self.server.state, "mpr_enabled", False):
752
+ return
753
+
754
+ active_volume_label = getattr(self.server.state, "active_volume_label", "")
755
+ if not active_volume_label:
756
+ return
757
+
758
+ # Find the active volume
759
+ active_volume = None
760
+ for volume in self.scene.volumes:
761
+ if volume.label == active_volume_label:
762
+ active_volume = volume
763
+ break
764
+
765
+ if not active_volume:
766
+ return
767
+
768
+ # Get current window/level values
769
+ window = getattr(self.server.state, "mpr_window", 400.0)
770
+ level = getattr(self.server.state, "mpr_level", 40.0)
771
+ current_frame = getattr(self.server.state, "frame", 0)
772
+
773
+ # Check if this change is from manual adjustment (not from preset)
774
+ # by checking if we're not in the middle of a preset update
775
+ if not getattr(self, "_updating_from_preset", False):
776
+ # Reset preset selection when manually adjusting window/level
777
+ current_preset = getattr(self.server.state, "mpr_window_level_preset", None)
778
+ if current_preset is not None:
779
+ self.server.state.mpr_window_level_preset = None
780
+
781
+ # Update window/level for MPR actors
782
+ active_volume.update_mpr_window_level(current_frame, window, level)
783
+
784
+ # Update all views
785
+ self.server.controller.view_update()
786
+
787
+ def update_mpr_preset(self, mpr_window_level_preset, **kwargs):
788
+ """Update MPR window/level when preset changes."""
789
+ from .window_level import presets
790
+
791
+ # Handle None value (Select W/L... option) - do nothing
792
+ if mpr_window_level_preset is None:
793
+ return
794
+
795
+ if mpr_window_level_preset in presets:
796
+ preset = presets[mpr_window_level_preset]
797
+
798
+ # Set flag to indicate we're updating from preset
799
+ self._updating_from_preset = True
800
+ try:
801
+ self.server.state.mpr_window = preset.window
802
+ self.server.state.mpr_level = preset.level
803
+
804
+ # Update the actual MPR views with new window/level
805
+ self.update_mpr_window_level()
806
+ finally:
807
+ # Always clear the flag
808
+ self._updating_from_preset = False
809
+
810
+ def update_mpr_rotation(self, **kwargs):
811
+ """Update MPR views when rotation changes."""
812
+ if not getattr(self.server.state, "mpr_enabled", False):
813
+ return
814
+
815
+ active_volume_label = getattr(self.server.state, "active_volume_label", "")
816
+ if not active_volume_label:
817
+ return
818
+
819
+ # Find the active volume
820
+ active_volume = None
821
+ for volume in self.scene.volumes:
822
+ if volume.label == active_volume_label:
823
+ active_volume = volume
824
+ break
825
+
826
+ if not active_volume:
827
+ return
828
+
829
+ # Get current slice positions
830
+ axial_slice = getattr(self.server.state, "axial_slice", 0.5)
831
+ sagittal_slice = getattr(self.server.state, "sagittal_slice", 0.5)
832
+ coronal_slice = getattr(self.server.state, "coronal_slice", 0.5)
833
+ current_frame = getattr(self.server.state, "frame", 0)
834
+
835
+ # Get rotation data - include all visible rotations
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
+ )
846
+
847
+ # Update slice positions with rotation
848
+ active_volume.update_slice_positions(
849
+ current_frame,
850
+ axial_slice,
851
+ sagittal_slice,
852
+ coronal_slice,
853
+ rotation_sequence,
854
+ rotation_angles,
855
+ )
856
+
857
+ # Update all views
858
+ self.server.controller.view_update()
859
+
860
+ def add_mpr_rotation(self, axis):
861
+ """Add a new rotation to the MPR rotation sequence."""
862
+ import copy
863
+
864
+ current_sequence = copy.deepcopy(
865
+ getattr(self.server.state, "mpr_rotation_sequence", [])
866
+ )
867
+ current_sequence.append({"axis": axis, "angle": 0})
868
+ self.server.state.mpr_rotation_sequence = current_sequence
869
+ self.update_mpr_rotation_labels()
870
+
871
+ def remove_mpr_rotation(self, index):
872
+ """Remove a rotation at given index and all subsequent rotations."""
873
+ sequence = list(getattr(self.server.state, "mpr_rotation_sequence", []))
874
+ if 0 <= index < len(sequence):
875
+ sequence = sequence[:index]
876
+ self.server.state.mpr_rotation_sequence = sequence
877
+
878
+ # Reset angle states for all removed rotations
879
+ for i in range(index, self.scene.max_mpr_rotations):
880
+ setattr(self.server.state, f"mpr_rotation_angle_{i}", 0)
881
+
882
+ self.update_mpr_rotation_labels()
883
+
884
+ def reset_mpr_rotations(self):
885
+ """Reset all MPR rotations."""
886
+ self.server.state.mpr_rotation_sequence = []
887
+ for i in range(self.scene.max_mpr_rotations):
888
+ setattr(self.server.state, f"mpr_rotation_angle_{i}", 0)
889
+ setattr(self.server.state, f"mpr_rotation_visible_{i}", True)
890
+ self.update_mpr_rotation_labels()
891
+
892
+ def update_mpr_rotation_labels(self):
893
+ """Update the rotation axis labels for display."""
894
+ rotation_sequence = getattr(self.server.state, "mpr_rotation_sequence", [])
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}")
904
+
905
+ @asynchronous.task
906
+ async def close_application(self):
907
+ """Close the application by stopping the server."""
908
+ await self.server.stop()
cardio/mesh.py CHANGED
@@ -3,7 +3,6 @@ import enum
3
3
  import logging
4
4
 
5
5
  # Third Party
6
- import numpy as np
7
6
  import pydantic as pc
8
7
  import vtk
9
8
 
@@ -167,9 +166,9 @@ class Mesh(Object):
167
166
 
168
167
  def color_transfer_function(self):
169
168
  ctf = vtk.vtkColorTransferFunction()
170
- ctf.AddRGBPoint(0.7, 0.0, 0.0, 1.0)
169
+ ctf.AddRGBPoint(self.ctf_min, 0.0, 0.0, 1.0)
171
170
  ctf.AddRGBPoint(1.0, 1.0, 0.0, 0.0)
172
- ctf.AddRGBPoint(1.3, 1.0, 1.0, 0.0)
171
+ ctf.AddRGBPoint(self.ctf_max, 1.0, 1.0, 0.0)
173
172
  return ctf
174
173
 
175
174
  def setup_scalar_coloring(self, mapper):