3333 deserialize_hdr ,
3434 deserialize_mat4 ,
3535 deserialize_options ,
36- deserialize_scene ,
3736 deserialize_volume_object_3d_data ,
3837 serialize_colormap_label ,
3938 serialize_enum ,
@@ -840,11 +839,43 @@ class NiiVue(BaseAnyWidget):
840839 scene = t .Instance (Scene , allow_none = True ).tag (
841840 sync = True ,
842841 to_json = serialize_scene ,
843- from_json = deserialize_scene ,
844842 )
845843 overlay_outline_width = t .Float (0.0 ).tag (sync = True ) # 0 for none
846844 overlay_alpha_shader = t .Float (1.0 ).tag (sync = True ) # 1 for opaque
847845
846+ other_nv = t .List (t .Instance (object , allow_none = False ), default_value = []).tag (
847+ sync = False
848+ )
849+ sync_opts = t .Dict (
850+ default_value = {
851+ "3d" : False ,
852+ "2d" : False ,
853+ "zoom_pan" : False ,
854+ "cal_min" : False ,
855+ "cal_max" : False ,
856+ "clip_plane" : False ,
857+ "gamma" : False ,
858+ "slice_type" : False ,
859+ "crosshair" : False ,
860+ }
861+ ).tag (sync = False )
862+
863+ @t .validate ("other_nv" )
864+ def _validate_other_nv (self , proposal ):
865+ value = proposal ["value" ]
866+ for nv_inst in value :
867+ if nv_inst is self :
868+ raise t .TraitError ("Cannot sync to self." )
869+ if (
870+ f"{ type (nv_inst ).__module__ } .{ type (nv_inst ).__qualname__ } "
871+ != "ipyniivue.widget.NiiVue"
872+ ):
873+ raise t .TraitError (
874+ "All items in `other_nv` must be NiiVue instances."
875+ + str (type (self ))
876+ )
877+ return value
878+
848879 def __init__ (self , height : int = 300 , ** options ): # noqa: D417
849880 r"""
850881 Initialize the NiiVue widget.
@@ -864,14 +895,15 @@ def __init__(self, height: int = 300, **options): # noqa: D417
864895 opts = ConfigOptions (parent = self , ** options )
865896 super ().__init__ (height = height , opts = opts , volumes = [], meshes = [])
866897
867- # Handle messages coming from frontend
868- self ._event_handlers = {}
869- self .on_msg (self ._handle_custom_msg )
870-
871898 # Initialize values
872899 self ._cluts = self ._get_initial_colormaps ()
873900 self .graph = Graph (parent = self )
874901 self .scene = Scene (parent = self )
902+ self .other_nv = []
903+
904+ # Handle messages coming from frontend
905+ self ._event_handlers = {}
906+ self .on_msg (self ._handle_custom_msg )
875907
876908 def __setattr__ (self , name , value ):
877909 """todo: remove this starting version 2.4.1."""
@@ -885,6 +917,13 @@ def __setattr__(self, name, value):
885917 setattr (self .opts , name , value )
886918 super ().__setattr__ (name , value )
887919
920+ def set_state (self , state ):
921+ """Override set_state to silence notifications for certain updates."""
922+ if "scene" in state :
923+ self .scene ._trait_values .update (state ["scene" ])
924+ return
925+ return super ().set_state (state )
926+
888927 def _notify_opts_changed (self ):
889928 self .notify_change (
890929 {
@@ -941,6 +980,11 @@ def _handle_custom_msg(self, content, buffers):
941980 self ._add_mesh_from_frontend (data )
942981 return
943982
983+ # sync
984+ elif event == "sync" :
985+ self .sync ()
986+ return
987+
944988 # check if the event has a registered handler
945989 handler = self ._event_handlers .get (event )
946990 if not handler :
@@ -2933,6 +2977,142 @@ def hover_idx_callback(data):
29332977 """
29342978 self ._register_callback ("hover_idx_change" , callback , remove = remove )
29352979
2980+ """
2981+ Sync
2982+ """
2983+
2984+ def broadcast_to (self , other_nv , sync_opts = None ):
2985+ """
2986+ Sync the scene controls from one NiiVue instance to others.
2987+
2988+ Useful for using one canvas to drive another.
2989+
2990+ Parameters
2991+ ----------
2992+ other_nv : :class:`NiiVue` or list of :class:`NiiVue`
2993+ The other NiiVue instance(s) to broadcast state to.
2994+ sync_opts : dict, optional
2995+ Options specifying which properties to sync. E.g., {'2d': True, '3d': True}
2996+ Possible keys are:
2997+ - gamma
2998+ - crosshair
2999+ - zoom_pan
3000+ - slice_type
3001+ - cal_min
3002+ - cal_max
3003+ - clip_plane
3004+ - 2d
3005+ - 3d
3006+ """
3007+ if not isinstance (sync_opts , dict ):
3008+ sync_opts = {"2d" : True , "3d" : True }
3009+
3010+ if not isinstance (other_nv , (list , tuple )):
3011+ other_nv = [other_nv ]
3012+
3013+ self .other_nv = other_nv
3014+ self .sync_opts = sync_opts
3015+
3016+ def _do_sync_3d (self , other_nv ):
3017+ """Synchronize 3D view settings with another NiiVue instance.
3018+
3019+ Do not call this by itself. This should be called by the sync method.
3020+ """
3021+ other_nv .scene ._trait_values ["render_azimuth" ] = self .scene .render_azimuth
3022+ other_nv .scene ._trait_values ["render_elevation" ] = self .scene .render_elevation
3023+ other_nv .scene ._trait_values ["vol_scale_multiplier" ] = (
3024+ self .scene .vol_scale_multiplier
3025+ )
3026+
3027+ def _do_sync_2d (self , other_nv ):
3028+ """Synchronize 2D crosshair pos + pan settings with another NiiVue instance.
3029+
3030+ Do not call this by itself. This should be called by the sync method.
3031+ """
3032+ this_mm = self .frac2mm (self .scene .crosshair_pos )
3033+ other_nv .scene ._trait_values ["crosshair_pos" ] = other_nv .mm2frac (this_mm )
3034+ other_nv .scene ._trait_values ["pan2d_xyzmm" ] = list (self .scene .pan2d_xyzmm )
3035+
3036+ def _do_sync_gamma (self , other_nv ):
3037+ """Synchronize gamma correction setting with another NiiVue instance."""
3038+ this_gamma = self .scene .gamma
3039+ other_gamma = other_nv .scene .gamma
3040+ if this_gamma != other_gamma :
3041+ other_nv .set_gamma (this_gamma )
3042+
3043+ def _do_sync_zoom_pan (self , other_nv ):
3044+ """Synchronize zoom/pan settings with another NiiVue instance.
3045+
3046+ Do not call this by itself. This should be called by the sync method.
3047+ """
3048+ other_nv .scene ._trait_values ["pan2d_xyzmm" ] = list (self .scene .pan2d_xyzmm )
3049+
3050+ def _do_sync_crosshair (self , other_nv ):
3051+ """Synchronize crosshair position with another NiiVue instance.
3052+
3053+ Do not call this by itself. This should be called by the sync method.
3054+ """
3055+ this_mm = self .frac2mm (self .scene .crosshair_pos )
3056+ other_nv .scene ._trait_values ["crosshair_pos" ] = other_nv .mm2frac (this_mm )
3057+
3058+ def _do_sync_cal_min (self , other_nv ):
3059+ """Synchronize cal_min with another NiiVue instance."""
3060+ if (
3061+ self .volumes
3062+ and other_nv .volumes
3063+ and self .volumes [0 ].cal_min != other_nv .volumes [0 ].cal_min
3064+ ):
3065+ other_nv .volumes [0 ].cal_min = self .volumes [0 ].cal_min
3066+
3067+ def _do_sync_cal_max (self , other_nv ):
3068+ """Synchronize cal_max with another NiiVue instance."""
3069+ if (
3070+ self .volumes
3071+ and other_nv .volumes
3072+ and self .volumes [0 ].cal_max != other_nv .volumes [0 ].cal_max
3073+ ):
3074+ other_nv .volumes [0 ].cal_max = self .volumes [0 ].cal_max
3075+
3076+ def _do_sync_slice_type (self , other_nv ):
3077+ """Synchronize slice view type with another NiiVue instance."""
3078+ other_nv .set_slice_type (self .opts .slice_type )
3079+
3080+ def _do_sync_clip_plane (self , other_nv ):
3081+ """Synchronize clip plane settings with another NiiVue instance."""
3082+ other_nv .scene ._trait_values ["clip_plane_depth_azi_elev" ] = list (
3083+ self .scene .clip_plane_depth_azi_elev
3084+ )
3085+
3086+ def sync (self ):
3087+ """Sync the scene controls from this NiiVue instance to others."""
3088+ for nv_obj in self .other_nv :
3089+ if not nv_obj ._canvas_attached :
3090+ # todo: add logging msg here
3091+ continue
3092+
3093+ if self .sync_opts .get ("gamma" ):
3094+ self ._do_sync_gamma (nv_obj )
3095+ if self .sync_opts .get ("crosshair" ):
3096+ self ._do_sync_crosshair (nv_obj )
3097+ if self .sync_opts .get ("zoom_pan" ):
3098+ self ._do_sync_zoom_pan (nv_obj )
3099+ if self .sync_opts .get ("slice_type" ):
3100+ self ._do_sync_slice_type (nv_obj )
3101+ if self .sync_opts .get ("cal_min" ):
3102+ self ._do_sync_cal_min (nv_obj )
3103+ if self .sync_opts .get ("cal_max" ):
3104+ self ._do_sync_cal_max (nv_obj )
3105+ if self .sync_opts .get ("clip_plane" ):
3106+ self ._do_sync_clip_plane (nv_obj )
3107+
3108+ # legacy 2d and 3d opts:
3109+ if self .sync_opts .get ("2d" ):
3110+ self ._do_sync_2d (nv_obj )
3111+ if self .sync_opts .get ("3d" ):
3112+ self ._do_sync_3d (nv_obj )
3113+
3114+ nv_obj ._notify_scene_changed ()
3115+
29363116 """
29373117 Custom utils
29383118 """
0 commit comments