fix(autocal): 将标定记录总览接入主窗口

合并旧占位页为标定记录页,并统一采集页的数据查看入口。通过45项相关测试、离屏布局检查及代码和文档门禁。
This commit is contained in:
ykcheungab
2026-10-11 19:35:25 +08:00
parent b200f3d55c
commit 432f45325f
8 changed files with 112 additions and 33 deletions
+4 -5
View File
@@ -225,12 +225,9 @@
"tab_sequence": "Test sequence",
"tab_finger": "Finger debug",
"tab_sensor": "Calibration capture",
"tab_surface": "Surface map",
"tab_points": "Calibration points",
"tab_records": "Calibration records",
"not_built": "Not built yet",
"reserved_sensor": "Sensor connection, live image and force curves will appear here: device and source selection, preview, and a compact version of the Studio curve view.",
"reserved_surface": "Multi-point calibration sensitivity will be drawn as a colour map on the sensor's 3D surface here: each point's sensitivity interpolated and coloured, with a legend and range.",
"reserved_points": "The calibration point table and capture controls will appear here: platform coordinates, reference load, sampling statistics and the resulting sensitivity."
"reserved_sensor": "Sensor connection, live image and force curves will appear here: device and source selection, preview, and a compact version of the Studio curve view."
},
"source": {
"kind": "Input source",
@@ -491,6 +488,8 @@
"flow_missing": "No valid displacement for this frame"
},
"overview": {
"open_session": "Open calibration session…",
"empty": "Choose the session folder containing session.json to view collected sites, normal response and sensitivity. You can also open current data from Calibration capture.",
"title": "Calibration record overview",
"notice": "Inspect acquired sites and normal response; click UV points or table rows. Circles: confirmed windows; squares: candidates. Trial shades differ. The fit uses all eligible loading data for the site and is not refitted by the trial filter.",
"coverage": "Acquisition status",
+4 -5
View File
@@ -225,12 +225,9 @@
"tab_sequence": "测试序列",
"tab_finger": "手指调试",
"tab_sensor": "标定采集",
"tab_surface": "表面分布",
"tab_points": "标定点",
"tab_records": "标定记录",
"not_built": "尚未实现",
"reserved_sensor": "传感器连接、实时图像与力曲线将在此显示:设备与信号源选择、预览、以及和 Studio 类似的曲线视图(占用空间更小)。",
"reserved_surface": "多点标定的灵敏度将在此以色图叠加在传感器三维表面上:每个标定点的灵敏度经插值着色,并给出色标与范围。",
"reserved_points": "标定点列表与采集控制将在此显示:平台坐标、参考载荷、采样统计与灵敏度结果。"
"reserved_sensor": "传感器连接、实时图像与力曲线将在此显示:设备与信号源选择、预览、以及和 Studio 类似的曲线视图(占用空间更小)。"
},
"source": {
"kind": "输入来源",
@@ -491,6 +488,8 @@
"flow_missing": "本帧无有效位移信号"
},
"overview": {
"open_session": "打开标定会话…",
"empty": "请选择包含 session.json 的会话文件夹,查看已采集测点、法向响应和灵敏度;也可从标定采集页打开本场数据。",
"title": "标定记录总览",
"notice": "查看实际采集覆盖与法向响应;点击 UV 点或表格联动。圆点为已确认窗口,方点为候选窗口;各 trial 用不同明暗度。拟合线为本测点全部符合条件的加载数据,筛选 trial 不会重新拟合。",
"coverage": "采集状态",
@@ -108,7 +108,7 @@ finger_F001_<时间>_<标识>/
主时间是电脑取得该帧时绑定的单调时间,UTC 用于追溯;处理完成时间不能代替采集时间。FT/ECAT 后续接入同源接收时间,按时间关联,不按行号配对;低延迟不等于零延迟。旧视频的 `host_replay` 是本次解码时间,只能调试,不能恢复原实验与参考设备的物理同步。
点击“查看标定数据…”选会话目录,进入记录总览;点击 UV 点或表格,联动法向响应和 trial 筛选,再“查看信号/复核窗口…”。
“标定记录”页打开会话目录;采集页“查看标定数据…”也跳转至此。点击 UV 点或表格,联动响应及 trial 筛选,再“查看信号/复核窗口…”。
| 总览 | 规则 |
|---|---|
@@ -36,6 +36,7 @@ class CalibrationPanel(QWidget):
"""One operator page. No motion commands are issued by this widget."""
loadBundleRequested = Signal()
reviewRequested = Signal(str)
def __init__(self, source_panel: QWidget, parent: QWidget | None = None) -> None:
super().__init__(parent)
@@ -351,19 +352,13 @@ class CalibrationPanel(QWidget):
self._sync()
def _review(self) -> None:
from .record_overview_dialog import RecordOverviewDialog
session = self.trial.session
directory = QFileDialog.getExistingDirectory(
self, t("review.open"),
str(session.directory) if session is not None else self.output_path.text(),
)
if directory:
try:
dialog = RecordOverviewDialog(Path(directory), self)
dialog.exec()
except (OSError, ValueError, KeyError, TypeError) as exc:
self.result_label.setText(t("calibration.error").format(detail=exc))
self.reviewRequested.emit(directory)
def shutdown(self) -> None:
self._timer.stop()
@@ -8,7 +8,7 @@ from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QPainter
from PySide6.QtWidgets import (
QComboBox, QDialog, QFileDialog, QHBoxLayout, QHeaderView, QLabel,
QPushButton, QTableWidget, QTableWidgetItem, QVBoxLayout,
QPushButton, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
)
from ..record_overview import load_overview, overview_rows, read_plan, save_overview
@@ -16,7 +16,63 @@ from ..session_review import review_session
from .session_review_dialog import SessionReviewDialog
class RecordOverviewPage(QWidget):
"""Keep the existing record viewer inside the main task workspace."""
def __init__(self, parent=None):
super().__init__(parent)
self.overview = None
self.directory = Path.cwd()
self.content_layout = QVBoxLayout(self)
self.open_button = QPushButton(self)
self.open_button.setObjectName("openCalibrationRecords")
self.open_button.clicked.connect(self._open)
self.path_label = QLabel(self)
self.path_label.setWordWrap(True)
self.message = QLabel(self)
self.message.setWordWrap(True)
self.content_layout.addWidget(self.open_button)
self.content_layout.addWidget(self.path_label)
self.content_layout.addWidget(self.message)
self.content_layout.addStretch(1)
self.retranslate()
def retranslate(self):
self.open_button.setText(t("overview.open_session"))
if self.overview is None:
self.path_label.setText(t("overview.empty"))
def _open(self):
path = QFileDialog.getExistingDirectory(self, t("overview.open_session"), str(self.directory))
if path:
self.load_session(Path(path))
def load_session(self, directory: Path):
try:
overview = RecordOverviewDialog(directory, self)
except (OSError, ValueError, KeyError, TypeError) as exc:
self.message.setText(t("calibration.error").format(detail=exc))
return
overview.setWindowFlags(Qt.WindowType.Widget)
if self.overview is not None:
self.content_layout.removeWidget(self.overview)
self.overview.hide()
self.overview.deleteLater()
else:
self.content_layout.takeAt(self.content_layout.count() - 1)
self.overview = overview
self.directory = directory
self.path_label.setText(str(directory))
self.message.clear()
self.content_layout.addWidget(overview, 1)
overview.show()
class RecordOverviewDialog(QDialog):
def reject(self):
if self.isWindow():
super().reject()
def __init__(self, directory: Path, parent=None):
super().__init__(parent)
self.directory = directory
@@ -305,6 +305,7 @@ class AutoCalMainWindow(QMainWindow):
body,
sensor_panel=self.calibration_panel,
)
self.calibration_panel.reviewRequested.connect(self._on_review_records)
self.main_splitter = QSplitter(Qt.Orientation.Horizontal, body)
self.main_splitter.addWidget(self.left_rail)
@@ -325,6 +326,10 @@ class AutoCalMainWindow(QMainWindow):
self._wire_signals()
def _on_review_records(self, directory: str) -> None:
self.workspace.page("records").load_session(Path(directory))
self.workspace.set_current_key("records")
def _wire_signals(self) -> None:
self.connection_panel.connectRequested.connect(self._on_connect)
self.connection_panel.disconnectRequested.connect(self._on_disconnect)
@@ -4,10 +4,7 @@ One tab per task the operator does on the platform, with the motion status strip
living *outside* this widget so a tab change cannot hide the state that decides
whether it is safe to act.
Pages that are not built yet are present as labelled placeholders rather than
missing: the layout is being reviewed for operator use, and a tab that appears
only later would change the layout under review. Each placeholder names what
will fill it, so nobody mistakes an empty page for a broken one.
Calibration records share one page for coverage, sensitivity and window review.
"""
from __future__ import annotations
@@ -23,25 +20,23 @@ from PySide6.QtWidgets import (
from .axis_panel import AxisPanel
from .sequence_panel import SequencePanel
from .record_overview_dialog import RecordOverviewPage
#: Tab identity -> locale key for its label. Order is the display order, and it
#: follows the work: commission the axes, describe the sequence, watch the
#: sensor, look at the surface, then record calibration points.
#: sensor, capture data, then review calibration records.
TABS: tuple[tuple[str, str], ...] = (
("axes", "workspace.tab_axes"),
("sequence", "workspace.tab_sequence"),
("finger", "workspace.tab_finger"),
("sensor", "workspace.tab_sensor"),
("surface", "workspace.tab_surface"),
("points", "workspace.tab_points"),
("records", "workspace.tab_records"),
)
#: Tabs whose content does not exist yet, with the locale key that explains what
#: will appear there and when.
_PLACEHOLDERS: dict[str, str] = {
"sensor": "workspace.reserved_sensor",
"surface": "workspace.reserved_surface",
"points": "workspace.reserved_points",
}
@@ -93,6 +88,7 @@ class WorkspaceTabs(QTabWidget):
self._pages["finger"] = finger_panel
if sensor_panel is not None:
self._pages["sensor"] = sensor_panel
self._pages["records"] = RecordOverviewPage(self)
for key, locale_key in _PLACEHOLDERS.items():
if key in self._pages:
continue
@@ -134,6 +130,7 @@ class WorkspaceTabs(QTabWidget):
page = self._pages[key]
if isinstance(page, PlaceholderPage):
page.retranslate()
self._pages["records"].retranslate()
def _on_current_changed(self, index: int) -> None:
keys = self.page_keys()
+33 -5
View File
@@ -159,15 +159,43 @@ def test_overview_gui_plan_click_trial_filter_and_saved_roundtrip(qt_app, overvi
dialog.close()
def test_calibration_review_action_opens_record_overview(make_window, overview_session, monkeypatch):
from orisys_autocal.ui.record_overview_dialog import RecordOverviewDialog
def test_calibration_review_action_opens_records_tab(make_window, overview_session, monkeypatch):
from orisys_autocal.ui.calibration_panel import QFileDialog
root, _, _ = overview_session
opened = []
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args: str(root))
monkeypatch.setattr(RecordOverviewDialog, "exec", lambda dialog: opened.append(dialog))
window = make_window()
assert window.workspace.page_keys() == ["axes", "sequence", "finger", "sensor", "records"]
window.calibration_panel.review_button.click()
assert len(opened) == 1 and len(opened[0].rows) == 3
assert window.workspace.current_key() == "records"
page = window.workspace.page("records")
assert len(page.overview.rows) == 3
assert not page.overview.isWindow()
window.close()
def test_records_tab_browse_error_and_reload(make_window, overview_session, monkeypatch, qt_app, tmp_path):
from PySide6.QtCore import Qt
from PySide6.QtTest import QTest
from orisys_autocal.ui.record_overview_dialog import QFileDialog
root, _, _ = overview_session
window = make_window()
window.resize(1600, 1000)
window.workspace.set_current_key("records")
page = window.workspace.page("records")
assert page.overview is None and page.path_label.text()
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args: str(root))
page.open_button.click()
first = page.overview
page.load_session(root / "not-a-session")
assert page.overview is first and page.message.text()
page.open_button.click()
assert page.overview is not first and not page.message.text()
window.show()
qt_app.processEvents()
assert page.overview.isVisible()
QTest.keyClick(page.overview, Qt.Key.Key_Escape)
assert page.overview.isVisible()
assert window.grab().save(str(tmp_path / "records-tab.png"))
window.close()