88
99import numpy as np
1010
11- from cellier .v2 ._state import CameraState , DimsState
1211from cellier .v2 .events import (
1312 AppearanceChangedEvent ,
1413 CameraChangedEvent ,
2928 PerspectiveCamera ,
3029)
3130from cellier .v2 .scene .canvas import Canvas
32- from cellier .v2 .scene .dims import CoordinateSystem , DimsManager
31+ from cellier .v2 .scene .dims import (
32+ AxisAlignedSelection ,
33+ CoordinateSystem ,
34+ DimsManager ,
35+ )
3336from cellier .v2 .scene .scene import Scene
3437from cellier .v2 .viewer_model import DataManager , ViewerModel
3538from cellier .v2 .visuals ._image import MultiscaleImageVisual
4043 from psygnal import EmissionInfo
4144 from PySide6 .QtWidgets import QWidget
4245
46+ from cellier .v2 ._state import CameraState , DimsState
4347 from cellier .v2 .data ._base_data_store import BaseDataStore
4448 from cellier .v2 .visuals ._image import ImageAppearance
4549
@@ -174,19 +178,23 @@ def add_scene(
174178 Scene
175179 The live model object. Use ``scene.id`` in subsequent calls.
176180 """
181+ ndim = len (coordinate_system .axis_labels )
177182 if dim == "3d" :
178- displayed_axes = ( 0 , 1 , 2 )
179- slice_indices : tuple [int , ... ] = ()
183+ displayed_axes = tuple ( range ( ndim ) )
184+ slice_indices_dict : dict [int , int ] = {}
180185 elif dim == "2d" :
181- displayed_axes = (0 , 1 )
182- slice_indices = (0 ,)
186+ # Display the last two axes; slice all others at 0.
187+ displayed_axes = tuple (range (ndim - 2 , ndim ))
188+ slice_indices_dict = {i : 0 for i in range (ndim - 2 )}
183189 else :
184190 raise ValueError (f"dim must be '2d' or '3d', got { dim !r} " )
185191
186192 dims = DimsManager (
187193 coordinate_system = coordinate_system ,
188- displayed_axes = displayed_axes ,
189- slice_indices = slice_indices ,
194+ selection = AxisAlignedSelection (
195+ displayed_axes = displayed_axes ,
196+ slice_indices = slice_indices_dict ,
197+ ),
190198 )
191199 scene = Scene (name = name , dims = dims )
192200 self ._model .scenes [scene .id ] = scene
@@ -267,7 +275,7 @@ def add_image(
267275 self ._model .data .stores [data .id ] = data
268276
269277 scene = self ._model .scenes [scene_id ]
270- dim = scene .dims .displayed_axes # (0,1,2) → 3d, (0,1) → 2d
278+ displayed_axes = scene .dims .selection . displayed_axes
271279
272280 downscale_factors = [2 ** i for i in range (data .n_levels )]
273281
@@ -280,12 +288,13 @@ def add_image(
280288
281289 scene .visuals .append (visual_model )
282290
283- render_modes = {"3d" } if len (dim ) == 3 else {"2d" }
291+ render_modes = {"3d" } if len (displayed_axes ) == 3 else {"2d" }
284292
285293 level_shapes = list (data .level_shapes )
286294 gfx_visual = GFXMultiscaleImageVisual .from_cellier_model (
287295 model = visual_model ,
288296 level_shapes = level_shapes ,
297+ displayed_axes = displayed_axes ,
289298 render_modes = render_modes ,
290299 block_size = block_size ,
291300 gpu_budget_bytes = gpu_budget_bytes ,
@@ -358,11 +367,13 @@ def add_visual(
358367 scene = self ._model .scenes [scene_id ]
359368 scene .visuals .append (visual_model )
360369
361- render_modes = {"3d" } if len (scene .dims .displayed_axes ) == 3 else {"2d" }
370+ displayed_axes = scene .dims .selection .displayed_axes
371+ render_modes = {"3d" } if len (displayed_axes ) == 3 else {"2d" }
362372 level_shapes = list (data_store .level_shapes )
363373 gfx_visual = GFXMultiscaleImageVisual .from_cellier_model (
364374 model = visual_model ,
365375 level_shapes = level_shapes ,
376+ displayed_axes = displayed_axes ,
366377 render_modes = render_modes ,
367378 block_size = block_size ,
368379 gpu_budget_bytes = gpu_budget_bytes ,
@@ -408,7 +419,9 @@ def add_canvas(
408419 # Infer dimensionality from the scene if not specified.
409420 if available_dims is None :
410421 scene = self ._model .scenes [scene_id ]
411- available_dims = "3d" if len (scene .dims .displayed_axes ) == 3 else "2d"
422+ available_dims = (
423+ "3d" if len (scene .dims .selection .displayed_axes ) == 3 else "2d"
424+ )
412425
413426 camera_model = PerspectiveCamera (
414427 fov = fov ,
@@ -487,33 +500,29 @@ def _build_visual_configs_for_scene(
487500
488501 def _dims_state_for_scene (self , scene_id : UUID ) -> DimsState :
489502 """Derive a DimsState from the scene's DimsManager."""
490- dims = self ._model .scenes [scene_id ].dims
491- return DimsState (
492- displayed_axes = dims .displayed_axes ,
493- slice_indices = dims .slice_indices ,
494- )
503+ return self ._model .scenes [scene_id ].dims .to_state ()
495504
496505 # ------------------------------------------------------------------
497506 # psygnal bridges
498507 # ------------------------------------------------------------------
499508
500509 def _wire_dims_model (self , scene : Scene ) -> None :
501510 """Subscribe to all field changes on a scene's DimsManager."""
502- self ._dims_cache [scene .id ] = scene .dims .displayed_axes
511+ self ._dims_cache [scene .id ] = scene .dims .selection . displayed_axes
503512 scene .dims .events .connect (self ._make_dims_handler (scene .id ))
504513
505514 def _make_dims_handler (self , scene_id : UUID ) -> Callable :
506515 """Return a psygnal catch-all handler for a scene's DimsManager."""
507516
508517 def _on_dims_psygnal (info : EmissionInfo ) -> None :
509- dims = self ._model .scenes [scene_id ].dims
510- new_state = DimsState (
511- displayed_axes = dims .displayed_axes ,
512- slice_indices = dims .slice_indices ,
513- )
518+ new_state = self ._model .scenes [scene_id ].dims .to_state ()
514519 prev_axes = self ._dims_cache [scene_id ]
515- displayed_axes_changed = prev_axes != new_state .displayed_axes
516- self ._dims_cache [scene_id ] = new_state .displayed_axes
520+ displayed_axes_changed = prev_axes != new_state .selection .displayed_axes
521+ self ._dims_cache [scene_id ] = new_state .selection .displayed_axes
522+ if displayed_axes_changed :
523+ self ._rebuild_visuals_geometry (
524+ scene_id , new_state .selection .displayed_axes
525+ )
517526 self ._event_bus .emit (
518527 DimsChangedEvent (
519528 source_id = self ._id ,
@@ -525,6 +534,28 @@ def _on_dims_psygnal(info: EmissionInfo) -> None:
525534
526535 return _on_dims_psygnal
527536
537+ def _rebuild_visuals_geometry (
538+ self , scene_id : UUID , displayed_axes : tuple [int , ...]
539+ ) -> None :
540+ """Rebuild geometry for all visuals in a scene after displayed_axes change."""
541+ scene = self ._model .scenes [scene_id ]
542+ scene_manager = self ._render_manager ._scenes [scene_id ]
543+
544+ for visual_model in scene .visuals :
545+ data_store = self ._model .data .stores [UUID (visual_model .data_store_id )]
546+ level_shapes = list (data_store .level_shapes )
547+ gfx_visual = scene_manager .get_visual (visual_model .id )
548+
549+ old_node , new_node = gfx_visual .rebuild_geometry (
550+ level_shapes , displayed_axes
551+ )
552+
553+ # Swap node in the pygfx scene graph.
554+ if old_node is not None :
555+ scene_manager .scene .remove (old_node )
556+ if new_node is not None :
557+ scene_manager .scene .add (new_node )
558+
528559 def _wire_appearance (self , visual : MultiscaleImageVisual ) -> None :
529560 """Subscribe to all field changes on a visual's ImageAppearance."""
530561 visual .appearance .events .connect (self ._make_appearance_handler (visual .id ))
0 commit comments