autocal: 测试序列页与几何与安装页共用 2D 平面图和 3D 预览
- FingerPanel 新增 release_views/restore_views,把表面选点图和三维预览 整体借出,不复制,已拾取的点、传感器包和 GL 视图仍是同一份状态 - SequencePanel 用共用视图替换“仿真阶段实现”的占位框(adopt_views/release_views) - 主窗口按当前标签页把两个视图挂到序列页或几何页 - 新增 tests/test_shared_views.py - 序列页借用视图时给两个框固定最小高度并隐藏长图例,修复 3D 视图下按钮与勾选框叠压
This commit is contained in:
@@ -261,6 +261,9 @@ class FingerPanel(QGroupBox):
|
|||||||
|
|
||||||
body = QHBoxLayout()
|
body = QHBoxLayout()
|
||||||
body.setSpacing(10)
|
body.setSpacing(10)
|
||||||
|
self._body = body
|
||||||
|
#: True while the 2D picker and 3D preview are shown on the sequence page.
|
||||||
|
self._views_lent = False
|
||||||
picker_box = self._build_picker()
|
picker_box = self._build_picker()
|
||||||
readout_box = self._build_readouts()
|
readout_box = self._build_readouts()
|
||||||
preview_box = self._build_preview()
|
preview_box = self._build_preview()
|
||||||
@@ -279,6 +282,44 @@ class FingerPanel(QGroupBox):
|
|||||||
self.set_tool_xy(*tool_xy_mm)
|
self.set_tool_xy(*tool_xy_mm)
|
||||||
self.retranslate()
|
self.retranslate()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- shared views
|
||||||
|
|
||||||
|
def release_views(self) -> tuple[QWidget, QWidget]:
|
||||||
|
"""Hand the 2D picker and the 3D preview to another page.
|
||||||
|
|
||||||
|
The sequence page needs the same two views, and a widget has one parent, so
|
||||||
|
they are *moved* rather than copied: one picked point, one bundle and one GL
|
||||||
|
context stay the single source of truth. Whoever takes them must give them
|
||||||
|
back through :meth:`restore_views`.
|
||||||
|
"""
|
||||||
|
if not self._views_lent:
|
||||||
|
self._body.removeWidget(self._picker_box)
|
||||||
|
self._body.removeWidget(self._preview_box)
|
||||||
|
self._views_lent = True
|
||||||
|
# Short on height elsewhere, the colour key goes to the tooltip only.
|
||||||
|
self.legend_label.setVisible(False)
|
||||||
|
return self._picker_box, self._preview_box
|
||||||
|
|
||||||
|
def restore_views(self) -> None:
|
||||||
|
"""Put the two views back in this page, after the readouts between them."""
|
||||||
|
if not self._views_lent:
|
||||||
|
return
|
||||||
|
self._body.insertWidget(0, self._picker_box, 2)
|
||||||
|
self._body.insertWidget(2, self._preview_box, 6)
|
||||||
|
self.legend_label.setVisible(True)
|
||||||
|
for box in (self._picker_box, self._preview_box):
|
||||||
|
layout = box.layout()
|
||||||
|
if layout is not None:
|
||||||
|
layout.invalidate()
|
||||||
|
layout.activate()
|
||||||
|
self._picker_box.show()
|
||||||
|
self._preview_box.show()
|
||||||
|
self._views_lent = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def views_lent(self) -> bool:
|
||||||
|
return self._views_lent
|
||||||
|
|
||||||
# ------------------------------------------------------------------ widgets
|
# ------------------------------------------------------------------ widgets
|
||||||
|
|
||||||
def _build_toolbar(self, bundle_path: str) -> QWidget:
|
def _build_toolbar(self, bundle_path: str) -> QWidget:
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ VALIDATION_CHECKS: tuple[tuple[str, str], ...] = (
|
|||||||
("contact_confirmable", "sequence.check_contact"),
|
("contact_confirmable", "sequence.check_contact"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: Height the shared 2D and 3D boxes get on this page, in pixels.
|
||||||
|
SHARED_VIEW_MIN_HEIGHT = 420
|
||||||
|
|
||||||
#: Placeholder rows, so the flow shows its shape. Marked as unmeasured: a row
|
#: Placeholder rows, so the flow shows its shape. Marked as unmeasured: a row
|
||||||
#: with no status is a design, not a measurement.
|
#: with no status is a design, not a measurement.
|
||||||
_EXAMPLE_POINTS: tuple[tuple[float, float, float], ...] = (
|
_EXAMPLE_POINTS: tuple[tuple[float, float, float], ...] = (
|
||||||
@@ -164,6 +167,8 @@ class SequencePanel(QGroupBox):
|
|||||||
self._check_rows: dict[str, QLabel] = {}
|
self._check_rows: dict[str, QLabel] = {}
|
||||||
self._summary: dict[str, Any] = {}
|
self._summary: dict[str, Any] = {}
|
||||||
self._dirty = False
|
self._dirty = False
|
||||||
|
#: The 2D picker and 3D preview, while the geometry page has lent them.
|
||||||
|
self._views: tuple[QWidget, QWidget] | None = None
|
||||||
|
|
||||||
outer = QVBoxLayout(self)
|
outer = QVBoxLayout(self)
|
||||||
outer.setContentsMargins(10, 8, 10, 10)
|
outer.setContentsMargins(10, 8, 10, 10)
|
||||||
@@ -267,6 +272,13 @@ class SequencePanel(QGroupBox):
|
|||||||
self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
column.addWidget(self.preview_label, 1)
|
column.addWidget(self.preview_label, 1)
|
||||||
|
|
||||||
|
# The geometry page's two views are shown here while this page is open:
|
||||||
|
# a sequence is designed against the surface and the pose, not a table.
|
||||||
|
self._view_row = QHBoxLayout()
|
||||||
|
self._view_row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self._view_row.setSpacing(8)
|
||||||
|
column.addLayout(self._view_row, 1)
|
||||||
|
|
||||||
self.preview_hint = QLabel(box)
|
self.preview_hint = QLabel(box)
|
||||||
self.preview_hint.setObjectName("hintLabel")
|
self.preview_hint.setObjectName("hintLabel")
|
||||||
self.preview_hint.setWordWrap(True)
|
self.preview_hint.setWordWrap(True)
|
||||||
@@ -274,6 +286,47 @@ class SequencePanel(QGroupBox):
|
|||||||
self._preview_box = box
|
self._preview_box = box
|
||||||
return box
|
return box
|
||||||
|
|
||||||
|
def adopt_views(self, picker: QWidget, preview: QWidget) -> None:
|
||||||
|
"""Show the shared 2D picker and 3D preview in place of the placeholder."""
|
||||||
|
if self._views is not None:
|
||||||
|
return
|
||||||
|
self._views = (picker, preview)
|
||||||
|
self.preview_label.setVisible(False)
|
||||||
|
# An explicit floor. Left to its contents the box was allotted less height
|
||||||
|
# than its rows (a 3D view that will not shrink below 240, check boxes,
|
||||||
|
# buttons) need, and the layout then drew them on top of each other.
|
||||||
|
for widget in (picker, preview):
|
||||||
|
widget.setMinimumHeight(SHARED_VIEW_MIN_HEIGHT)
|
||||||
|
self._view_row.addWidget(picker, 2)
|
||||||
|
self._view_row.addWidget(preview, 3)
|
||||||
|
picker.show()
|
||||||
|
preview.show()
|
||||||
|
self._relayout(preview)
|
||||||
|
|
||||||
|
def release_views(self) -> bool:
|
||||||
|
"""Stop showing the shared views; the caller puts them back where they live."""
|
||||||
|
if self._views is None:
|
||||||
|
return False
|
||||||
|
for widget in self._views:
|
||||||
|
self._view_row.removeWidget(widget)
|
||||||
|
widget.setMinimumHeight(0)
|
||||||
|
self._views = None
|
||||||
|
self.preview_label.setVisible(True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _relayout(widget: QWidget) -> None:
|
||||||
|
"""Recompute a moved widget's inner layout for its new size."""
|
||||||
|
layout = widget.layout()
|
||||||
|
if layout is not None:
|
||||||
|
layout.invalidate()
|
||||||
|
layout.activate()
|
||||||
|
widget.updateGeometry()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_views(self) -> bool:
|
||||||
|
return self._views is not None
|
||||||
|
|
||||||
# --------------------------------------------------------------- validation
|
# --------------------------------------------------------------- validation
|
||||||
|
|
||||||
def _build_validation(self) -> QWidget:
|
def _build_validation(self) -> QWidget:
|
||||||
|
|||||||
@@ -328,6 +328,9 @@ class AutoCalMainWindow(QMainWindow):
|
|||||||
sensor_panel=self.calibration_panel,
|
sensor_panel=self.calibration_panel,
|
||||||
)
|
)
|
||||||
self.calibration_panel.reviewRequested.connect(self._on_review_records)
|
self.calibration_panel.reviewRequested.connect(self._on_review_records)
|
||||||
|
# The geometry page's 2D and 3D views are also the sequence page's.
|
||||||
|
self.workspace.pageChanged.connect(self._place_shared_views)
|
||||||
|
self._place_shared_views(self.workspace.current_key())
|
||||||
|
|
||||||
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)
|
||||||
@@ -405,6 +408,18 @@ class AutoCalMainWindow(QMainWindow):
|
|||||||
|
|
||||||
# ----------------------------------------------------------- device setup
|
# ----------------------------------------------------------- device setup
|
||||||
|
|
||||||
|
def _place_shared_views(self, key: str) -> None:
|
||||||
|
"""Show the surface picker and 3D preview on whichever of the two pages is open.
|
||||||
|
|
||||||
|
One set of widgets, so a point picked on either page, the loaded bundle and
|
||||||
|
the simulation are the same state; only the parent changes with the tab.
|
||||||
|
"""
|
||||||
|
if key == "sequence":
|
||||||
|
picker, preview = self.finger_panel.release_views()
|
||||||
|
self.sequence_panel.adopt_views(picker, preview)
|
||||||
|
elif self.sequence_panel.release_views() or self.finger_panel.views_lent:
|
||||||
|
self.finger_panel.restore_views()
|
||||||
|
|
||||||
def _show_device_setup(self, tab: str | None = None) -> None:
|
def _show_device_setup(self, tab: str | None = None) -> None:
|
||||||
"""Open the connection/bus dialog, optionally on one of its tabs."""
|
"""Open the connection/bus dialog, optionally on one of its tabs."""
|
||||||
self.device_setup.open_for(tab)
|
self.device_setup.open_for(tab)
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""The surface picker and 3D preview are one set of widgets shared by two pages."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def _inside(widget, page) -> bool:
|
||||||
|
while widget is not None:
|
||||||
|
if widget is page:
|
||||||
|
return True
|
||||||
|
widget = widget.parentWidget()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_views_follow_the_open_tab(make_window):
|
||||||
|
window = make_window()
|
||||||
|
finger, sequence = window.finger_panel, window.sequence_panel
|
||||||
|
preview, picker = finger.preview, finger.view
|
||||||
|
|
||||||
|
window.workspace.set_current_key("sequence")
|
||||||
|
assert _inside(preview, sequence) and _inside(picker, sequence)
|
||||||
|
assert not sequence.preview_label.isVisibleTo(sequence)
|
||||||
|
assert finger.views_lent
|
||||||
|
|
||||||
|
window.workspace.set_current_key("finger")
|
||||||
|
assert _inside(preview, finger) and _inside(picker, finger)
|
||||||
|
assert sequence.preview_label.isVisibleTo(sequence) or not sequence.has_views
|
||||||
|
assert not finger.views_lent
|
||||||
|
|
||||||
|
# Going back and forth never creates a second copy.
|
||||||
|
window.workspace.set_current_key("sequence")
|
||||||
|
window.workspace.set_current_key("axes")
|
||||||
|
assert finger.preview is preview and finger.view is picker
|
||||||
|
assert _inside(preview, finger)
|
||||||
|
window.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_point_picked_on_the_sequence_page_is_the_geometry_pages_point(make_window):
|
||||||
|
window = make_window()
|
||||||
|
window.workspace.set_current_key("sequence")
|
||||||
|
seen: list[tuple[int, int]] = []
|
||||||
|
window.finger_panel.pickRequested.connect(lambda x, y: seen.append((x, y)))
|
||||||
|
window.finger_panel.view.pixelPicked.emit(10, 20)
|
||||||
|
assert seen == [(10, 20)]
|
||||||
|
window.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_sequence_page_gives_the_views_room_and_drops_the_long_legend(make_window):
|
||||||
|
window = make_window()
|
||||||
|
finger, sequence = window.finger_panel, window.sequence_panel
|
||||||
|
window.workspace.set_current_key("sequence")
|
||||||
|
assert finger.preview.parentWidget().minimumHeight() >= 400
|
||||||
|
assert not finger.legend_label.isVisibleTo(finger.legend_label.parentWidget())
|
||||||
|
window.workspace.set_current_key("finger")
|
||||||
|
assert finger.preview.parentWidget().minimumHeight() == 0
|
||||||
|
assert not finger.legend_label.isHidden()
|
||||||
|
window.close()
|
||||||
Reference in New Issue
Block a user