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_sequence": "Test sequence",
"tab_finger": "Finger debug", "tab_finger": "Finger debug",
"tab_sensor": "Calibration capture", "tab_sensor": "Calibration capture",
"tab_surface": "Surface map", "tab_records": "Calibration records",
"tab_points": "Calibration points",
"not_built": "Not built yet", "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_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."
}, },
"source": { "source": {
"kind": "Input source", "kind": "Input source",
@@ -491,6 +488,8 @@
"flow_missing": "No valid displacement for this frame" "flow_missing": "No valid displacement for this frame"
}, },
"overview": { "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", "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.", "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", "coverage": "Acquisition status",
+4 -5
View File
@@ -225,12 +225,9 @@
"tab_sequence": "测试序列", "tab_sequence": "测试序列",
"tab_finger": "手指调试", "tab_finger": "手指调试",
"tab_sensor": "标定采集", "tab_sensor": "标定采集",
"tab_surface": "表面分布", "tab_records": "标定记录",
"tab_points": "标定点",
"not_built": "尚未实现", "not_built": "尚未实现",
"reserved_sensor": "传感器连接、实时图像与力曲线将在此显示:设备与信号源选择、预览、以及和 Studio 类似的曲线视图(占用空间更小)。", "reserved_sensor": "传感器连接、实时图像与力曲线将在此显示:设备与信号源选择、预览、以及和 Studio 类似的曲线视图(占用空间更小)。"
"reserved_surface": "多点标定的灵敏度将在此以色图叠加在传感器三维表面上:每个标定点的灵敏度经插值着色,并给出色标与范围。",
"reserved_points": "标定点列表与采集控制将在此显示:平台坐标、参考载荷、采样统计与灵敏度结果。"
}, },
"source": { "source": {
"kind": "输入来源", "kind": "输入来源",
@@ -491,6 +488,8 @@
"flow_missing": "本帧无有效位移信号" "flow_missing": "本帧无有效位移信号"
}, },
"overview": { "overview": {
"open_session": "打开标定会话…",
"empty": "请选择包含 session.json 的会话文件夹,查看已采集测点、法向响应和灵敏度;也可从标定采集页打开本场数据。",
"title": "标定记录总览", "title": "标定记录总览",
"notice": "查看实际采集覆盖与法向响应;点击 UV 点或表格联动。圆点为已确认窗口,方点为候选窗口;各 trial 用不同明暗度。拟合线为本测点全部符合条件的加载数据,筛选 trial 不会重新拟合。", "notice": "查看实际采集覆盖与法向响应;点击 UV 点或表格联动。圆点为已确认窗口,方点为候选窗口;各 trial 用不同明暗度。拟合线为本测点全部符合条件的加载数据,筛选 trial 不会重新拟合。",
"coverage": "采集状态", "coverage": "采集状态",
@@ -108,7 +108,7 @@ finger_F001_<时间>_<标识>/
主时间是电脑取得该帧时绑定的单调时间,UTC 用于追溯;处理完成时间不能代替采集时间。FT/ECAT 后续接入同源接收时间,按时间关联,不按行号配对;低延迟不等于零延迟。旧视频的 `host_replay` 是本次解码时间,只能调试,不能恢复原实验与参考设备的物理同步。 主时间是电脑取得该帧时绑定的单调时间,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.""" """One operator page. No motion commands are issued by this widget."""
loadBundleRequested = Signal() loadBundleRequested = Signal()
reviewRequested = Signal(str)
def __init__(self, source_panel: QWidget, parent: QWidget | None = None) -> None: def __init__(self, source_panel: QWidget, parent: QWidget | None = None) -> None:
super().__init__(parent) super().__init__(parent)
@@ -351,19 +352,13 @@ class CalibrationPanel(QWidget):
self._sync() self._sync()
def _review(self) -> None: def _review(self) -> None:
from .record_overview_dialog import RecordOverviewDialog
session = self.trial.session session = self.trial.session
directory = QFileDialog.getExistingDirectory( directory = QFileDialog.getExistingDirectory(
self, t("review.open"), self, t("review.open"),
str(session.directory) if session is not None else self.output_path.text(), str(session.directory) if session is not None else self.output_path.text(),
) )
if directory: if directory:
try: self.reviewRequested.emit(directory)
dialog = RecordOverviewDialog(Path(directory), self)
dialog.exec()
except (OSError, ValueError, KeyError, TypeError) as exc:
self.result_label.setText(t("calibration.error").format(detail=exc))
def shutdown(self) -> None: def shutdown(self) -> None:
self._timer.stop() self._timer.stop()
@@ -8,7 +8,7 @@ from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QPainter from PySide6.QtGui import QColor, QPainter
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QComboBox, QDialog, QFileDialog, QHBoxLayout, QHeaderView, QLabel, 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 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 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): class RecordOverviewDialog(QDialog):
def reject(self):
if self.isWindow():
super().reject()
def __init__(self, directory: Path, parent=None): def __init__(self, directory: Path, parent=None):
super().__init__(parent) super().__init__(parent)
self.directory = directory self.directory = directory
@@ -305,6 +305,7 @@ class AutoCalMainWindow(QMainWindow):
body, body,
sensor_panel=self.calibration_panel, 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 = QSplitter(Qt.Orientation.Horizontal, body)
self.main_splitter.addWidget(self.left_rail) self.main_splitter.addWidget(self.left_rail)
@@ -325,6 +326,10 @@ class AutoCalMainWindow(QMainWindow):
self._wire_signals() 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: def _wire_signals(self) -> None:
self.connection_panel.connectRequested.connect(self._on_connect) self.connection_panel.connectRequested.connect(self._on_connect)
self.connection_panel.disconnectRequested.connect(self._on_disconnect) 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 living *outside* this widget so a tab change cannot hide the state that decides
whether it is safe to act. whether it is safe to act.
Pages that are not built yet are present as labelled placeholders rather than Calibration records share one page for coverage, sensitivity and window review.
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.
""" """
from __future__ import annotations from __future__ import annotations
@@ -23,25 +20,23 @@ from PySide6.QtWidgets import (
from .axis_panel import AxisPanel from .axis_panel import AxisPanel
from .sequence_panel import SequencePanel 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 #: 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 #: 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], ...] = ( TABS: tuple[tuple[str, str], ...] = (
("axes", "workspace.tab_axes"), ("axes", "workspace.tab_axes"),
("sequence", "workspace.tab_sequence"), ("sequence", "workspace.tab_sequence"),
("finger", "workspace.tab_finger"), ("finger", "workspace.tab_finger"),
("sensor", "workspace.tab_sensor"), ("sensor", "workspace.tab_sensor"),
("surface", "workspace.tab_surface"), ("records", "workspace.tab_records"),
("points", "workspace.tab_points"),
) )
#: Tabs whose content does not exist yet, with the locale key that explains what #: Tabs whose content does not exist yet, with the locale key that explains what
#: will appear there and when. #: will appear there and when.
_PLACEHOLDERS: dict[str, str] = { _PLACEHOLDERS: dict[str, str] = {
"sensor": "workspace.reserved_sensor", "sensor": "workspace.reserved_sensor",
"surface": "workspace.reserved_surface",
"points": "workspace.reserved_points",
} }
@@ -93,6 +88,7 @@ class WorkspaceTabs(QTabWidget):
self._pages["finger"] = finger_panel self._pages["finger"] = finger_panel
if sensor_panel is not None: if sensor_panel is not None:
self._pages["sensor"] = sensor_panel self._pages["sensor"] = sensor_panel
self._pages["records"] = RecordOverviewPage(self)
for key, locale_key in _PLACEHOLDERS.items(): for key, locale_key in _PLACEHOLDERS.items():
if key in self._pages: if key in self._pages:
continue continue
@@ -134,6 +130,7 @@ class WorkspaceTabs(QTabWidget):
page = self._pages[key] page = self._pages[key]
if isinstance(page, PlaceholderPage): if isinstance(page, PlaceholderPage):
page.retranslate() page.retranslate()
self._pages["records"].retranslate()
def _on_current_changed(self, index: int) -> None: def _on_current_changed(self, index: int) -> None:
keys = self.page_keys() 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() dialog.close()
def test_calibration_review_action_opens_record_overview(make_window, overview_session, monkeypatch): def test_calibration_review_action_opens_records_tab(make_window, overview_session, monkeypatch):
from orisys_autocal.ui.record_overview_dialog import RecordOverviewDialog
from orisys_autocal.ui.calibration_panel import QFileDialog from orisys_autocal.ui.calibration_panel import QFileDialog
root, _, _ = overview_session root, _, _ = overview_session
opened = []
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args: str(root)) monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args: str(root))
monkeypatch.setattr(RecordOverviewDialog, "exec", lambda dialog: opened.append(dialog))
window = make_window() window = make_window()
assert window.workspace.page_keys() == ["axes", "sequence", "finger", "sensor", "records"]
window.calibration_panel.review_button.click() 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() window.close()