Skip to content

Commit 352f5b1

Browse files
committed
fix: apply colourmap change and refactored actor/mapper update into single function
1 parent 8124f36 commit 352f5b1

1 file changed

Lines changed: 248 additions & 68 deletions

File tree

loopstructural/gui/visualisation/object_properties_widget.py

Lines changed: 248 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)