Source code for pyOMA.GUI.ChanDofEditorGUI
# SPDX-License-Identifier: GPL-3.0-or-later
"""Minimal PyQt6 dialog to define a channel-DOF assignment.
Wraps :meth:`~pyOMA.core.PreProcessingTools.PreProcessSignals.set_chan_dof` /
:meth:`~...remove_chan_dof`: a :class:`~pyOMA.core.PlotMSH.ModeShapePlot`
preview shows every existing channel-DOF assignment, with the one currently
being edited highlighted in a different color. OK commits the node/azimuth/
elevation currently shown, Cancel discards, Delete removes any existing
assignment for this channel.
Widget layout lives in ``ui/chan_dof_editor.ui`` (compiled to
``generated/ui_chan_dof_editor.py`` by ``scripts/build_ui.py``); this module
only wires signals/slots and holds the preview logic.
"""
import logging
from PyQt6.QtWidgets import QDialog, QMessageBox
from .generated.ui_chan_dof_editor import Ui_ChanDofEditorGUI
from ..core.PreProcessingTools import PreProcessSignals, GeometryProcessor
from ..core.PlotMSH import ModeShapePlot
logger = logging.getLogger(__name__)
_HIGHLIGHT_COLOR = 'red'
[docs]
class ChanDofEditorGUI(QDialog, Ui_ChanDofEditorGUI):
"""Dialog to add/edit/delete the DOF assignment of a single channel.
Parameters
----------
prep_signals : PreProcessSignals
Provides the channel to edit and the ``chan_dofs`` list mutated by
this dialog (via :meth:`~PreProcessSignals.set_chan_dof` /
:meth:`~PreProcessSignals.remove_chan_dof`).
geometry_data : GeometryProcessor
Supplies the nodes offered in the node dropdown and drawn in the
preview.
channel : int
Index of the channel being assigned.
parent : QWidget, optional
"""
[docs]
def __init__(self, prep_signals, geometry_data, channel, parent=None):
super().__init__(parent)
if not isinstance(prep_signals, PreProcessSignals):
raise TypeError(
f"prep_signals must be a PreProcessSignals instance, "
f"got {type(prep_signals).__name__}")
if not isinstance(geometry_data, GeometryProcessor):
raise TypeError(
f"geometry_data must be a GeometryProcessor instance, "
f"got {type(geometry_data).__name__}")
self.prep_signals = prep_signals
self.geometry_data = geometry_data
self.channel = channel
self.mode_shape_plot = ModeShapePlot(geometry_data, prep_signals=prep_signals)
# This channel's own (possibly pre-existing) assignment is redrawn
# separately below, highlighted, as the live-edited draft - drop it
# from the "other assignments" background context so it isn't drawn
# twice.
self.mode_shape_plot.chan_dofs = [
cd for cd in self.mode_shape_plot.chan_dofs if cd[0] != channel]
# Parent-child assignments are unrelated to channel-DOF assignment
# and would only clutter this preview.
self.mode_shape_plot.show_parent_childs = False
self.setupUi(self)
channel_name = prep_signals.channel_headers[channel]
self.setWindowTitle(f"DOF assignment: channel {channel}: {channel_name}")
self._wire_canvas()
self._wire_controls()
self._populate_node_combo()
self._load_existing_assignment()
self._redraw_preview()
# ------------------------------------------------------------------
# Wiring
# ------------------------------------------------------------------
def _wire_canvas(self):
fig = self.mode_shape_plot.fig
self.canvas.set_figure(fig)
self.mode_shape_plot.canvas = self.canvas
subplot = self.mode_shape_plot.subplot
self.canvas.mpl_connect('motion_notify_event', subplot._on_move)
self.canvas.mpl_connect('button_press_event', subplot._button_press)
self.canvas.mpl_connect('button_release_event', subplot._button_release)
subplot.mouse_init()
def _wire_controls(self):
self.combo_node.currentIndexChanged.connect(self._redraw_preview)
self.spin_az.valueChanged.connect(self._redraw_preview)
self.spin_elev.valueChanged.connect(self._redraw_preview)
self.btn_ok.clicked.connect(self._on_ok)
self.btn_cancel.clicked.connect(self.reject)
self.btn_delete.clicked.connect(self._on_delete)
def _populate_node_combo(self):
self.combo_node.addItems(sorted(self.geometry_data.nodes.keys()))
def _load_existing_assignment(self):
existing = self.prep_signals.get_chan_dof(self.channel)
self.btn_delete.setEnabled(existing is not None)
if existing is None:
return
node, az, elev = existing
self.combo_node.setCurrentText(node)
self.spin_az.setValue(az)
self.spin_elev.setValue(elev)
# ------------------------------------------------------------------
# Preview
# ------------------------------------------------------------------
def _redraw_preview(self):
# Full clear + redraw (nodes/lines/parent-childs/other channel-DOF
# assignments), mirroring GeometryProcessorGUI._redraw_geometry -
# reset_view() only ever adds artists, it never removes ones left
# over from a previous draw.
msh = self.mode_shape_plot
subplot = msh.subplot
subplot.cla()
subplot.set_aspect('equal', 'datalim')
subplot.patch = msh.fig.patch
subplot.grid(False)
subplot.set_axis_off()
subplot.mouse_init()
msh.patches_objects = {}
msh.lines_objects = []
msh.nd_lines_objects = []
msh.cn_lines_objects = {}
msh.arrows_objects = []
msh.channels_objects = []
msh.axis_obj = {}
msh.reset_view()
node = self.combo_node.currentText()
if node:
self._draw_highlighted_draft(node)
self.canvas.draw_idle()
def _draw_highlighted_draft(self, node):
msh = self.mode_shape_plot
az = self.spin_az.value()
elev = self.spin_elev.value()
channel_name = self.prep_signals.channel_headers[self.channel]
index = len(msh.channels_objects)
msh.add_chan_dof(self.channel, node, az, elev, channel_name, index)
arrow = msh.channels_objects[index]
arrow.set_color(_HIGHLIGHT_COLOR)
if arrow.text is not None:
arrow.text.set_color(_HIGHLIGHT_COLOR)
# ------------------------------------------------------------------
# Actions
# ------------------------------------------------------------------
def _on_ok(self):
node = self.combo_node.currentText()
if not node:
QMessageBox.warning(self, "DOF assignment", "Select a node first.")
return
self.prep_signals.set_chan_dof(
self.channel, node, self.spin_az.value(), self.spin_elev.value())
self.accept()
def _on_delete(self):
self.prep_signals.remove_chan_dof(self.channel)
self.accept()