@@ -26,7 +26,6 @@ def __init__(self, parent=None, *, viewer=None):
2626 layout .addWidget (QLabel ("Active Scalar:" ))
2727 self .scalar_combo = QComboBox ()
2828 self .scalar_combo .setSizePolicy (QSizePolicy .Expanding , QSizePolicy .Fixed )
29- self .scalar_combo .addItem ("<none>" )
3029 self .scalar_combo .currentTextChanged .connect (self ._on_scalar_changed )
3130 layout .addWidget (self .scalar_combo )
3231
@@ -46,6 +45,8 @@ def __init__(self, parent=None, *, viewer=None):
4645 self .colormap_combo = QComboBox ()
4746 self .colormap_combo .addItems (["viridis" , "plasma" , "inferno" , "magma" , "greys" ])
4847 self .colormap_combo .setSizePolicy (QSizePolicy .Expanding , QSizePolicy .Fixed )
48+ # apply colormap changes when user selects a different cmap
49+ self .colormap_combo .currentTextChanged .connect (self ._on_colormap_changed )
4950 layout .addWidget (self .colormap_combo )
5051
5152 # Opacity
@@ -295,7 +296,7 @@ def setCurrentObject(self, object_name: str):
295296 # populate scalar combo
296297 self .scalar_combo .blockSignals (True )
297298 self .scalar_combo .clear ()
298- self . scalar_combo . addItem ( "<none>" )
299+
299300 try :
300301 pdata = getattr (self .current_mesh , 'point_data' , None ) or {}
301302 cdata = getattr (self .current_mesh , 'cell_data' , None ) or {}
@@ -531,6 +532,82 @@ def _on_color_with_scalar_toggled(self, checked: bool):
531532 except Exception :
532533 pass
533534
535+ def _on_colormap_changed (self , cmap : str ):
536+ """Apply or persist selected colormap for the current object.
537+ Best-effort: try in-place application via _apply_scalar_to_actor, otherwise remove and re-add the mesh with the new cmap."""
538+ try :
539+ if not self .current_object_name or self .viewer is None :
540+ return
541+
542+ # persist cmap in metadata even when not coloring by scalar
543+ try :
544+ if self .current_object_name in getattr (self .viewer , 'meshes' , {}):
545+ mesh_entry = self .viewer .meshes [self .current_object_name ]
546+ kwargs = mesh_entry .get ('kwargs' , {}) if isinstance (mesh_entry , dict ) else {}
547+ kwargs ['cmap' ] = cmap or None
548+ mesh_entry ['kwargs' ] = kwargs
549+ # write back
550+ if hasattr (self .viewer , 'meshes' ):
551+ self .viewer .meshes [self .current_object_name ] = mesh_entry
552+ except Exception :
553+ pass
554+
555+ # only need to change rendering if we're coloring by scalar
556+ if not self .color_with_scalar_checkbox .isChecked ():
557+ return
558+
559+ scalar_name = self .scalar_combo .currentText ()
560+ if not scalar_name or scalar_name == "<none>" :
561+ return
562+
563+ # try in-place update first
564+ try :
565+ self ._apply_scalar_to_actor (self .current_object_name , scalar_name )
566+ return
567+ except Exception :
568+ pass
569+
570+ # fallback: remove and re-add mesh with new cmap
571+ mesh_entry = self .viewer .meshes .get (self .current_object_name , None )
572+ if mesh_entry is None :
573+ return
574+ mesh = mesh_entry .get ('mesh' )
575+ old_kwargs = mesh_entry .get ('kwargs' , {}) if isinstance (mesh_entry , dict ) else {}
576+
577+ scalars = None
578+ if scalar_name and scalar_name != "<none>" :
579+ if scalar_name .startswith ('cell:' ):
580+ scalars = scalar_name .split (':' , 1 )[1 ]
581+ else :
582+ scalars = scalar_name
583+
584+ clim = None
585+ try :
586+ if self .range_min .text () and self .range_max .text ():
587+ clim = (float (self .range_min .text ()), float (self .range_max .text ()))
588+ except Exception :
589+ clim = None
590+
591+ opacity = old_kwargs .get ('opacity' , None )
592+ show_scalar_bar = self .scalar_bar_checkbox .isChecked ()
593+
594+ try :
595+ self .viewer .remove_object (self .current_object_name )
596+ except Exception :
597+ pass
598+
599+ try :
600+ self .viewer .add_mesh_object (mesh , name = self .current_object_name , scalars = scalars , cmap = cmap or None , clim = clim , opacity = opacity , show_scalar_bar = show_scalar_bar )
601+ self .current_mesh = self .viewer .meshes .get (self .current_object_name , {}).get ('mesh' )
602+ except Exception :
603+ try :
604+ self .viewer .add_mesh_object (mesh , name = self .current_object_name )
605+ self .current_mesh = self .viewer .meshes .get (self .current_object_name , {}).get ('mesh' )
606+ except Exception :
607+ pass
608+ except Exception :
609+ pass
610+
534611 def _get_scalar_values (self , scalar_name : str ):
535612 if not scalar_name or scalar_name == "<none>" or self .current_mesh is None :
536613 return None
@@ -567,6 +644,165 @@ def _update_histogram(self, values):
567644 except Exception :
568645 pass
569646
647+ def _update_actor_mapper (self , mesh_entry , scalars , cmap , clim , values , actor , plotter ):
648+ """Centralized actor/mapper update:
649+ - select/enable scalar array
650+ - set scalar range
651+ - build and assign a LUT from matplotlib cmap when possible
652+ - persist kwargs and trigger render
653+ """
654+ try :
655+ mapper = getattr (actor , 'mapper' , None )
656+ # if plotter can update scalars more directly, prefer that
657+ if plotter is not None and hasattr (plotter , 'update_scalars' ) and values is not None :
658+ try :
659+ plotter .update_scalars (values , mesh = mesh_entry .get ('mesh' ), render = False , name = self .current_object_name )
660+ except Exception :
661+ pass
662+
663+ if mapper is None :
664+ return
665+
666+ # select color array
667+ try :
668+ if scalars and hasattr (mapper , 'SelectColorArray' ):
669+ try :
670+ mapper .SelectColorArray (scalars )
671+ except Exception :
672+ pass
673+ except Exception :
674+ pass
675+ try :
676+ if scalars and hasattr (mapper , 'SetArrayName' ):
677+ try :
678+ mapper .SetArrayName (scalars )
679+ except Exception :
680+ pass
681+ except Exception :
682+ pass
683+
684+ # enable scalar visibility
685+ try :
686+ if hasattr (mapper , 'scalar_visibility' ):
687+ try :
688+ mapper .scalar_visibility = True
689+ except Exception :
690+ pass
691+ except Exception :
692+ pass
693+ try :
694+ if hasattr (mapper , 'ScalarVisibilityOn' ):
695+ try :
696+ mapper .ScalarVisibilityOn ()
697+ except Exception :
698+ pass
699+ except Exception :
700+ pass
701+
702+ # set scalar range
703+ try :
704+ mn = mx = None
705+ if clim :
706+ mn , mx = float (clim [0 ]), float (clim [1 ])
707+ else :
708+ try :
709+ import numpy as _np
710+ arr = _np .asarray (values ) if values is not None else None
711+ if arr is not None and arr .size > 0 :
712+ mn = float (_np .nanmin (arr ))
713+ mx = float (_np .nanmax (arr ))
714+ except Exception :
715+ pass
716+ if mn is not None and mx is not None :
717+ try :
718+ if hasattr (mapper , 'SetScalarRange' ):
719+ mapper .SetScalarRange (mn , mx )
720+ except Exception :
721+ pass
722+ except Exception :
723+ pass
724+
725+ # build and assign LUT from matplotlib cmap
726+ try :
727+ if cmap :
728+ vtkLookupTable = None
729+ try :
730+ from vtk import vtkLookupTable as _vtkLookupTable # type: ignore
731+ vtkLookupTable = _vtkLookupTable
732+ except Exception :
733+ try :
734+ from vtkmodules .vtkCommonCore import vtkLookupTable as _vtkLookupTable # type: ignore
735+ vtkLookupTable = _vtkLookupTable
736+ except Exception :
737+ vtkLookupTable = None
738+ if vtkLookupTable is not None :
739+ lut = vtkLookupTable ()
740+ lut .SetNumberOfTableValues (256 )
741+ lut .Build ()
742+ try :
743+ import matplotlib .cm as mcm
744+ cm = mcm .get_cmap (cmap )
745+ for i in range (256 ):
746+ r , g , b , a = cm (i / 255.0 )
747+ try :
748+ lut .SetTableValue (i , float (r ), float (g ), float (b ), float (a ))
749+ except Exception :
750+ try :
751+ lut .SetTableValue (i , r , g , b , a )
752+ except Exception :
753+ pass
754+ except Exception :
755+ pass
756+
757+ # set LUT range if we know clim
758+ try :
759+ if clim is not None and len (clim ) == 2 :
760+ try :
761+ lut .SetRange (float (clim [0 ]), float (clim [1 ]))
762+ except Exception :
763+ pass
764+ except Exception :
765+ pass
766+
767+ # assign to mapper
768+ try :
769+ if hasattr (mapper , 'SetLookupTable' ):
770+ try :
771+ mapper .SetLookupTable (lut )
772+ except Exception :
773+ pass
774+ if hasattr (mapper , 'SetUseLookupTableScalarRange' ):
775+ try :
776+ mapper .SetUseLookupTableScalarRange (True )
777+ except Exception :
778+ pass
779+ except Exception :
780+ pass
781+ except Exception :
782+ pass
783+
784+ # persist kwargs
785+ try :
786+ kwargs = mesh_entry .get ('kwargs' , {}) if isinstance (mesh_entry , dict ) else {}
787+ kwargs ['scalars' ] = scalars
788+ kwargs ['cmap' ] = cmap or None
789+ if clim is not None :
790+ kwargs ['clim' ] = (float (clim [0 ]), float (clim [1 ]))
791+ mesh_entry ['kwargs' ] = kwargs
792+ if hasattr (self .viewer , 'meshes' ) and self .current_object_name in self .viewer .meshes :
793+ self .viewer .meshes [self .current_object_name ] = mesh_entry
794+ except Exception :
795+ pass
796+
797+ # request render
798+ try :
799+ if plotter is not None and hasattr (plotter , 'render' ):
800+ plotter .render ()
801+ except Exception :
802+ pass
803+ except Exception :
804+ pass
805+
570806 def _apply_scalar_to_actor (self , object_name : str , scalar_name : str ):
571807 if not object_name or self .viewer is None :
572808 raise RuntimeError ("No viewer or object specified" )
@@ -607,73 +843,17 @@ def _apply_scalar_to_actor(self, object_name: str, scalar_name: str):
607843 except Exception :
608844 applied = False
609845
846+ # If we didn't use plotter.update_scalars, use the centralized mapper update helper
610847 if not applied :
611- mapper = getattr (actor , 'mapper' , None )
612- if mapper is None :
613- raise RuntimeError ('Actor has no mapper to update' )
614- # try to select color array
615- try :
616- if hasattr (mapper , 'SelectColorArray' ):
617- mapper .SelectColorArray (scalars )
618- except Exception :
619- pass
620- try :
621- if hasattr (mapper , 'SetArrayName' ):
622- mapper .SetArrayName (scalars )
623- except Exception :
624- pass
625848 try :
626- if hasattr (mapper , 'scalar_visibility' ):
627- mapper .scalar_visibility = True
628- except Exception :
629- pass
630- try :
631- if hasattr (mapper , 'ScalarVisibilityOn' ):
632- mapper .ScalarVisibilityOn ()
633- except Exception :
634- pass
635- # set scalar range
636- try :
637- if self .range_min .text () and self .range_max .text ():
638- mn = float (self .range_min .text ())
639- mx = float (self .range_max .text ())
640- if hasattr (mapper , 'SetScalarRange' ):
641- mapper .SetScalarRange (mn , mx )
642- elif hasattr (mapper , 'scalar_range' ):
643- mapper .scalar_range = (mn , mx )
644- except Exception :
645- pass
646-
647- # best-effort to set colormap via plotter's actor/lookup table
648- try :
649- cmap = self .colormap_combo .currentText () or None
650- if cmap and hasattr (plotter , 'add_mesh' ):
651- # Changing lookup-table programmatically is backend/version dependent; try to trigger a re-render
849+ cmap = self .colormap_combo .currentText () or None
850+ clim = None
652851 try :
653- if hasattr ( plotter , 'render' ):
654- plotter . render ( )
852+ if self . range_min . text () and self . range_max . text ( ):
853+ clim = ( float ( self . range_min . text ()), float ( self . range_max . text ()) )
655854 except Exception :
656- pass
657- except Exception :
658- pass
659-
660- # update metadata
661- try :
662- kwargs = mesh_entry .get ('kwargs' , {}) if isinstance (mesh_entry , dict ) else {}
663- kwargs ['scalars' ] = scalars
664- kwargs ['cmap' ] = self .colormap_combo .currentText () or None
665- if self .range_min .text () and self .range_max .text ():
666- try :
667- kwargs ['clim' ] = (float (self .range_min .text ()), float (self .range_max .text ()))
668- except Exception :
669- kwargs ['clim' ] = None
670- mesh_entry ['kwargs' ] = kwargs
671- except Exception :
672- pass
673-
674- # request render
675- try :
676- if plotter is not None and hasattr (plotter , 'render' ):
677- plotter .render ()
678- except Exception :
679- pass
855+ clim = None
856+ self ._update_actor_mapper (mesh_entry , scalars , cmap , clim , values , actor , plotter )
857+ return
858+ except Exception :
859+ pass
0 commit comments