From 92e6de69133fab6212adcb51e9ec012210bb4c66 Mon Sep 17 00:00:00 2001 From: ykcheungab Date: Wed, 7 Oct 2026 20:48:48 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BF=E6=94=B6=E7=B4=A7=E7=BB=98=E5=9B=BE?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E4=B8=BA=E4=B8=A4=E9=A1=B5=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=8A=8A=20FN=20=E7=89=9B=E9=A1=BF=E4=B8=8B=E9=99=90=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E4=B8=BA=200=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 手指 2D 方向改到 ArrowPanel 设置对话框;归零阈值不变。已用 display_prefs、finger_2d_orientation 与绘图对话框相关 smoke 测试验证。 --- apps/studio/config/locales/en_US.json | 5 +- apps/studio/config/locales/zh_CN.json | 5 +- apps/studio/src/orisys_studio/app_window.py | 20 ++- .../studio/src/orisys_studio/display_prefs.py | 9 +- .../src/orisys_studio/ui/arrow_panel.py | 20 ++- .../ui/finger_orientation_dialog.py | 80 +++++++++++ .../orisys_studio/ui/plot_settings_dialog.py | 125 +++++++----------- apps/studio/tests/test_display_prefs.py | 9 +- .../tests/test_finger_2d_orientation.py | 16 +-- apps/studio/tests/test_ui_smoke.py | 16 ++- 10 files changed, 194 insertions(+), 111 deletions(-) create mode 100644 apps/studio/src/orisys_studio/ui/finger_orientation_dialog.py diff --git a/apps/studio/config/locales/en_US.json b/apps/studio/config/locales/en_US.json index f2af5e5..3cb4f10 100644 --- a/apps/studio/config/locales/en_US.json +++ b/apps/studio/config/locales/en_US.json @@ -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.", diff --git a/apps/studio/config/locales/zh_CN.json b/apps/studio/config/locales/zh_CN.json index 992fd03..a04b1ce 100644 --- a/apps/studio/config/locales/zh_CN.json +++ b/apps/studio/config/locales/zh_CN.json @@ -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": "尚无分析结果。打开录像会自动分析,或点顶部「分析视频…」。", diff --git a/apps/studio/src/orisys_studio/app_window.py b/apps/studio/src/orisys_studio/app_window.py index dfadc04..b7b12c4 100644 --- a/apps/studio/src/orisys_studio/app_window.py +++ b/apps/studio/src/orisys_studio/app_window.py @@ -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. diff --git a/apps/studio/src/orisys_studio/display_prefs.py b/apps/studio/src/orisys_studio/display_prefs.py index 151d9dd..3a92fd2 100644 --- a/apps/studio/src/orisys_studio/display_prefs.py +++ b/apps/studio/src/orisys_studio/display_prefs.py @@ -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) diff --git a/apps/studio/src/orisys_studio/ui/arrow_panel.py b/apps/studio/src/orisys_studio/ui/arrow_panel.py index 04e195f..6961741 100644 --- a/apps/studio/src/orisys_studio/ui/arrow_panel.py +++ b/apps/studio/src/orisys_studio/ui/arrow_panel.py @@ -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() diff --git a/apps/studio/src/orisys_studio/ui/finger_orientation_dialog.py b/apps/studio/src/orisys_studio/ui/finger_orientation_dialog.py new file mode 100644 index 0000000..5d52eb4 --- /dev/null +++ b/apps/studio/src/orisys_studio/ui/finger_orientation_dialog.py @@ -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")) diff --git a/apps/studio/src/orisys_studio/ui/plot_settings_dialog.py b/apps/studio/src/orisys_studio/ui/plot_settings_dialog.py index 8d1094e..34be436 100644 --- a/apps/studio/src/orisys_studio/ui/plot_settings_dialog.py +++ b/apps/studio/src/orisys_studio/ui/plot_settings_dialog.py @@ -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")) diff --git a/apps/studio/tests/test_display_prefs.py b/apps/studio/tests/test_display_prefs.py index a98dc06..7863093 100644 --- a/apps/studio/tests/test_display_prefs.py +++ b/apps/studio/tests/test_display_prefs.py @@ -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 diff --git a/apps/studio/tests/test_finger_2d_orientation.py b/apps/studio/tests/test_finger_2d_orientation.py index 0dc7f1b..f430b88 100644 --- a/apps/studio/tests/test_finger_2d_orientation.py +++ b/apps/studio/tests/test_finger_2d_orientation.py @@ -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" diff --git a/apps/studio/tests/test_ui_smoke.py b/apps/studio/tests/test_ui_smoke.py index 64475f7..9958231 100644 --- a/apps/studio/tests/test_ui_smoke.py +++ b/apps/studio/tests/test_ui_smoke.py @@ -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()