diff --git a/apps/customer/config/locales/en_US.json b/apps/customer/config/locales/en_US.json index f6b7e91..cbc8fc7 100644 --- a/apps/customer/config/locales/en_US.json +++ b/apps/customer/config/locales/en_US.json @@ -20,6 +20,7 @@ "finger_3d": "Finger 3D view", "show_fps": "Show FPS", "plot_settings": "Plot settings…", + "rotate_arrows": "Rotate arrows 90°", "help": "Help", "about": "About" }, diff --git a/apps/customer/config/locales/zh_CN.json b/apps/customer/config/locales/zh_CN.json index 0e57793..414944d 100644 --- a/apps/customer/config/locales/zh_CN.json +++ b/apps/customer/config/locales/zh_CN.json @@ -20,6 +20,7 @@ "finger_3d": "手指 3D 视图", "show_fps": "显示 FPS", "plot_settings": "绘图设置…", + "rotate_arrows": "旋转箭头图 90°", "help": "帮助", "about": "关于" }, diff --git a/apps/customer/config/preferences.default.json b/apps/customer/config/preferences.default.json index 593d65c..e8e45c0 100644 --- a/apps/customer/config/preferences.default.json +++ b/apps/customer/config/preferences.default.json @@ -13,5 +13,6 @@ "plot_window_sec": 4.0, "plot_fn_max": 20.0, "plot_shear_max": 20.0, - "image_view_mode": "arrows_pure" + "image_view_mode": "arrows_pure", + "arrow_rotation_deg": 90 } diff --git a/apps/customer/config/sensors/box/force_calibration.json b/apps/customer/config/sensors/box/force_calibration.json index 351eb0e..015fc07 100644 --- a/apps/customer/config/sensors/box/force_calibration.json +++ b/apps/customer/config/sensors/box/force_calibration.json @@ -1,5 +1,5 @@ { - "schema_version": 2, + "schema_version": 1, "unit": "N", "channels": { "normal": { @@ -8,105 +8,22 @@ "extrapolate": true, "clamp_output_min": 0.0, "points": [ - [ - 3167.3289065950753, - 0.0 - ], - [ - 3617.3242262492, - 1.0 - ] + [1000.0, 0.0], + [10000.0, 0.377], + [30000.0, 1.377], + [62000.0, 2.377] ] }, - "shear_x": { + "shear": { "method": "piecewise_linear", - "raw_key": "fshearx", + "input": "components", + "signed": true, "extrapolate": true, "clamp_output_min": 0.0, "points": [ - [ - 1471.613952418736, - 0.0 - ], - [ - 1743.8539805266817, - 1.0 - ] - ], - "signed": true - }, - "shear_y": { - "method": "piecewise_linear", - "raw_key": "fsheary", - "extrapolate": true, - "clamp_output_min": 0.0, - "points": [ - [ - 3845.5798720537605, - 1.0 - ], - [ - 3881.793802339737, - 0.0 - ] - ], - "signed": true - } - }, - "meta": { - "date": "2026-09-06T21:54:09+08:00", - "sensor_type": "box", - "sn": "a", - "stitch_resolution": [ - 365, - 365 - ], - "dis_mode": 3, - "hhd_downsample": 3, - "record_duration_s": 7.0, - "config_ref": "D:\\repo\\orisys-app\\apps\\customer\\config\\sensors\\box\\sensor.json", - "samples": { - "fn": [ - { - "weight_n": 0.0, - "raw_mean": 3167.3289065950753, - "n": 283, - "std": 283.516893458341 - }, - { - "weight_n": 1.0, - "raw_mean": 3617.3242262492, - "n": 293, - "std": 271.21386400281835 - } - ], - "fx": [ - { - "weight_n": 0.0, - "raw_mean": -1471.613952418736, - "n": 280, - "std": 311.9173147283949 - }, - { - "weight_n": 1.0, - "raw_mean": -1743.8539805266817, - "n": 295, - "std": 324.2175747527586 - } - ], - "fy": [ - { - "weight_n": 0.0, - "raw_mean": 3881.793802339737, - "n": 292, - "std": 362.16000171157486 - }, - { - "weight_n": 1.0, - "raw_mean": 3845.5798720537605, - "n": 295, - "std": 390.42421990805127 - } + [1000.0, 0.0], + [36000.0, 1.19], + [68000.0, 2.06] ] } } diff --git a/apps/customer/src/orisys_customer/app_window.py b/apps/customer/src/orisys_customer/app_window.py index c49051d..037cd2a 100644 --- a/apps/customer/src/orisys_customer/app_window.py +++ b/apps/customer/src/orisys_customer/app_window.py @@ -49,6 +49,7 @@ from orisys_customer.ui.toolbar import ToolbarWidget logger = logging.getLogger(__name__) RUNNABLE_TYPES = frozenset({"box", "mini", "finger"}) +ARROW_ROTATE_TYPES = frozenset({"box", "mini"}) def _bgr_to_qimage(frame: np.ndarray) -> QImage: @@ -196,6 +197,7 @@ class CustomerMainWindow(QMainWindow): self.toolbar.set_units(str(self.prefs.get("units") or "newton")) self.arrow_panel.set_view_mode(str(self.prefs.get("image_view_mode") or "arrows_pure")) self.arrow_panel.set_fps_visible(self._show_fps) + self.arrow_panel.set_rotation_deg(self.prefs.get("arrow_rotation_deg", 90)) self._apply_plot_prefs_to_panel() sensor_type = str(self.prefs.get("sensor_type") or "box") @@ -205,6 +207,7 @@ class CustomerMainWindow(QMainWindow): self.sensor_picker.set_config_name(config_name) else: self.prefs["config_name"] = self.sensor_picker.config_name() + self._apply_arrow_rotation_for_sensor() def _build_menus(self) -> None: menu_bar = self.menuBar() @@ -239,6 +242,10 @@ class CustomerMainWindow(QMainWindow): self.act_plot_settings = QAction(self) self.act_plot_settings.triggered.connect(self._on_plot_settings) self.view_menu.addAction(self.act_plot_settings) + + self.act_rotate_arrows = QAction(self) + self.act_rotate_arrows.triggered.connect(self._on_rotate_arrows) + self.view_menu.addAction(self.act_rotate_arrows) self.view_menu.addSeparator() self.theme_menu = self.view_menu.addMenu("") @@ -289,6 +296,7 @@ class CustomerMainWindow(QMainWindow): self.act_finger_3d.blockSignals(True) self.act_finger_3d.setChecked(bool(self._show_finger_3d) if is_finger else False) self.act_finger_3d.blockSignals(False) + self._apply_arrow_rotation_for_sensor() def _apply_panel_visibility(self) -> None: is_finger = self.sensor_picker.sensor_type() == "finger" @@ -329,6 +337,7 @@ class CustomerMainWindow(QMainWindow): self.act_finger_3d.setText(t("menu.finger_3d")) self.act_show_fps.setText(t("menu.show_fps")) self.act_plot_settings.setText(t("menu.plot_settings")) + self.act_rotate_arrows.setText(t("menu.rotate_arrows")) self.theme_menu.setTitle(t("menu.theme")) self.act_theme_dark.setText(t("menu.theme_dark")) self.act_theme_light.setText(t("menu.theme_light")) @@ -449,9 +458,22 @@ class CustomerMainWindow(QMainWindow): config_ref=self._resolve_config_ref(), ) self._apply_panel_visibility() + self._apply_arrow_rotation_for_sensor() self._update_run_enabled() self._persist_prefs() + def _apply_arrow_rotation_for_sensor(self) -> None: + enabled = self.sensor_picker.sensor_type() in ARROW_ROTATE_TYPES + self.arrow_panel.set_rotation_enabled(enabled) + self.act_rotate_arrows.setEnabled(enabled) + + def _on_rotate_arrows(self) -> None: + if self.sensor_picker.sensor_type() not in ARROW_ROTATE_TYPES: + return + deg = self.arrow_panel.rotate_90_ccw() + self.prefs["arrow_rotation_deg"] = deg + self._persist_prefs() + def _on_config_changed(self, config_name: str) -> None: self.prefs["config_name"] = str(config_name) self._persist_prefs() @@ -824,6 +846,7 @@ class CustomerMainWindow(QMainWindow): self.prefs["video_path"] = self.toolbar.video_path() self.prefs["units"] = self.toolbar.units() self.prefs["image_view_mode"] = self.arrow_panel.view_mode() + self.prefs["arrow_rotation_deg"] = self.arrow_panel.rotation_deg() self.prefs.setdefault("plot_window_sec", DEFAULT_PLOT_WINDOW_SEC) self.prefs.setdefault("plot_fn_max", DEFAULT_PLOT_FN_MAX) self.prefs.setdefault("plot_shear_max", DEFAULT_PLOT_SHEAR_MAX) diff --git a/apps/customer/src/orisys_customer/ui/arrow_panel.py b/apps/customer/src/orisys_customer/ui/arrow_panel.py index 1043ff2..4bb0a78 100644 --- a/apps/customer/src/orisys_customer/ui/arrow_panel.py +++ b/apps/customer/src/orisys_customer/ui/arrow_panel.py @@ -3,7 +3,7 @@ from __future__ import annotations from PyQt5.QtCore import Qt, pyqtSignal -from PyQt5.QtGui import QImage, QPixmap +from PyQt5.QtGui import QImage, QPixmap, QTransform from PyQt5.QtWidgets import ( QComboBox, QFrame, @@ -16,6 +16,17 @@ from PyQt5.QtWidgets import ( from orisys_app_shared.i18n import t VIEW_MODES = ("arrows_pure", "arrows_stitched", "depth") +_ROTATION_STEPS = (0, 90, 180, 270) + + +def _normalize_rotation_deg(value: object, default: int = 90) -> int: + try: + deg = int(value) % 360 # type: ignore[arg-type] + except (TypeError, ValueError): + return default + if deg not in _ROTATION_STEPS: + deg = int(round(deg / 90.0) * 90) % 360 + return deg class ArrowPanel(QFrame): @@ -34,6 +45,9 @@ class ArrowPanel(QFrame): self.fps_label.setVisible(False) self._fps_visible = False self._fps_value: float | None = None + self._rotation_deg = 90 + self._rotation_enabled = True + self._source_image: QImage | None = None self.view_combo = QComboBox() self.view_combo.setMinimumWidth(140) self.view_combo.currentIndexChanged.connect(self._on_view_index_changed) @@ -68,6 +82,26 @@ class ArrowPanel(QFrame): self.view_combo.setCurrentIndex(idx) self.view_combo.blockSignals(False) + def rotation_deg(self) -> int: + return self._rotation_deg + + def set_rotation_deg(self, deg: object) -> None: + self._rotation_deg = _normalize_rotation_deg(deg) + self._repaint_source() + + def set_rotation_enabled(self, enabled: bool) -> None: + wanted = bool(enabled) + if wanted == self._rotation_enabled: + return + self._rotation_enabled = wanted + self._repaint_source() + + def rotate_90_ccw(self) -> int: + if not self._rotation_enabled: + return self._rotation_deg + self.set_rotation_deg(self._rotation_deg + 90) + return self._rotation_deg + def set_fps_visible(self, visible: bool) -> None: self._fps_visible = bool(visible) self._refresh_fps_label() @@ -113,10 +147,22 @@ class ArrowPanel(QFrame): self._placeholder = t("app.scaffold_hint") self.image_label.setText(self._placeholder) - def show_frame(self, image: QImage) -> None: - if image.isNull(): + def _effective_rotation_deg(self) -> int: + if not self._rotation_enabled: + return 0 + return self._rotation_deg + + def _repaint_source(self) -> None: + if self._source_image is None or self._source_image.isNull(): return + self._paint_image(self._source_image) + + def _paint_image(self, image: QImage) -> None: pix = QPixmap.fromImage(image) + deg = self._effective_rotation_deg() + if deg: + # QTransform.rotate is clockwise on screen pixmaps; negate for CCW. + pix = pix.transformed(QTransform().rotate(-deg), Qt.FastTransformation) scaled = pix.scaled( self.image_label.size(), Qt.KeepAspectRatio, @@ -125,7 +171,14 @@ class ArrowPanel(QFrame): self.image_label.setPixmap(scaled) self.image_label.setText("") + def show_frame(self, image: QImage) -> None: + if image.isNull(): + return + self._source_image = image + self._paint_image(image) + def clear_preview(self, message: str | None = None) -> None: + self._source_image = None self.image_label.clear() self.image_label.setPixmap(QPixmap()) text = message if message is not None else t("app.scaffold_hint") diff --git a/shared/src/orisys_app_shared/prefs/store.py b/shared/src/orisys_app_shared/prefs/store.py index d6e43cb..8a97a6f 100644 --- a/shared/src/orisys_app_shared/prefs/store.py +++ b/shared/src/orisys_app_shared/prefs/store.py @@ -25,6 +25,7 @@ _DEFAULTS: dict[str, Any] = { "plot_fn_max": 20.0, "plot_shear_max": 20.0, "image_view_mode": "arrows_pure", + "arrow_rotation_deg": 90, }