收紧绘图设置为两页,并把 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
+4 -1
View File
@@ -52,10 +52,12 @@
},
"dialog": {
"plot_settings_title": "Plot settings",
"plot_tab_curves": "Curves",
"plot_tab_filters": "Filters",
"plot_window": "Time window",
"plot_window_tip": "Force plot time window (seconds)",
"plot_fn_min": "FN min",
"plot_fn_min_tip": "Normal-force axis minimum (Newton mode). It may be negative: leaving room below zero keeps the idle curve off the FX frame and makes small presses visible",
"plot_fn_min_tip": "Normal-force axis minimum (Newton mode); fixed at 0",
"plot_fn_max": "FN max",
"plot_fn_max_tip": "Normal force axis max (Newton mode)",
"plot_shear_max": "Shear max",
@@ -204,6 +206,7 @@
"view_depth": "Depth map",
"view_recorded": "Recorded image",
"view_mode_tip": "Image display mode while running",
"arrow_settings_tip": "Finger 2D presentation (orientation; display only)",
"replay_pipeline": "Recorded video (raw camera image)",
"replay_pipeline_tip": "Replay shows the recorded raw camera image with no live arrow overlay; to get arrows and depth from analysis, use View → Analyze video",
"replay_raw_note": "No analysis result yet. Opening a recording runs analysis, or choose Analyze video… above.",
+4 -1
View File
@@ -52,10 +52,12 @@
},
"dialog": {
"plot_settings_title": "绘图设置",
"plot_tab_curves": "曲线",
"plot_tab_filters": "滤波",
"plot_window": "时间窗口",
"plot_window_tip": "力曲线时间窗口(秒)",
"plot_fn_min": "FN 下限",
"plot_fn_min_tip": "法向力纵轴下限(牛顿模式)。可以是负数:留出零点以下的空间,静止曲线不会压在 FX 框线上,小压力也更清楚",
"plot_fn_min_tip": "法向力纵轴下限(牛顿模式);固定为 0",
"plot_fn_max": "FN 上限",
"plot_fn_max_tip": "法向力纵轴上限(牛顿模式)",
"plot_shear_max": "剪切上限",
@@ -204,6 +206,7 @@
"view_depth": "深度图",
"view_recorded": "原始图像",
"view_mode_tip": "运行时的图像显示模式",
"arrow_settings_tip": "手指 2D 显示方向(只影响画面)",
"replay_pipeline": "录制视频(原始相机图像)",
"replay_pipeline_tip": "回放显示的是录制下来的原始相机图像,不含实时箭头叠加;要看分析得到的箭头与深度请用「视图 → 分析视频」",
"replay_raw_note": "尚无分析结果。打开录像会自动分析,或点顶部「分析视频…」。",
+13 -7
View File
@@ -65,6 +65,7 @@ from orisys_studio.ui.brand_bar import BrandBar
from orisys_studio.ui.cal_panel import CalPanel
from orisys_studio.ui.force_plots import CHANNEL_LABELS, ForcePlotsPanel
from orisys_studio.ui.mesh3d_panel import Mesh3dPanel
from orisys_studio.ui.finger_orientation_dialog import FingerOrientationDialog
from orisys_studio.ui.plot_settings_dialog import (
DEFAULT_PLOT_FN_MAX,
DEFAULT_PLOT_FN_RAW_MAX,
@@ -926,6 +927,8 @@ class StudioMainWindow(QMainWindow):
self.display_ctl.on_image_view_mode_changed
)
self.arrow_panel.view_mode_changed.connect(lambda _mode: self.replay_ctl.render_frame())
# Finger 2D presentation lives on the 2D panel (not in plot settings).
self.arrow_panel.settings_requested.connect(self._on_finger_orientation_settings)
# The unit chip on the chart header switches the same unit everywhere.
self.force_plots.unit_toggle_requested.connect(
self.display_ctl.on_chart_unit_toggle
@@ -1766,13 +1769,21 @@ class StudioMainWindow(QMainWindow):
self.display_ctl.show_fps = visible
def _on_finger_orientation_settings(self) -> None:
"""2D panel gear: how the finger canvas is presented (display only)."""
dialog = FingerOrientationDialog(
self, finger_orientation=self.finger_orientation()
)
if dialog.exec() != FingerOrientationDialog.Accepted:
return
self.set_finger_orientation(dialog.orientation())
def _on_plot_settings(self) -> None:
settings = self.display_ctl.plot_settings()
dialog = PlotSettingsDialog(
self,
window_sec=settings["window_sec"],
# FN's lower bound may be negative: that is the "show me below zero" the
# operator asked for, and it is what puts the 0 tick back on the axis.
# Newton FN floor is fixed at zero; the dialog still accepts the key.
fn_min=settings["fn_min"],
fn_max=settings["fn_max"],
shear_max=settings["shear_max"],
@@ -1790,17 +1801,12 @@ class StudioMainWindow(QMainWindow):
# dialog is the place that has to say "next session, not this one".
filters_locked=bool(self._running or self.recorder.is_recording),
sample_rate_hz=self.display_ctl.device_sample_rate(),
finger_orientation=self._finger_orientation,
)
if dialog.exec() != PlotSettingsDialog.Accepted:
return
# One call for the whole picture: the dialog owns the schema, the controller
# writes what it was given and re-reads the rest.
self.display_ctl.apply_plot_settings(dialog.settings())
# The 2D presentation is applied here rather than in the controller: it changes
# the picture the *window* draws (``_show_image_frame``), and it must take
# effect without a restart.
self.set_finger_orientation(dialog.orientation())
filters = dialog.filter_values()
# The same keys ``_filter_settings()`` reads back, so what the dialog
# shows next time is what the session will actually be given.
@@ -42,8 +42,8 @@ from orisys_studio.ui.plot_settings_dialog import (
logger = logging.getLogger(__name__)
#: The chart range, per channel and per unit. ``fn_min`` is the one that may be
#: negative (FN is asymmetric); FX/FY take one absolute magnitude each.
#: The chart range, per channel and per unit. Newton ``fn_min`` is fixed at zero;
#: FX/FY take one absolute magnitude each. Raw FN still has its own lower bound.
PLOT_RANGE_DEFAULTS = {
"window_sec": DEFAULT_PLOT_WINDOW_SEC,
"fn_min": DEFAULT_PLOT_FN_MIN,
@@ -162,7 +162,6 @@ class DisplayPrefsController(QObject):
# --------------------------------------------------------------------- plot
def plot_settings(self) -> dict:
"""The chart range, deadband and window as the settings dialog shows them."""
prefs = self._host.prefs
settings = {
key: self._number(
f"plot_{key}", self._plot_defaults.get(key, fallback)
@@ -171,6 +170,8 @@ class DisplayPrefsController(QObject):
**PLOT_RANGE_DEFAULTS, **PLOT_DEADBAND_DEFAULTS, **PLOT_PRECISION_DEFAULTS
}.items()
}
# Newton FN floor is fixed; older prefs may still hold a negative value.
settings["fn_min"] = float(DEFAULT_PLOT_FN_MIN)
settings["units"] = self._toolbar.units()
return settings
@@ -197,6 +198,8 @@ class DisplayPrefsController(QObject):
for key in (*PLOT_RANGE_DEFAULTS, *PLOT_DEADBAND_DEFAULTS, *PLOT_PRECISION_DEFAULTS):
if values.get(key) is not None:
self._host.prefs[f"plot_{key}"] = values[key]
# Always rewrite Newton FN floor so a stored negative cannot linger.
self._host.prefs["plot_fn_min"] = float(DEFAULT_PLOT_FN_MIN)
settings = self.plot_settings()
self._charts.set_window_sec(float(settings["window_sec"]))
self._apply_ranges(settings)
@@ -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"))
+5 -4
View File
@@ -257,12 +257,12 @@ def test_applying_plot_settings_writes_the_preferences_and_the_panel():
}
)
assert parts["host"].prefs["plot_window_sec"] == 20.0
assert parts["host"].prefs["plot_fn_min"] == -2.0
assert parts["host"].prefs["plot_fn_min"] == 0.0
assert parts["host"].prefs["plot_shear_raw_max"] == 3e5
assert parts["host"].prefs["plot_deadband_n"] == 0.25
assert parts["charts"].window_sec == [20.0]
assert parts["charts"].limits[-1] == {
"fn_min": -2.0,
"fn_min": 0.0,
"fn_max": 8.0,
"shear_max": 9.0,
"fn_raw_min": -1000.0,
@@ -281,8 +281,9 @@ def test_a_partial_plot_settings_mapping_leaves_the_rest_as_stored():
{"window_sec": 6.0, "fn_max": 12.0, "shear_max": 3.0,
"fn_raw_max": 4e5, "shear_raw_max": 5e5}
)
assert parts["host"].prefs["plot_fn_min"] == -5.0
assert parts["charts"].limits[-1]["fn_min"] == -5.0
# Newton FN floor is always rewritten to 0; other stored values stay.
assert parts["host"].prefs["plot_fn_min"] == 0.0
assert parts["charts"].limits[-1]["fn_min"] == 0.0
assert parts["charts"].deadband_floors[-1]["raw"] == 900.0
@@ -524,7 +524,7 @@ def test_the_orientation_preference_survives_a_restart(make_window, qt_app) -> N
def test_the_dialog_carries_the_orientation_to_the_window(make_window, monkeypatch) -> None:
"""The control is in the dialog the app already opens, and reaches the prefs."""
"""The 2D panel settings dialog carries orientation into prefs."""
from orisys_app_shared.prefs import load_preferences
from orisys_studio import app_window as app_window_module
@@ -539,22 +539,12 @@ def test_the_dialog_carries_the_orientation_to_the_window(make_window, monkeypat
def exec(self) -> int:
return 1
def settings(self) -> dict:
return {}
def filter_values(self):
from orisys_app_shared.force.filters import FilterSettings
return FilterSettings()
def orientation(self) -> str:
return ORIENTATION_MIRROR_V
monkeypatch.setattr(app_window_module, "PlotSettingsDialog", _Dialog)
monkeypatch.setattr(app_window_module, "FingerOrientationDialog", _Dialog)
window = make_window(sensor_type="finger")
# No device: the dialog path would otherwise create the acquisition workers.
window._ensure_workers = lambda: None
window._on_plot_settings()
window._on_finger_orientation_settings()
assert seen.get("finger_orientation") == ORIENTATION_ROTATE_180, (
"the dialog was not told what the window is showing"
+9 -7
View File
@@ -3044,22 +3044,24 @@ def test_the_dialog_writes_the_keys_the_session_reads_back(qt_app):
def test_the_plot_dialog_carries_the_ranges_and_the_zero_threshold(qt_app):
"""FN has a lower bound (which may be negative); both units a deadband."""
"""Newton FN floor is fixed at 0; both units keep a deadband."""
from orisys_studio.ui.plot_settings_dialog import PlotSettingsDialog
dialog = PlotSettingsDialog(
units="newton", fn_min=-2.0, fn_max=6.0, deadband_n=0.25, deadband_raw=2000.0
)
assert dialog.settings()["fn_min"] == -2.0
assert dialog.settings()["fn_min"] == 0.0
assert dialog.settings()["fn_max"] == 6.0
assert dialog.deadbands() == {"deadband_n": 0.25, "deadband_raw": 2000.0}
assert dialog.tabs.count() == 2
# Only the half that belongs to the unit being shown can be edited.
assert dialog.fn_min_spin.isEnabled() and not dialog.fn_raw_min_spin.isEnabled()
assert dialog.fn_max_spin.isEnabled() and not dialog.fn_raw_min_spin.isEnabled()
assert dialog.deadband_n_spin.isEnabled()
assert not dialog.deadband_raw_spin.isEnabled()
assert not hasattr(dialog, "fn_min_spin")
raw = PlotSettingsDialog(units="raw")
assert raw.fn_raw_min_spin.isEnabled() and not raw.fn_min_spin.isEnabled()
assert raw.fn_raw_min_spin.isEnabled() and not raw.fn_max_spin.isEnabled()
assert raw.deadband_raw_spin.isEnabled() and not raw.deadband_n_spin.isEnabled()
raw.fn_raw_min_spin.setValue(-50000.0)
raw.deadband_raw_spin.setValue(5000.0)
@@ -3094,17 +3096,17 @@ def test_the_plot_dialog_values_reach_the_chart(make_window):
window.display_ctl.apply_plot_settings(dialog.settings())
plots = window.force_plots
assert (plots._fn_min, plots._fn_max, plots._shear_max) == (-2.0, 6.0, 3.0)
assert (plots._fn_min, plots._fn_max, plots._shear_max) == (0.0, 6.0, 3.0)
assert (plots._fn_raw_min, plots._fn_raw_max, plots._shear_raw_max) == (
-1000.0, 5e5, 2e5,
)
assert plots.presentation.min_deadband_for("N") == 0.25
assert plots.presentation.min_deadband_for("raw") == 3000.0
assert window.prefs["plot_fn_min"] == -2.0
assert window.prefs["plot_fn_min"] == 0.0
assert window.prefs["plot_deadband_raw"] == 3000.0
assert plots.history.window_sec == 6.0
# And the dialog would open on the same numbers next time.
assert window.display_ctl.plot_settings()["fn_min"] == -2.0
assert window.display_ctl.plot_settings()["fn_min"] == 0.0
window.close()