收紧绘图设置为两页,并把 FN 牛顿下限固定为 0。

手指 2D 方向改到 ArrowPanel 设置对话框;归零阈值不变。已用 display_prefs、finger_2d_orientation 与绘图对话框相关 smoke 测试验证。
This commit is contained in:
ykcheungab
2026-10-07 20:48:48 +08:00
parent ff8288bcf9
commit 92e6de6913
10 changed files with 194 additions and 111 deletions
@@ -17,17 +17,19 @@ from orisys_app_shared.acquisition.image_backend import (
ImagePlan,
)
from orisys_app_shared.i18n import t
from PySide6.QtCore import Qt, Signal
from PySide6.QtCore import QSize, Qt, Signal
from PySide6.QtGui import QImage, QPainter, QPixmap, QTransform
from PySide6.QtWidgets import (
QComboBox,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QVBoxLayout,
)
from orisys_studio.ui import icons
from orisys_studio.ui.fit_text import ShrinkableLabel
VIEW_MODES = (ARROW_PURE, ARROW_STITCHED, DEPTH)
@@ -149,6 +151,8 @@ class ColorBarLabel(QLabel):
class ArrowPanel(QFrame):
view_mode_changed = Signal(str)
#: Finger 2D presentation (orientation). The window opens the small dialog.
settings_requested = Signal()
def __init__(self, parent=None, *, theme: str = "dark") -> None:
super().__init__(parent)
@@ -178,9 +182,15 @@ class ArrowPanel(QFrame):
self.view_combo = QComboBox()
self.view_combo.setMinimumWidth(140)
self.view_combo.currentIndexChanged.connect(self._on_view_index_changed)
self.btn_settings = QPushButton()
self.btn_settings.setProperty("compact", "true")
self.btn_settings.setProperty("iconOnly", "true")
self.btn_settings.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_settings.clicked.connect(self.settings_requested.emit)
header.addWidget(self.title)
header.addStretch(1)
header.addWidget(self.view_combo)
header.addWidget(self.btn_settings)
layout.addLayout(header)
self.image_label = AspectImageLabel()
@@ -197,6 +207,7 @@ class ArrowPanel(QFrame):
self._placeholder = ""
self._populate_view_combo()
self._apply_icons()
self.retranslate()
self._refresh_footer_visibility()
self._sync_aspect()
@@ -382,6 +393,12 @@ class ArrowPanel(QFrame):
def set_theme(self, theme: str) -> None:
self._theme = str(theme)
self.colorbar.set_theme(self._theme)
self._apply_icons()
def _apply_icons(self) -> None:
size = QSize(icons.ICON_SIZE, icons.ICON_SIZE)
self.btn_settings.setIcon(icons.icon("settings", self._theme))
self.btn_settings.setIconSize(size)
def set_depth_scale(
self, text: str | None, *, tip_key: str = "panels.depth_scale_tip"
@@ -458,6 +475,7 @@ class ArrowPanel(QFrame):
if i < self.view_combo.count():
self.view_combo.setItemText(i, t(_VIEW_LABELS.get(mode, mode)))
self.view_combo.setToolTip(t("panels.view_mode_tip"))
self.btn_settings.setToolTip(t("panels.arrow_settings_tip"))
self.image_label.setToolTip(t("panels.arrow_empty_tip"))
self._refresh_pipeline_label()
self._refresh_scale_label()
@@ -0,0 +1,80 @@
"""Finger 2D canvas presentation: how the unrolled domain is shown.
Display only — the record and stored arrays keep the domain's own orientation.
The 3D view is untouched (its frame is the design export's).
"""
from __future__ import annotations
from orisys_app_shared.acquisition.display_orientation import (
DEFAULT_FINGER_ORIENTATION,
ORIENTATIONS,
normalise_orientation,
)
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import (
QComboBox,
QDialog,
QDialogButtonBox,
QFormLayout,
QLabel,
QVBoxLayout,
)
class FingerOrientationDialog(QDialog):
def __init__(
self,
parent=None,
*,
finger_orientation: str = DEFAULT_FINGER_ORIENTATION,
) -> None:
super().__init__(parent)
self.setModal(True)
self.resize(420, 200)
root = QVBoxLayout(self)
form = QFormLayout()
self.orientation_label = QLabel()
self.orientation_combo = QComboBox()
for name in ORIENTATIONS:
self.orientation_combo.addItem(t(f"dialog.orientation_{name}"), name)
index = self.orientation_combo.findData(
normalise_orientation(finger_orientation)
)
self.orientation_combo.setCurrentIndex(max(index, 0))
self.orientation_note = QLabel()
self.orientation_note.setObjectName("hintText")
self.orientation_note.setWordWrap(True)
form.addRow(self.orientation_label, self.orientation_combo)
form.addRow(self.orientation_note)
root.addLayout(form)
self.button_box = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
root.addWidget(self.button_box)
self.retranslate()
def orientation(self) -> str:
return normalise_orientation(self.orientation_combo.currentData())
def retranslate(self) -> None:
self.setWindowTitle(t("dialog.plot_orientation_group"))
self.orientation_label.setText(t("dialog.plot_orientation"))
self.orientation_combo.setToolTip(t("dialog.plot_orientation_tip"))
self.orientation_note.setText(t("dialog.plot_orientation_note"))
for index in range(self.orientation_combo.count()):
name = str(self.orientation_combo.itemData(index) or "")
self.orientation_combo.setItemText(index, t(f"dialog.orientation_{name}"))
ok_btn = self.button_box.button(QDialogButtonBox.StandardButton.Ok)
cancel_btn = self.button_box.button(QDialogButtonBox.StandardButton.Cancel)
if ok_btn is not None:
ok_btn.setText(t("dialog.ok"))
if cancel_btn is not None:
cancel_btn.setText(t("dialog.cancel"))
@@ -6,16 +6,14 @@ a resting channel at zero), and what the acquisition is allowed to filter.
The filter half is a *request*: it is handed to the SDK through
``FilterSettings.apply_to`` at the start of the next session, and what the device
reports as done is what the record will say (see ``force/filters.py``).
The FN Newton lower bound is fixed at zero (see :data:`DEFAULT_PLOT_FN_MIN`); it
is not offered as a control. Finger 2D presentation lives on the 2D panel.
"""
from __future__ import annotations
from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
from orisys_app_shared.acquisition.display_orientation import (
DEFAULT_FINGER_ORIENTATION,
ORIENTATIONS,
normalise_orientation,
)
from orisys_app_shared.force.filters import (
FilterSettings,
available_presets,
@@ -33,13 +31,15 @@ from PySide6.QtWidgets import (
QLabel,
QPushButton,
QSpinBox,
QTabWidget,
QVBoxLayout,
QWidget,
)
DEFAULT_PLOT_WINDOW_SEC = 4.0
#: Lower bounds. FN is allowed below zero: a resting normal force has no reason
#: to sit exactly on the frame, and with the lower bound at 0 the idle curve is
#: drawn along the subplot border that abuts the FX frame (see force_plots).
#: FN Newton Y lower bound is always zero. A negative floor used to keep the
#: idle curve off the FX frame; that control was removed so the axis always
#: starts at rest = 0.
DEFAULT_PLOT_FN_MIN = 0.0
DEFAULT_PLOT_FN_MAX = 20.0
DEFAULT_PLOT_SHEAR_MAX = 20.0
@@ -75,20 +75,23 @@ class PlotSettingsDialog(QDialog):
filters: FilterSettings | None = None,
filters_locked: bool = False,
sample_rate_hz: float = FORCE_SAMPLE_RATE_HZ,
finger_orientation: str = DEFAULT_FINGER_ORIENTATION,
) -> None:
super().__init__(parent)
self.setModal(True)
# The layout's own minimum (618 px with the 2D presentation group) keeps the
# buttons on screen; this is the height it opens at.
self.resize(460, 640)
self.resize(460, 420)
self._sample_rate_hz = float(sample_rate_hz)
# Callers may still pass a remembered lower bound; Newton FN floor is fixed.
_ = fn_min
#: True while the widgets are being filled in from a preset, so the
#: "edited -> custom" handler does not fire on our own writes.
self._syncing_filters = False
root = QVBoxLayout(self)
form = QFormLayout()
self.tabs = QTabWidget(self)
root.addWidget(self.tabs, 1)
curves = QWidget(self.tabs)
form = QFormLayout(curves)
self.window_label = QLabel()
self.window_spin = QDoubleSpinBox()
@@ -98,15 +101,6 @@ class PlotSettingsDialog(QDialog):
self.window_spin.setSuffix(" s")
self.window_spin.setValue(float(window_sec))
# FN is asymmetric: a lower bound that may be negative, and an upper one.
self.fn_min_label = QLabel()
self.fn_min_spin = QDoubleSpinBox()
self.fn_min_spin.setRange(-1000.0, 1000.0)
self.fn_min_spin.setSingleStep(1.0)
self.fn_min_spin.setDecimals(1)
self.fn_min_spin.setSuffix(" N")
self.fn_min_spin.setValue(float(fn_min))
self.fn_max_label = QLabel()
self.fn_max_spin = QDoubleSpinBox()
self.fn_max_spin.setRange(1.0, 100.0)
@@ -173,24 +167,7 @@ class PlotSettingsDialog(QDialog):
precision_row.addWidget(QLabel("raw"))
precision_row.addWidget(self.raw_decimals_spin)
# How the finger's 2D canvas is turned for display. A *presentation* choice, so
# it lives with the other display ones (this dialog), not with the acquisition
# filters below it, and it is applied at display time only — see
# ``acquisition.display_orientation`` for why the shipped default is a half turn.
self.orientation_label = QLabel()
self.orientation_combo = QComboBox()
for name in ORIENTATIONS:
self.orientation_combo.addItem(t(f"dialog.orientation_{name}"), name)
index = self.orientation_combo.findData(
normalise_orientation(finger_orientation)
)
self.orientation_combo.setCurrentIndex(max(index, 0))
self.orientation_note = QLabel()
self.orientation_note.setObjectName("hintText")
self.orientation_note.setWordWrap(True)
form.addRow(self.window_label, self.window_spin)
form.addRow(self.fn_min_label, self.fn_min_spin)
form.addRow(self.fn_max_label, self.fn_max_spin)
form.addRow(self.shear_max_label, self.shear_max_spin)
form.addRow(self.fn_raw_min_label, self.fn_raw_min_spin)
@@ -200,13 +177,15 @@ class PlotSettingsDialog(QDialog):
form.addRow(self.deadband_raw_label, self.deadband_raw_spin)
self.precision_label = QLabel()
form.addRow(self.precision_label, precision_row)
root.addLayout(form)
self.tabs.addTab(curves, "")
self.orientation_group = QGroupBox()
orientation_form = QFormLayout(self.orientation_group)
orientation_form.addRow(self.orientation_label, self.orientation_combo)
orientation_form.addRow(self.orientation_note)
root.addWidget(self.orientation_group)
filters_page = QWidget(self.tabs)
filters_layout = QVBoxLayout(filters_page)
filters_layout.setContentsMargins(0, 8, 0, 0)
self._build_filter_section(
filters_layout, filters or FilterSettings(), filters_locked
)
self.tabs.addTab(filters_page, "")
newton = str(units or "newton").lower().startswith("n")
raw_widgets = (
@@ -219,21 +198,26 @@ class PlotSettingsDialog(QDialog):
self.shear_raw_max_label,
self.deadband_raw_label,
)
for widget in (self.fn_min_spin, self.fn_max_spin, self.shear_max_spin,
self.deadband_n_spin, self.fn_min_label, self.fn_max_label,
self.shear_max_label, self.deadband_n_label):
for widget in (
self.fn_max_spin,
self.shear_max_spin,
self.deadband_n_spin,
self.fn_max_label,
self.shear_max_label,
self.deadband_n_label,
):
widget.setEnabled(newton)
for widget in raw_widgets:
widget.setEnabled(not newton)
self._build_filter_section(root, filters or FilterSettings(), filters_locked)
buttons_row = QHBoxLayout()
self.btn_reset = QPushButton()
self.btn_reset.clicked.connect(self._reset_defaults)
buttons_row.addWidget(self.btn_reset)
buttons_row.addStretch(1)
self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
self.button_box = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
buttons_row.addWidget(self.button_box)
@@ -310,8 +294,13 @@ class PlotSettingsDialog(QDialog):
group_form.addRow(self.lowpass_order_label, self.lowpass_order_spin)
group_form.addRow(self.rate_auto_check, self.rate_spin)
for widget in (self.flow_ema_spin, self.median_spin, self.lowpass_spin,
self.rate_spin, self.rate_auto_check):
for widget in (
self.flow_ema_spin,
self.median_spin,
self.lowpass_spin,
self.rate_spin,
self.rate_auto_check,
):
widget.setToolTip("") # filled in by retranslate()
self.flow_ema_spin.valueChanged.connect(self._on_filter_value_edited)
self.median_spin.valueChanged.connect(self._on_filter_value_edited)
@@ -337,6 +326,7 @@ class PlotSettingsDialog(QDialog):
root.addWidget(self.filters_group)
root.addWidget(self.filter_note)
root.addWidget(self.filter_warning)
root.addStretch(1)
self._filters_locked = bool(locked)
self.filters_group.setEnabled(not self._filters_locked)
@@ -443,7 +433,7 @@ class PlotSettingsDialog(QDialog):
"""Every axis limit, keyed the way the preference names it."""
return {
"window_sec": float(self.window_spin.value()),
"fn_min": float(self.fn_min_spin.value()),
"fn_min": DEFAULT_PLOT_FN_MIN,
"fn_max": float(self.fn_max_spin.value()),
"shear_max": float(self.shear_max_spin.value()),
"fn_raw_min": float(self.fn_raw_min_spin.value()),
@@ -461,27 +451,19 @@ class PlotSettingsDialog(QDialog):
def settings(self) -> dict[str, float]:
"""Everything the plot half of the dialog decides, in one mapping."""
return {
**self.ranges(), **self.deadbands(),
**self.ranges(),
**self.deadbands(),
"newton_decimals": int(self.newton_decimals_spin.value()),
"raw_decimals": int(self.raw_decimals_spin.value()),
}
def orientation(self) -> str:
"""The 2D presentation the dialog is showing (see ``display_orientation``)."""
return normalise_orientation(self.orientation_combo.currentData())
def _reset_defaults(self) -> None:
self.window_spin.setValue(DEFAULT_PLOT_WINDOW_SEC)
self.fn_min_spin.setValue(DEFAULT_PLOT_FN_MIN)
self.fn_max_spin.setValue(DEFAULT_PLOT_FN_MAX)
self.shear_max_spin.setValue(DEFAULT_PLOT_SHEAR_MAX)
self.fn_raw_min_spin.setValue(DEFAULT_PLOT_FN_RAW_MIN)
self.fn_raw_max_spin.setValue(DEFAULT_PLOT_FN_RAW_MAX)
self.shear_raw_max_spin.setValue(DEFAULT_PLOT_SHEAR_RAW_MAX)
# "Reset" means the shipped picture as well: the finger's derived default.
self.orientation_combo.setCurrentIndex(
max(self.orientation_combo.findData(DEFAULT_FINGER_ORIENTATION), 0)
)
self.deadband_n_spin.setValue(DEFAULT_PLOT_DEADBAND_N)
self.deadband_raw_spin.setValue(DEFAULT_PLOT_DEADBAND_RAW)
self.newton_decimals_spin.setValue(DEFAULT_PLOT_NEWTON_DECIMALS)
@@ -498,10 +480,10 @@ class PlotSettingsDialog(QDialog):
def retranslate(self) -> None:
self.setWindowTitle(t("dialog.plot_settings_title"))
self.tabs.setTabText(0, t("dialog.plot_tab_curves"))
self.tabs.setTabText(1, t("dialog.plot_tab_filters"))
self.window_label.setText(t("dialog.plot_window"))
self.window_spin.setToolTip(t("dialog.plot_window_tip"))
self.fn_min_label.setText(t("dialog.plot_fn_min"))
self.fn_min_spin.setToolTip(t("dialog.plot_fn_min_tip"))
self.fn_max_label.setText(t("dialog.plot_fn_max"))
self.fn_max_spin.setToolTip(t("dialog.plot_fn_max_tip"))
self.shear_max_label.setText(t("dialog.plot_shear_max"))
@@ -518,17 +500,12 @@ class PlotSettingsDialog(QDialog):
self.deadband_raw_spin.setToolTip(t("dialog.plot_deadband_raw_tip"))
self.precision_label.setText(t("dialog.plot_value_decimals"))
self.btn_reset.setText(t("dialog.reset_defaults"))
self.orientation_group.setTitle(t("dialog.plot_orientation_group"))
self.orientation_label.setText(t("dialog.plot_orientation"))
self.orientation_combo.setToolTip(t("dialog.plot_orientation_tip"))
self.orientation_note.setText(t("dialog.plot_orientation_note"))
for index in range(self.orientation_combo.count()):
name = str(self.orientation_combo.itemData(index) or "")
self.orientation_combo.setItemText(index, t(f"dialog.orientation_{name}"))
self.filters_group.setTitle(t("dialog.plot_filters"))
self.filter_preset_label.setText(t("dialog.filter_preset"))
self.preset_combo.setToolTip(t("dialog.filter_preset_tip"))
self.preset_combo.setItemText(self.preset_combo.count() - 1, t("dialog.filter_preset_custom"))
self.preset_combo.setItemText(
self.preset_combo.count() - 1, t("dialog.filter_preset_custom")
)
self.flow_ema_label.setText(t("dialog.filter_flow_ema"))
self.flow_ema_spin.setToolTip(t("dialog.filter_flow_ema_tip"))
self.median_label.setText(t("dialog.filter_median"))