class VTKImportPlugin(QtCore.QObject):
"""
Plugin “VTK Import & Display”.
Structure:
- Menu action + shortcut for import
- Display control box, applied to the current dataset
- Best effort detection of time-series via PyVista reader
"""
def __init__(self, window):
super().__init__(window)
self.window = window
# UI state (for current dataset - updates on each selection in the tree)
self._current_ds: Optional[int] = None
self._time_values: Optional[List[float]] = None
self._time_idx: int = 0
# ----- Menu & shortcuts --------------------------------------
self._action_import = QtGui.QAction(QtGui.QIcon(), "Import VTK…", self)
# Shortcut: ⌘⇧I (mac) / Ctrl+Shift+I (others)
self._action_import.setShortcut(QtGui.QKeySequence("Ctrl+Shift+I"))
# Note: Qt automatically adjusts shortcuts on macOS to use Cmd based on the platform shortcut context
self._action_import.triggered.connect(self.open_dialog)
# Aggiungi in File
try:
mb = window.menuBar()
for a in mb.actions():
if a.text().replace("&", "") == "File":
a.menu().addAction(self._action_import)
break
except Exception:
pass
# ----- Box UI in DisplayPanel ---------------------------------
self._panel = self._build_display_box()
_add_to_display_panel(window, "VTK Display", self._panel)
# Keep the controls synchronized with the selected dataset
self.window.treeMCTS.itemSelectionChanged.connect(self._on_tree_selection_changed)
try:
window.treeMCTS.itemSelectionChanged.connect(self._on_tree_selection_changed)
except Exception:
pass
# -----------------------------------------------------------------
# DIALOG DI IMPORT
# -----------------------------------------------------------------
@QtCore.Slot()
def open_dialog(self):
"""
Dialog to select a SINGLE file and import it using PyVista.
Handles MultiBlock → SINGLE actor.
Detects (if present) a time-series and exposes a slider.
"""
dlg = QtWidgets.QFileDialog(self.window, "Import VTK")
dlg.setFileMode(QtWidgets.QFileDialog.ExistingFile)
dlg.setNameFilters([
"All supported (*.vtk *.vtp *.vtu *.vtr *.vts *.vtm *.vti *.obj *.stl)",
"VTK legacy (*.vtk)",
"VTK XML PolyData (*.vtp)",
"VTK XML UnstructuredGrid (*.vtu)",
"VTK XML RectilinearGrid (*.vtr)",
"VTK XML StructuredGrid (*.vts)",
"VTK XML MultiBlock (*.vtm)",
"VTK ImageData (*.vti)",
"Meshes (*.obj *.stl)",
"All files (*)",
])
if not dlg.exec():
return
paths = dlg.selectedFiles()
if not paths:
return
path = paths[0]
try:
reader = pv.get_reader(path)
except Exception as ex:
self._msg(f"[VTK] Reader error: {ex}", error=True)
return
# Try to detect time values
self._time_values = None
try:
tvals = getattr(reader, "time_values", None)
if tvals is not None and len(tvals) > 0:
self._time_values = list(tvals)
reader.set_active_time_value(self._time_values[0])
except Exception:
self._time_values = None
try:
data = reader.read()
except Exception as ex:
self._msg(f"[VTK] Read error: {ex}", error=True)
return
# MultiBlock → unico attore
dataset_to_add = data
try:
if isinstance(data, pv.MultiBlock):
# If you really want *a single actor*, you can render the MultiBlock directly
# (PyVista internally handles the blocks with a single composite mapper/actor).
dataset_to_add = data
except Exception:
pass
# Add to viewer
try:
ds_index = self._add_dataset_to_viewer(dataset_to_add, path)
except Exception as ex:
self._msg(f"[VTK] Viewer add failed: {ex}", error=True)
return
# Fit camera
try:
self.window.viewer3d.view_fit()
except Exception:
pass
# Select the new dataset in the tree
try:
self._select_tree_item_for_ds(ds_index)
except Exception:
pass
# Show/update time slider if needed
self._sync_time_slider_visibility()
# Update Inspector
try:
self.window._refresh_inspector_tree()
except Exception:
pass
self._msg(f"[VTK] Imported: {os.path.basename(path)}")
def _add_dataset_to_viewer(self, data, path: str) -> int:
"""
Aggiunge il dataset PyVista al Viewer3D e registra in mcts.
Ritorna l'indice di dataset (ds_index).
"""
name = os.path.splitext(os.path.basename(path))[0]
# Prefer dedicated APIs if available
ds_index = None
try:
if hasattr(self.window.viewer3d, "add_pyvista_mesh"):
ds_index = self.window.viewer3d.add_pyvista_mesh(data)
else:
# Generic fallback (used if add_pyvista_mesh is not available)
actor = self.window.viewer3d.plotter.add_mesh(data, name=name)
# Manual registration in viewer3d._datasets if necessary
# Assuming the official API does not handle this automatically
if not hasattr(self.window.viewer3d, "_datasets"):
self.window.viewer3d._datasets = []
ds_index = len(self.window.viewer3d._datasets)
self.window.viewer3d._datasets.append({"mesh": data, "actor": actor})
raise RuntimeError("Viewer3D.add_pyvista_mesh not available; used fallback registration.")
except Exception as ex:
raise
# Register in mcts (new instance always at the end of the list)
entry = {
"name": name,
"kind": "mesh", # PolyData / Grid / MultiBlock → keep as "mesh"
"ds_index": ds_index,
"source_path": path, # automatic reopening
# default initial style
"representation": "Surface",
"opacity": 100,
"color_mode": "Solid Color",
"solid_color": (255, 255, 255),
"colormap": "Viridis",
"scalar_bar": False,
"edge_visibility": False,
"edge_color": (0, 0, 0),
"point_size": 3,
"line_width": 1,
"lighting": True,
}
self.window.mcts[name] = entry
self.window.mct = entry # becomes "current"
# Create tree node if needed (reuse MainWindow pipeline if available)
try:
# If the official MainWindow import already builds the tree, you might skip this part.
# Here we construct a minimal node as an example:
self.window.treeMCTS.blockSignals(True)
root = QtWidgets.QTreeWidgetItem([name])
root.setFlags(root.flags() | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsAutoTristate)
root.setCheckState(0, QtCore.Qt.Checked)
# metadata sul nodo root
root.setData(0, QtCore.Qt.UserRole, {"kind": "mesh", "ds": ds_index})
# Aggiungi albero “Mesh”
it_mesh = QtWidgets.QTreeWidgetItem(["Mesh"])
it_mesh.setFlags(it_mesh.flags() | QtCore.Qt.ItemIsUserCheckable)
it_mesh.setCheckState(0, QtCore.Qt.Checked)
it_mesh.setData(0, QtCore.Qt.UserRole, {"kind": "mesh", "ds": ds_index})
root.addChild(it_mesh)
self.window.treeMCTS.addTopLevelItem(root)
self.window.treeMCTS.blockSignals(False)
except Exception:
pass
return ds_index
# -----------------------------------------------------------------
# BOX CONTROLS (ParaView, LIVE application)
# -----------------------------------------------------------------
def _build_display_box(self) -> QtWidgets.QWidget:
w = QtWidgets.QWidget()
w.setMaximumWidth(300)
w.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum)
lay = QtWidgets.QFormLayout(w)
lay.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
lay.setHorizontalSpacing(6)
lay.setVerticalSpacing(6)
lay.setContentsMargins(6, 6, 6, 6)
# Representation
self.cmbRep = QtWidgets.QComboBox()
self.cmbRep.addItems(["Points", "Wireframe", "Surface", "Surface with Edges", "Volume"])
self.cmbRep.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.cmbRep.currentTextChanged.connect(self._on_rep_changed)
lay.addRow("Representation", self.cmbRep)
# Color By
self.cmbColorBy = QtWidgets.QComboBox()
self.cmbColorBy.setMinimumWidth(120)
self.cmbColorBy.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.cmbColorBy.currentTextChanged.connect(self._on_color_by_changed)
lay.addRow("Color by", self.cmbColorBy)
# Vector component
self.cmbVectorMode = QtWidgets.QComboBox()
self.cmbVectorMode.addItems(["Magnitude", "X", "Y", "Z"])
self.cmbVectorMode.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.cmbVectorMode.currentTextChanged.connect(self._on_color_by_changed)
lay.addRow("Vector component", self.cmbVectorMode)
# LUT
self.cmbLUT = QtWidgets.QComboBox()
self.cmbLUT.addItems(["Viridis", "Plasma", "CoolWarm", "Gray", "Jet"])
self.cmbLUT.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.cmbLUT.currentTextChanged.connect(self._on_color_by_changed)
lay.addRow("LUT", self.cmbLUT)
# Invert LUT
self.chkInvertLUT = QtWidgets.QCheckBox("Invert")
self.chkInvertLUT.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.chkInvertLUT.toggled.connect(self._on_color_by_changed)
lay.addRow("", self.chkInvertLUT)
# Solid Color button
self.btnSolid = _solid_color_button()
self.btnSolid.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.btnSolid.clicked.connect(self._on_pick_solid_color)
lay.addRow(self.btnSolid)
# Scalar range with min/max and buttons
rngw = QtWidgets.QWidget()
rngw.setMaximumWidth(260)
rngw.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
rngLay = QtWidgets.QVBoxLayout(rngw)
rngLay.setContentsMargins(0,0,0,0)
rngLay.setSpacing(4)
frm = QtWidgets.QFormLayout()
frm.setContentsMargins(0,0,0,0)
frm.setSpacing(4)
self.editMin = QtWidgets.QLineEdit()
self.editMin.setPlaceholderText("min")
self.editMin.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.editMax = QtWidgets.QLineEdit()
self.editMax.setPlaceholderText("max")
self.editMax.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
frm.addRow("Min", self.editMin)
frm.addRow("Max", self.editMax)
rngLay.addLayout(frm)
btnRow = QtWidgets.QHBoxLayout()
btnRow.setSpacing(4)
self.btnAuto = QtWidgets.QPushButton("Auto")
self.btnAuto.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.btnAuto.clicked.connect(self._on_range_auto)
self.btnRescale = QtWidgets.QPushButton("Rescale")
self.btnRescale.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.btnRescale.clicked.connect(self._on_rescale_to_data)
btnRow.addWidget(self.btnAuto)
btnRow.addWidget(self.btnRescale)
rngLay.addLayout(btnRow)
lay.addRow("Scalar range", rngw)
# Scalar bar
self.chkScalarBar = QtWidgets.QCheckBox("Show scalar bar")
self.chkScalarBar.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.chkScalarBar.toggled.connect(self._on_scalar_bar_toggle)
lay.addRow(self.chkScalarBar)
# Opacity slider
self.sldOpacity = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.sldOpacity.setRange(0, 100)
self.sldOpacity.setValue(100)
self.sldOpacity.setMaximumWidth(260)
self.sldOpacity.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.sldOpacity.valueChanged.connect(self._on_opacity_changed)
lay.addRow("Opacity", self.sldOpacity)
# Point size slider
self.sldPointSize = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.sldPointSize.setRange(1, 15)
self.sldPointSize.setValue(3)
self.sldPointSize.setMaximumWidth(260)
self.sldPointSize.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.sldPointSize.valueChanged.connect(self._on_point_size_changed)
lay.addRow("Point size", self.sldPointSize)
# Line width slider
self.sldLineWidth = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.sldLineWidth.setRange(1, 10)
self.sldLineWidth.setValue(1)
self.sldLineWidth.setMaximumWidth(260)
self.sldLineWidth.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.sldLineWidth.valueChanged.connect(self._on_line_width_changed)
lay.addRow("Line width", self.sldLineWidth)
# Edges visible
self.chkEdges = QtWidgets.QCheckBox("Edges visible")
self.chkEdges.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.chkEdges.toggled.connect(self._on_edges_toggle)
lay.addRow(self.chkEdges)
# Edge color button
self.btnEdgeColor = QtWidgets.QPushButton("Edge color…")
self.btnEdgeColor.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.btnEdgeColor.clicked.connect(self._on_pick_edge_color)
lay.addRow(self.btnEdgeColor)
# Lighting
self.chkLighting = QtWidgets.QCheckBox("Lighting")
self.chkLighting.setChecked(True)
self.chkLighting.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.chkLighting.toggled.connect(self._on_lighting_toggle)
lay.addRow(self.chkLighting)
# Time-series group
self.grpTime = QtWidgets.QGroupBox("Time")
self.grpTime.setMaximumWidth(300)
time_lay = QtWidgets.QVBoxLayout(self.grpTime)
self.sldTime = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.sldTime.setRange(0, 0)
self.sldTime.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.sldTime.valueChanged.connect(self._on_time_changed)
self.lblTime = QtWidgets.QLabel("—")
self.lblTime.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.lblTime.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
time_lay.addWidget(self.sldTime)
time_lay.addWidget(self.lblTime)
self.grpTime.setVisible(False)
lay.addRow(self.grpTime)
# Reset button
self.btnReset = QtWidgets.QPushButton("Reset defaults")
self.btnReset.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.btnReset.clicked.connect(self._on_reset_defaults)
lay.addRow(self.btnReset)
return w
# -----------------------------------------------------------------
# SYNC UI with current dataset / MCT entry
# -----------------------------------------------------------------
def _on_tree_selection_changed(self):
ds = self._current_dataset_index()
self._current_ds = ds
self._rebuild_colorby_combo()
# carica stato dal mct (se presente)
entry = self._current_mct()
if entry:
self._load_ui_from_entry(entry)
def _current_dataset_index(self) -> Optional[int]:
try:
return self.window._current_dataset_index()
except Exception:
return None
def _current_mct(self) -> Optional[dict]:
try:
ds = self._current_dataset_index()
for e in self.window.mcts.values():
if e.get("ds_index") == ds:
return e
return self.window.mct if self.window.mct.get("ds_index") == ds else None
except Exception:
return None
def _select_tree_item_for_ds(self, ds_index: int) -> None:
"""Try to select the root item in the tree with ds=ds_index."""
t = self.window.treeMCTS
for i in range(t.topLevelItemCount()):
root = t.topLevelItem(i)
data = root.data(0, QtCore.Qt.UserRole)
if isinstance(data, dict) and data.get("ds") == ds_index:
t.setCurrentItem(root)
break
def _sync_time_slider_visibility(self) -> None:
has_time = bool(self._time_values) and len(self._time_values) > 1
self.grpTime.setVisible(has_time)
if has_time:
self.sldTime.blockSignals(True)
self.sldTime.setRange(0, len(self._time_values) - 1)
self.sldTime.setValue(0)
self.sldTime.blockSignals(False)
self.lblTime.setText(f"t = {self._time_values[0]:.6g}")
# -----------------------------------------------------------------
# UI EVENTS → APPLY
# -----------------------------------------------------------------
def _on_rep_changed(self, mode: str):
ds = self._current_dataset_index()
if ds is None: return
# Mappatura semplice
self._apply_representation(ds, mode)
self._save_to_mct("representation", mode)
def _on_color_by_changed(self):
ds = self._current_dataset_index()
if ds is None: return
label = self.cmbColorBy.currentText()
vec_mode = self.cmbVectorMode.currentText()
lut = self.cmbLUT.currentText()
invert = self.chkInvertLUT.isChecked()
self._apply_coloring(ds, label, vec_mode, lut, invert)
self._save_to_mct("color_mode", label)
self._save_to_mct("colormap", lut)
self._save_to_mct("invert_lut", bool(invert))
self._save_to_mct("vector_mode", vec_mode)
def _on_pick_solid_color(self):
col = QtWidgets.QColorDialog.getColor(parent=self.window, title="Solid Color")
if not col.isValid(): return
ds = self._current_dataset_index()
if ds is None: return
self._apply_solid_color(ds, (col.red(), col.green(), col.blue()))
self._save_to_mct("color_mode", "Solid Color")
self._save_to_mct("solid_color", (col.red(), col.green(), col.blue()))
# Force combo box to "Solid Color"
self.cmbColorBy.blockSignals(True)
self.cmbColorBy.setCurrentText("Solid Color")
self.cmbColorBy.blockSignals(False)
def _on_range_auto(self):
self.editMin.clear(); self.editMax.clear()
self._on_color_by_changed() # re-apply with auto-range
def _on_rescale_to_data(self):
# Re-apply colormap asking the viewer to use data range
self._on_color_by_changed()
def _on_scalar_bar_toggle(self, on: bool):
ds = self._current_dataset_index()
if ds is None: return
self._apply_scalar_bar(ds, on)
self._save_to_mct("scalar_bar", bool(on))
def _on_opacity_changed(self, val: int):
ds = self._current_dataset_index()
if ds is None: return
self._apply_opacity(ds, val)
self._save_to_mct("opacity", int(val))
def _on_point_size_changed(self, val: int):
ds = self._current_dataset_index()
if ds is None: return
self._apply_point_size(ds, val)
self._save_to_mct("point_size", int(val))
def _on_line_width_changed(self, val: int):
ds = self._current_dataset_index()
if ds is None: return
self._apply_line_width(ds, val)
self._save_to_mct("line_width", int(val))
def _on_edges_toggle(self, on: bool):
ds = self._current_dataset_index()
if ds is None: return
self._apply_edges(ds, bool(on))
self._save_to_mct("edge_visibility", bool(on))
def _on_pick_edge_color(self):
col = QtWidgets.QColorDialog.getColor(parent=self.window, title="Edge Color")
if not col.isValid(): return
ds = self._current_dataset_index()
if ds is None: return
self._apply_edge_color(ds, (col.red(), col.green(), col.blue()))
self._save_to_mct("edge_color", (col.red(), col.green(), col.blue()))
def _on_lighting_toggle(self, on: bool):
ds = self._current_dataset_index()
if ds is None: return
self._apply_lighting(ds, bool(on))
self._save_to_mct("lighting", bool(on))
def _on_time_changed(self, idx: int):
if not self._time_values: return
self._time_idx = int(idx)
t = self._time_values[self._time_idx]
self.lblTime.setText(f"t = {t:.6g}")
# Rileggi il dataset al tempo selezionato
# NOTE: servirebbe conservare il reader in self; per semplicità omesso.
# TODO: estendere per ricaricare dal reader e aggiornare l'attore.
def _on_reset_defaults(self):
# Reset UI
self.cmbRep.setCurrentText("Surface")
self.cmbColorBy.setCurrentText("Solid Color")
self.cmbVectorMode.setCurrentText("Magnitude")
self.cmbLUT.setCurrentText("Viridis")
self.chkInvertLUT.setChecked(False)
self.chkScalarBar.setChecked(False)
self.sldOpacity.setValue(100)
self.sldPointSize.setValue(3)
self.sldLineWidth.setValue(1)
self.chkEdges.setChecked(False)
self.chkLighting.setChecked(True)
# Applica allo stato corrente
self._on_rep_changed("Surface")
self._on_color_by_changed()
self._on_scalar_bar_toggle(False)
self._on_opacity_changed(100)
self._on_point_size_changed(3)
self._on_line_width_changed(1)
self._on_edges_toggle(False)
self._on_lighting_toggle(True)
# -----------------------------------------------------------------
# APPLY (adapter using the viewer's API if available, else fallback)
# -----------------------------------------------------------------
def _apply_representation(self, ds: int, mode: str):
"""
Map of representations:
- Points, Wireframe, Surface, Surface with Edges, Volume
"""
# Se il tuo Viewer3D espone un metodo diretto:
fn = getattr(self.window.viewer3d, "set_mesh_representation", None)
if callable(fn):
fn(ds, mode)
return
# Fallback: re-render via PyVista
e = self._current_mct() or {}
_fallback_render(
self.window, e, ds,
representation=mode,
manual_range=self._manual_range_or_none(),
color_mode=e.get("color_mode", "Solid Color"),
lut=e.get("colormap", "Viridis"),
invert=self.chkInvertLUT.isChecked(),
scalar_bar=self.chkScalarBar.isChecked(),
edge_visibility=self.chkEdges.isChecked(),
edge_color=e.get("edge_color", (0, 0, 0)),
opacity=self.sldOpacity.value(),
point_size=self.sldPointSize.value(),
line_width=self.sldLineWidth.value(),
lighting=self.chkLighting.isChecked(),
vector_mode=self.cmbVectorMode.currentText(),
)
def _apply_solid_color(self, ds: int, rgb: Tuple[int, int, int]):
fn = getattr(self.window.viewer3d, "set_dataset_color", None)
if callable(fn):
fn(ds, *rgb)
return
e = self._current_mct() or {}
_fallback_render(
self.window, e, ds,
color_mode="Solid Color",
solid_color=rgb,
representation=e.get("representation", "Surface"),
lut=e.get("colormap", "Viridis"),
invert=self.chkInvertLUT.isChecked(),
scalar_bar=self.chkScalarBar.isChecked(),
edge_visibility=self.chkEdges.isChecked(),
edge_color=e.get("edge_color", (0, 0, 0)),
opacity=self.sldOpacity.value(),
point_size=self.sldPointSize.value(),
line_width=self.sldLineWidth.value(),
lighting=self.chkLighting.isChecked(),
manual_range=self._manual_range_or_none(),
vector_mode=self.cmbVectorMode.currentText(),
)
def _apply_coloring(self, ds: int, label: str, vec_mode: str, lut: str, invert: bool):
"""
label = "Solid Color" oppure "PointData/<array>" o "CellData/<array>"
vec_mode = Magnitude / X / Y / Z
"""
# Caso Solid Color → forza colore uniforme
if label == "Solid Color":
self._apply_solid_color(ds, self._current_mct().get("solid_color", (255, 255, 255)))
# Se il viewer ha un “color mode”, impostalo
try:
self.window.viewer3d.set_color_mode("Solid Color", ds)
except Exception:
pass
return
# Parsing “PointData/NAME” o “CellData/NAME”
assoc = "POINT"
array_name = label
if label.startswith("PointData/"):
assoc = "POINT"
array_name = label.split("/", 1)[1]
elif label.startswith("CellData/"):
assoc = "CELL"
array_name = label.split("/", 1)[1]
# Viewer API personalizzata (se esiste):
# Immaginiamo una API del tipo: set_scalar_coloring(ds, array_name, assoc, component, lut, invert, range)
fn = getattr(self.window.viewer3d, "set_scalar_coloring", None)
rng = self._manual_range_or_none()
component = {"Magnitude": None, "X": 0, "Y": 1, "Z": 2}.get(vec_mode, None)
if callable(fn):
fn(ds, array_name, assoc, component, lut, bool(invert), rng)
return
e = self._current_mct() or {}
_fallback_render(
self.window, e, ds,
color_mode=label,
lut=lut,
invert=bool(invert),
representation=e.get("representation", "Surface"),
scalar_bar=self.chkScalarBar.isChecked(),
edge_visibility=self.chkEdges.isChecked(),
edge_color=e.get("edge_color", (0, 0, 0)),
opacity=self.sldOpacity.value(),
point_size=self.sldPointSize.value(),
line_width=self.sldLineWidth.value(),
lighting=self.chkLighting.isChecked(),
manual_range=self._manual_range_or_none(),
vector_mode=self.cmbVectorMode.currentText(),
)
def _manual_range_or_none(self) -> Optional[Tuple[float, float]]:
try:
smin = self.editMin.text().strip()
smax = self.editMax.text().strip()
if not smin or not smax:
return None
return (float(smin), float(smax))
except Exception:
return None
def _apply_scalar_bar(self, ds: int, show: bool):
fn = getattr(self.window.viewer3d, "set_scalar_bar_visible", None)
if callable(fn):
fn(ds, bool(show))
return
e = self._current_mct() or {}
_fallback_render(
self.window, e, ds,
scalar_bar=bool(show),
representation=e.get("representation", "Surface"),
color_mode=e.get("color_mode", "Solid Color"),
lut=e.get("colormap", "Viridis"),
invert=self.chkInvertLUT.isChecked(),
edge_visibility=self.chkEdges.isChecked(),
edge_color=e.get("edge_color", (0, 0, 0)),
opacity=self.sldOpacity.value(),
point_size=self.sldPointSize.value(),
line_width=self.sldLineWidth.value(),
lighting=self.chkLighting.isChecked(),
manual_range=self._manual_range_or_none(),
vector_mode=self.cmbVectorMode.currentText(),
)
def _apply_opacity(self, ds: int, val: int):
fn = getattr(self.window.viewer3d, "set_mesh_opacity", None)
if callable(fn):
fn(ds, int(val))
return
e = self._current_mct() or {}
_fallback_render(
self.window, e, ds,
opacity=int(val),
representation=e.get("representation", "Surface"),
color_mode=e.get("color_mode", "Solid Color"),
lut=e.get("colormap", "Viridis"),
invert=self.chkInvertLUT.isChecked(),
scalar_bar=self.chkScalarBar.isChecked(),
edge_visibility=self.chkEdges.isChecked(),
edge_color=e.get("edge_color", (0, 0, 0)),
point_size=self.sldPointSize.value(),
line_width=self.sldLineWidth.value(),
lighting=self.chkLighting.isChecked(),
manual_range=self._manual_range_or_none(),
vector_mode=self.cmbVectorMode.currentText(),
)
def _apply_point_size(self, ds: int, val: int):
fn = getattr(self.window.viewer3d, "set_point_size", None)
if callable(fn):
fn(int(val), ds)
return
e = self._current_mct() or {}
_fallback_render(
self.window, e, ds,
point_size=int(val),
representation=e.get("representation", "Surface"),
color_mode=e.get("color_mode", "Solid Color"),
lut=e.get("colormap", "Viridis"),
invert=self.chkInvertLUT.isChecked(),
scalar_bar=self.chkScalarBar.isChecked(),
edge_visibility=self.chkEdges.isChecked(),
edge_color=e.get("edge_color", (0, 0, 0)),
opacity=self.sldOpacity.value(),
line_width=self.sldLineWidth.value(),
lighting=self.chkLighting.isChecked(),
manual_range=self._manual_range_or_none(),
vector_mode=self.cmbVectorMode.currentText(),
)
def _apply_line_width(self, ds: int, val: int):
fn = getattr(self.window.viewer3d, "set_line_width", None)
if callable(fn):
fn(ds, int(val))
return
e = self._current_mct() or {}
_fallback_render(
self.window, e, ds,
line_width=int(val),
representation=e.get("representation", "Surface"),
color_mode=e.get("color_mode", "Solid Color"),
lut=e.get("colormap", "Viridis"),
invert=self.chkInvertLUT.isChecked(),
scalar_bar=self.chkScalarBar.isChecked(),
edge_visibility=self.chkEdges.isChecked(),
edge_color=e.get("edge_color", (0, 0, 0)),
opacity=self.sldOpacity.value(),
point_size=self.sldPointSize.value(),
lighting=self.chkLighting.isChecked(),
manual_range=self._manual_range_or_none(),
vector_mode=self.cmbVectorMode.currentText(),
)
def _apply_edges(self, ds: int, on: bool):
fn = getattr(self.window.viewer3d, "set_edge_visibility", None)
if callable(fn):
fn(ds, bool(on))
return
e = self._current_mct() or {}
_fallback_render(
self.window, e, ds,
edge_visibility=bool(on),
representation=e.get("representation", "Surface"),
color_mode=e.get("color_mode", "Solid Color"),
lut=e.get("colormap", "Viridis"),
invert=self.chkInvertLUT.isChecked(),
scalar_bar=self.chkScalarBar.isChecked(),
edge_color=e.get("edge_color", (0, 0, 0)),
opacity=self.sldOpacity.value(),
point_size=self.sldPointSize.value(),
line_width=self.sldLineWidth.value(),
lighting=self.chkLighting.isChecked(),
manual_range=self._manual_range_or_none(),
vector_mode=self.cmbVectorMode.currentText(),
)
def _apply_edge_color(self, ds: int, rgb: Tuple[int, int, int]):
fn = getattr(self.window.viewer3d, "set_edge_color", None)
if callable(fn):
fn(ds, *rgb)
return
e = self._current_mct() or {}
_fallback_render(
self.window, e, ds,
edge_color=rgb,
representation=e.get("representation", "Surface"),
color_mode=e.get("color_mode", "Solid Color"),
lut=e.get("colormap", "Viridis"),
invert=self.chkInvertLUT.isChecked(),
scalar_bar=self.chkScalarBar.isChecked(),
edge_visibility=self.chkEdges.isChecked(),
opacity=self.sldOpacity.value(),
point_size=self.sldPointSize.value(),
line_width=self.sldLineWidth.value(),
lighting=self.chkLighting.isChecked(),
manual_range=self._manual_range_or_none(),
vector_mode=self.cmbVectorMode.currentText(),
)
def _apply_lighting(self, ds: int, on: bool):
fn = getattr(self.window.viewer3d, "set_lighting_enabled", None)
if callable(fn):
fn(ds, bool(on))
return
e = self._current_mct() or {}
_fallback_render(
self.window, e, ds,
lighting=bool(on),
representation=e.get("representation", "Surface"),
color_mode=e.get("color_mode", "Solid Color"),
lut=e.get("colormap", "Viridis"),
invert=self.chkInvertLUT.isChecked(),
scalar_bar=self.chkScalarBar.isChecked(),
edge_visibility=self.chkEdges.isChecked(),
edge_color=e.get("edge_color", (0, 0, 0)),
opacity=self.sldOpacity.value(),
point_size=self.sldPointSize.value(),
line_width=self.sldLineWidth.value(),
manual_range=self._manual_range_or_none(),
vector_mode=self.cmbVectorMode.currentText(),
)
# -----------------------------------------------------------------
# COMBO BOX "Color by" REBUILD
# -----------------------------------------------------------------
def _rebuild_colorby_combo(self):
"""Reads arrays from the current PolyData/mesh and populates the “Color by” combo box."""
self.cmbColorBy.blockSignals(True)
self.cmbColorBy.clear()
self.cmbColorBy.addItem("Solid Color")
ds = self._current_dataset_index()
if ds is None:
self.cmbColorBy.blockSignals(False)
return
# if possible, retrieve arrays from the original dataset
arrays_pt, arrays_cell = [], []
try:
recs = getattr(self.window.viewer3d, "_datasets", [])
rec = recs[ds]
# Prefer original dataset for listing arrays (more complete), fall back to surface
pdata = rec.get("mesh_orig") or rec.get("mesh") or rec.get("pdata") or rec.get("full_pdata")
surf = rec.get("mesh_surface") or rec.get("mesh") or rec.get("pdata") or rec.get("full_pdata")
target = pdata or surf
if isinstance(target, pv.MultiBlock):
try:
target = target[0]
except Exception:
target = None
rec["mesh"] = target
if target is not None:
# PointData
try:
for name in list(target.point_data.keys()):
if str(name).startswith("vtkOriginal"):
continue
arrays_pt.append(str(name))
except Exception:
pass
# CellData
try:
for name in list(target.cell_data.keys()):
if str(name).startswith("vtkOriginal"):
continue
arrays_cell.append(str(name))
except Exception:
pass
except Exception:
pass
if arrays_pt:
for n in arrays_pt:
self.cmbColorBy.addItem(f"PointData/{n}")
if arrays_cell:
for n in arrays_cell:
self.cmbColorBy.addItem(f"CellData/{n}")
self.cmbColorBy.blockSignals(False)
def _load_ui_from_entry(self, e: dict) -> None:
"""Load (best effort) controls from persisted values in the mct entry."""
try: self.cmbRep.setCurrentText(e.get("representation", "Surface"))
except Exception: pass
try: self.cmbColorBy.setCurrentText(e.get("color_mode", "Solid Color"))
except Exception: pass
try: self.cmbLUT.setCurrentText(e.get("colormap", "Viridis"))
except Exception: pass
try: self.chkScalarBar.setChecked(bool(e.get("scalar_bar", False)))
except Exception: pass
try: self.sldOpacity.setValue(int(e.get("opacity", 100)))
except Exception: pass
try: self.sldPointSize.setValue(int(e.get("point_size", 3)))
except Exception: pass
try: self.sldLineWidth.setValue(int(e.get("line_width", 1)))
except Exception: pass
try: self.chkEdges.setChecked(bool(e.get("edge_visibility", False)))
except Exception: pass
try: self.chkLighting.setChecked(bool(e.get("lighting", True)))
except Exception: pass
def _save_to_mct(self, key: str, val):
e = self._current_mct()
if e is not None:
e[key] = val
# -----------------------------------------------------------------
# LOGGING/UTILITY
# -----------------------------------------------------------------
def _msg(self, text: str, error: bool = False):
# Write to status bar + message panel (if available)
try:
self.window.statusBar().showMessage(text, 5000)
except Exception:
pass
try:
self.window.txtMessages.appendPlainText(text)
except Exception:
pass
if error:
print(text)