11#!/usr/bin/env python
2- """Demo: in-memory 3D image viewed as 2D slice and 3D volume .
2+ """Demo: in-memory 3D image with toggleable 2D/3D view in a single scene .
33
44Run with:
55 uv run python scripts/v2/image_memory_demo.py
1414from PySide6 .QtCore import QTimer
1515from PySide6 .QtWidgets import (
1616 QApplication ,
17+ QCheckBox ,
1718 QHBoxLayout ,
1819 QLabel ,
20+ QPushButton ,
1921 QSpinBox ,
2022 QVBoxLayout ,
2123 QWidget ,
@@ -32,120 +34,123 @@ def main() -> None:
3234 app = QApplication (sys .argv )
3335
3436 # ── 1. Create data ──────────────────────────────────────────────────
35- volume = binary_blobs (length = 128 , volume_fraction = 0.1 , n_dim = 3 ).astype (np .float32 )
37+ volume = binary_blobs (length = 128 , volume_fraction = 0.03 , n_dim = 3 ).astype (np .float32 )
3638 store = ImageMemoryStore (data = volume , name = "demo_volume" )
3739
3840 # ── 2. Set up the controller ────────────────────────────────────────
3941 controller = CellierController ()
4042
41- # Coordinate system: 3 spatial axes
4243 cs = CoordinateSystem (name = "world" , axis_labels = ("z" , "y" , "x" ))
4344
44- # 2D scene: displayed_axes=(1,2), slice_indices={0:0}
45- scene_2d = controller .add_scene (dim = "2d" , coordinate_system = cs , name = "slice" )
46- # 3D scene: displayed_axes=(0,1,2), slice_indices={}
47- scene_3d = controller .add_scene (dim = "3d" , coordinate_system = cs , name = "volume" )
48-
49- appearance = ImageMemoryAppearance (
50- color_map = "viridis" ,
51- clim = (0.0 , 1.0 ),
45+ # Single scene that supports both 2D and 3D rendering.
46+ # Start in 3D; displayed_axes=(0,1,2).
47+ scene = controller .add_scene (
48+ dim = "3d" ,
49+ coordinate_system = cs ,
50+ name = "volume" ,
51+ render_modes = {"2d" , "3d" },
5252 )
5353
54- # Add the same store to both scenes (store is registered only once)
55- visual_2d = controller .add_image (
56- data = store ,
57- scene_id = scene_2d .id ,
58- appearance = appearance ,
59- name = "slice_view" ,
60- )
61- visual_3d = controller .add_image (
54+ appearance = ImageMemoryAppearance (color_map = "viridis" , clim = (0.0 , 1.0 ))
55+ visual = controller .add_image (
6256 data = store ,
63- scene_id = scene_3d .id ,
57+ scene_id = scene .id ,
6458 appearance = appearance ,
6559 name = "volume_view" ,
6660 )
61+ visual .aabb .color = "#ff00ff"
6762
68- # ── 3. Create canvases and z-slice control ─────────────────────────
63+ # ── 3. Build UI ──────────────────────────── ─────────────────────────
6964 root = QWidget ()
7065 outer_layout = QVBoxLayout (root )
7166 controller .set_widget_parent (root )
7267
73- # Canvas row
74- canvas_row = QWidget ()
75- canvas_layout = QHBoxLayout (canvas_row )
76- canvas_layout .setContentsMargins (0 , 0 , 0 , 0 )
68+ canvas_widget = controller .add_canvas (scene_id = scene .id )
69+ outer_layout .addWidget (canvas_widget )
7770
78- widget_2d = controller .add_canvas (scene_id = scene_2d .id )
79- widget_3d = controller .add_canvas (scene_id = scene_3d .id )
80-
81- canvas_layout .addWidget (widget_2d )
82- canvas_layout .addWidget (widget_3d )
83- outer_layout .addWidget (canvas_row )
84-
85- # Z-slice spinbox
86- z_depth = store .shape [0 ] # axis 0 = z
71+ # Controls row
8772 controls = QWidget ()
8873 ctrl_layout = QHBoxLayout (controls )
8974 ctrl_layout .setContentsMargins (0 , 0 , 0 , 0 )
90- ctrl_layout .addWidget (QLabel ("Z slice:" ))
9175
76+ z_depth = store .shape [0 ]
77+ ctrl_layout .addWidget (QLabel ("Z slice:" ))
9278 z_spin = QSpinBox ()
9379 z_spin .setRange (0 , z_depth - 1 )
94- z_spin .setValue (0 )
80+ z_spin .setValue (z_depth // 2 )
9581 ctrl_layout .addWidget (z_spin )
9682 ctrl_layout .addStretch ()
83+
84+ aabb_check = QCheckBox ("Show bounding box" )
85+ aabb_check .setChecked (False )
86+ ctrl_layout .addWidget (aabb_check )
87+
88+ toggle_btn = QPushButton ("Switch to 2D" )
89+ ctrl_layout .addWidget (toggle_btn )
9790 outer_layout .addWidget (controls )
9891
92+ root .resize (900 , 700 )
93+ root .show ()
94+
95+ # ── 4. Toggle logic ─────────────────────────────────────────────────
96+ _current_dim = ["3d" ]
97+
98+ def _toggle () -> None :
99+ if _current_dim [0 ] == "3d" :
100+ _current_dim [0 ] = "2d"
101+ toggle_btn .setText ("Switch to 3D" )
102+ scene .dims .selection .slice_indices = {0 : z_spin .value ()}
103+ scene .dims .selection .displayed_axes = (1 , 2 )
104+ else :
105+ _current_dim [0 ] = "3d"
106+ toggle_btn .setText ("Switch to 2D" )
107+ scene .dims .selection .displayed_axes = (0 , 1 , 2 )
108+
109+ controller .reslice_scene (scene .id )
110+
111+ toggle_btn .clicked .connect (_toggle )
112+
99113 def _on_z_changed (z : int ) -> None :
100- scene_2d .dims .selection .slice_indices = {0 : z }
101- controller .reslice_scene (scene_2d .id )
114+ scene .dims .selection .slice_indices = {0 : z }
115+ if _current_dim [0 ] == "2d" :
116+ controller .reslice_scene (scene .id )
102117
103118 z_spin .valueChanged .connect (_on_z_changed )
104119
105- root . resize ( 1200 , 700 )
106- root . show ()
120+ def _on_aabb_toggled ( checked : bool ) -> None :
121+ visual . aabb . enabled = checked
107122
108- # ── 4. Initial reslice + camera fit ────────────────────────────────
109- # The async slicer needs a running event loop, so reslice is deferred
110- # with QTimer.singleShot(0, ...) until QtAsyncio.run() has started.
111- #
112- # look_at_visual computes the bounding box from the current node
113- # geometry, so it must run *after* data has been committed to the GPU
114- # (otherwise the placeholder texture gives a near-zero bounding box).
115- # We wrap the on_data_ready callbacks to trigger camera fit on the
116- # first delivery — event-driven, not time-based.
117-
118- visuals_loaded : set [str ] = set ()
119-
120- def _wrap_callback (original , visual_id , label , view_dir , up ):
121- def _wrapped (batch ):
122- original (batch )
123- if label not in visuals_loaded :
124- visuals_loaded .add (label )
125- controller .look_at_visual (visual_id , view_direction = view_dir , up = up )
126-
127- return _wrapped
128-
129- # Patch the render-layer visuals to fire camera fit on first data.
130- scene_mgr_2d = controller ._render_manager ._scenes [scene_2d .id ]
131- gfx_vis_2d = scene_mgr_2d .get_visual (visual_2d .id )
132- gfx_vis_2d .on_data_ready_2d = _wrap_callback (
133- gfx_vis_2d .on_data_ready_2d ,
134- visual_2d .id ,
135- "2d" ,
136- view_dir = (0 , 0 , - 1 ),
137- up = (0 , 1 , 0 ),
138- )
123+ aabb_check .toggled .connect (_on_aabb_toggled )
139124
140- scene_mgr_3d = controller ._render_manager ._scenes [scene_3d .id ]
141- gfx_vis_3d = scene_mgr_3d .get_visual (visual_3d .id )
142- gfx_vis_3d .on_data_ready = _wrap_callback (
143- gfx_vis_3d .on_data_ready ,
144- visual_3d .id ,
145- "3d" ,
146- view_dir = (- 1 , - 1 , - 1 ),
147- up = (0 , 0 , 1 ),
148- )
125+ # ── 5. Initial reslice + camera fit ────────────────────────────────
126+ # Patch on_data_ready to fit the camera on the first delivery.
127+ scene_mgr = controller ._render_manager ._scenes [scene .id ]
128+ gfx_vis = scene_mgr .get_visual (visual .id )
129+ _camera_fitted : set [str ] = set ()
130+
131+ _orig_3d = gfx_vis .on_data_ready
132+
133+ def _patched_3d (batch ):
134+ _orig_3d (batch )
135+ if "3d" not in _camera_fitted :
136+ _camera_fitted .add ("3d" )
137+ controller .look_at_visual (
138+ visual .id , view_direction = (- 1 , - 1 , - 1 ), up = (0 , 0 , 1 )
139+ )
140+
141+ gfx_vis .on_data_ready = _patched_3d
142+
143+ _orig_2d = gfx_vis .on_data_ready_2d
144+
145+ def _patched_2d (batch ):
146+ _orig_2d (batch )
147+ if "2d" not in _camera_fitted :
148+ _camera_fitted .add ("2d" )
149+ controller .look_at_visual (
150+ visual .id , view_direction = (0 , 0 , - 1 ), up = (0 , 1 , 0 )
151+ )
152+
153+ gfx_vis .on_data_ready_2d = _patched_2d
149154
150155 QTimer .singleShot (0 , controller .reslice_all )
151156
0 commit comments