fix(autocal): 将标定记录总览接入主窗口
合并旧占位页为标定记录页,并统一采集页的数据查看入口。通过45项相关测试、离屏布局检查及代码和文档门禁。
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user