"""The 3D pane's interface: a picture, one legend line, and a settings dialog. The pane used to be half form fields, and that is what left the 3D view occupying a corner of its own splitter slot. Everything configurable now lives in ``Scene3dSettingsDialog``; what stays under the picture is what *reading* it needs — the colour ramp, the numbers it was drawn with, and which source produced the field. These tests pin three things: the pane shows no settings, the dialog holds the **same** control widgets the panel has always had (so the settings path did not fork), and the view menu / the pane's header both reach that dialog. """ from __future__ import annotations import pytest from PySide6.QtCore import Signal from PySide6.QtWidgets import QWidget from orisys_app_shared.i18n import t from orisys_studio.ui import icons @pytest.fixture def panel(qt_app): """A ``ThreeDPanel`` with the coordinator and widget it needs, and no scene.""" from orisys_studio.scene3d.camera import OrbitCamera from orisys_studio.scene3d.coordinator import ThreeDCoordinator from orisys_studio.scene3d.gl_backend import GLRenderer from orisys_studio.scene3d.panel import ThreeDPanel from orisys_studio.scene3d.widget import Sensor3DWidget widget = Sensor3DWidget(GLRenderer(), OrbitCamera()) coordinator = ThreeDCoordinator(widget, composer=object(), executor=None, max_fps=30) created = ThreeDPanel(coordinator, widget) yield created coordinator.close() created.deleteLater() # --------------------------------------------------------------------- the pane def test_the_pane_holds_a_legend_and_no_settings(panel): """What is on the pane has to be readable, not adjustable.""" # The readouts are part of the pane itself... assert panel.legend_label.parent() is panel assert panel.source_label.parent() is panel assert panel.scale_bar.parent() is panel assert panel.scale_lower_label.parent() is panel assert panel.scale_upper_label.parent() is panel # ...and every setting is in one block that the pane never shows. assert panel.controls_widget.parent() is panel assert not panel.controls_widget.isVisible() for control in ( panel.enable_check, panel.preset_combo, panel.housing_check, panel.deform_check, panel.gain_spin, panel.scale_combo, panel.fps_combo, panel.reset_button, ): assert control.parent() is panel.controls_widget, control # The engineering counters went with the settings rather than under the picture. assert panel.stats_label.parent() is panel.controls_widget def test_the_settings_dialog_hosts_the_panes_own_controls(panel): """One set of widgets either way: the dialog moves them, it does not copy them.""" from orisys_studio.scene3d.settings_dialog import Scene3dSettingsDialog dialog = Scene3dSettingsDialog(panel) try: assert panel.controls_widget.parent() is dialog, ( "the dialog kept a second copy of the controls" ) assert panel.controls_widget.isVisibleTo(dialog) assert not dialog.isModal(), "the pane has to stay visible while tuning it" assert dialog.windowTitle() == t("scene3d.settings_title") # ...and they still work from there: the signal path is the panel's own. seen: list[bool] = [] panel.enabledChanged.connect(seen.append) panel.enable_check.setChecked(True) assert seen == [True], "a control in the dialog stopped reaching the view" # Closing hides it: the controls have to survive for the next time. dialog.close() assert not dialog.isVisible() assert panel.enable_check.text(), "closing the dialog destroyed its controls" assert panel.settings().max_fps == 30 finally: dialog.deleteLater() def test_the_pane_header_offers_the_two_actions(qt_app): """A gear and a view reset: the two things that belong to *looking*.""" from orisys_studio.ui.mesh3d_panel import Mesh3dPanel assert "reset_view" in icons.NAMES for theme in ("dark", "light"): assert not icons.icon("reset_view", theme).isNull() pane = Mesh3dPanel(theme="dark") try: assert pane.btn_settings.toolTip() == t("panels.mesh3d_settings_tip") assert pane.btn_reset_view.toolTip() == t("panels.mesh3d_reset_tip") assert not pane.btn_settings.icon().isNull() assert not pane.btn_reset_view.icon().isNull() # A box sensor has no scene, so there is nothing to configure and the pane says # so instead of opening an empty dialog. pane.set_sensor("box") assert pane.open_settings() is False assert pane.view is None # Reset forwards to the view when there is one. class _View(QWidget): diagnosticRaised = Signal(object) enabledChanged = Signal(bool) def __init__(self) -> None: super().__init__() self.resets = 0 self.available = True def set_source_kind(self, kind: str) -> None: self.source_kind = kind def set_enabled(self, enabled: bool) -> None: self.enabled = bool(enabled) def restore_settings(self, settings) -> None: self.settings_value = settings def reset_view(self) -> None: self.resets += 1 view = _View() pane.attach_view(view) # type: ignore[arg-type] pane.reset_view() assert view.resets == 1 finally: pane.deleteLater() # --------------------------------------------------------------- which quantity def test_the_pane_offers_the_two_quantities_and_says_which_one_it_drew(panel): """The choice is the operator's; a frame that cannot honour it says so.""" from orisys_app_shared.scene3d.contracts import FieldDescriptor assert [panel.field_combo.itemData(i) for i in range(panel.field_combo.count())] == [ "depth", "flow", ] assert panel.field() == "depth", "the depth response is what the pane offers first" assert panel.settings().field == "depth" # The chosen quantity is drawn: no note. panel.set_descriptor(FieldDescriptor("depth_response", "1", "approximate", False)) assert not panel.field_note.isVisible() or not panel.field_note.text() # The other one is drawn instead: the note names both, and the legend still shows # what is really on screen (the range is for the field that was drawn). panel.set_diagnostics_visible(True) panel.set_descriptor(FieldDescriptor("flow_magnitude", "px", "approximate", False)) note = panel.field_note.text() assert t("scene3d.field_depth") in note and t("scene3d.field_flow") in note, note assert "flow_magnitude" not in panel.legend_label.text() # Choosing flow makes that frame the honoured one, and the note goes away. index = panel.field_combo.findData("flow") panel.field_combo.setCurrentIndex(index) assert panel.settings().field == "flow" panel.set_descriptor(FieldDescriptor("flow_magnitude", "px", "approximate", False)) assert not panel.field_note.isVisible() or not panel.field_note.text() # A restored preference moves the combo (and therefore the adapter). from orisys_app_shared.scene3d.contracts import DisplaySettings panel.apply_settings(DisplaySettings(field="depth")) assert panel.field() == "depth" def test_the_diagnostics_switch_governs_what_the_pane_explains(panel): """Warnings are claims about the measurement and always show; info is explanation.""" from orisys_app_shared.scene3d.contracts import FieldDescriptor, RenderStats, diagnostic panel.set_descriptor(FieldDescriptor("flow_magnitude", "px", "approximate", False)) panel.set_render_stats( RenderStats(scale_lower=0.25, scale_upper=2.5, scale_mode="automatic") ) panel.set_diagnostics_visible(False) assert not panel.scale_bar.isHidden() assert panel.scale_lower_label.text() == "0.25" assert panel.scale_upper_label.text() == "2.5" assert not panel.scale_lower_label.isHidden() assert not panel.scale_upper_label.isHidden() assert panel.legend_label.isHidden() assert panel.source_label.isHidden() assert panel.state_label.isHidden() assert panel.field_note.isHidden() panel.set_diagnostic(diagnostic("field_stale", "the array is from another frame")) assert "another frame" in panel.legend_label.text(), ( "a warning about the measurement was hidden" ) panel.set_diagnostic(diagnostic("geometry_unverified", "the scene was not compared")) assert "not compared" in panel.legend_label.text(), ( "a claim about verification was hidden" ) panel.set_diagnostic(diagnostic("stale_frame", "the picture is not current")) assert "not current" not in panel.legend_label.text(), ( "an informational line survived diagnostics being off" ) assert panel.legend_label.isHidden() panel.set_diagnostics_visible(True) assert not panel.legend_label.isHidden() assert not panel.source_label.isHidden() assert not panel.state_label.isHidden() panel.set_diagnostic(diagnostic("stale_frame", "the picture is not current")) assert "not current" in panel.legend_label.text() def test_the_2d_footer_shows_the_picture_and_hides_the_pipeline(make_window): """One line under the canvas: what the picture is, not how it was made.""" from PySide6.QtWidgets import QApplication window = make_window() window.show() QApplication.processEvents() try: arrow = window.arrow_panel arrow.set_plan(None) arrow.set_diagnostics_visible(False) assert not arrow.scale_label.isVisible() assert not arrow.pipeline_label.isVisible() assert not arrow.area_label.isVisible() arrow.set_diagnostics_visible(True) assert arrow.pipeline_label.isVisible() or not arrow.view_modes(), ( "the pipeline line came back" ) # The window's switch is the same thing, and it is remembered. window.act_show_diagnostics.setChecked(True) QApplication.processEvents() assert window.display_ctl.show_diagnostics is True assert arrow.diagnostics_visible() is True assert window.prefs["show_diagnostics"] is True finally: window.close() # --------------------------------------------------------------------- the menu def test_the_view_menu_opens_the_3d_settings(make_window): """视图 ▸ 三维显示设置… is the way in that does not depend on the pane's header.""" from PySide6.QtWidgets import QApplication window = make_window(sensor_type="finger", show_finger_3d=True) window.show() QApplication.processEvents() try: assert window.act_scene3d_settings.text() == t("menu.scene3d_settings") assert window.act_scene3d_settings in window.view_menu.actions() view = window.mesh3d_panel.view if view is None or not view.available: # No GL here: the entry has to say why instead of doing nothing. window.act_scene3d_settings.trigger() QApplication.processEvents() assert window.statusBar().currentMessage().strip() return window.mesh3d_panel.ensure_view() window.act_scene3d_settings.trigger() QApplication.processEvents() dialog = window.mesh3d_panel.view.settings_dialog() assert dialog is not None and dialog.isVisible() dialog.hide() finally: window.close()