"""The studio-side three-dimensional skeleton: importable, inert, Qt-safe. Acceptance for stage 0 (``docs/architecture/studio-3d-stages.md``): the module tree exists, it can be imported without PyOpenGL, and the one piece that must run before ``QApplication`` — the surface format — selects a complete profile. Nothing here renders. These tests are the floor a later stage must keep: the GL binding is imported only once a real context exists, and the stubs say which stage fills them in. """ from __future__ import annotations import sys import pytest @pytest.fixture def restore_surface_format(): """The default surface format is process-wide; put it back afterwards.""" from PySide6.QtGui import QSurfaceFormat previous = QSurfaceFormat.defaultFormat() try: yield finally: QSurfaceFormat.setDefaultFormat(previous) def test_the_skeleton_does_not_import_a_gl_binding(qt_app): """PyOpenGL must be imported for the platform Qt chose, not at import time. The binding decides GLX versus EGL when it first loads, so importing it while merely constructing the module tree would freeze a guess. The test fails if a future change adds a module-level ``import OpenGL``. """ for name in [name for name in sys.modules if name == "OpenGL" or name.startswith("OpenGL.")]: del sys.modules[name] import orisys_studio.scene3d.camera # noqa: F401 import orisys_studio.scene3d.coordinator # noqa: F401 import orisys_studio.scene3d.gl_backend # noqa: F401 import orisys_studio.scene3d.panel # noqa: F401 import orisys_studio.scene3d.widget # noqa: F401 loaded = [name for name in sys.modules if name == "OpenGL" or name.startswith("OpenGL.")] assert loaded == [], f"importing the skeleton pulled in {loaded}" def test_configure_surface_format_selects_a_complete_profile(qt_app, restore_surface_format): from orisys_studio.scene3d.gl_backend import configure_surface_format from PySide6.QtGui import QSurfaceFormat configure_surface_format("desktop33") desktop = QSurfaceFormat.defaultFormat() assert desktop.renderableType() == QSurfaceFormat.RenderableType.OpenGL assert (desktop.majorVersion(), desktop.minorVersion()) == (3, 3) assert desktop.profile() == QSurfaceFormat.OpenGLContextProfile.CoreProfile assert desktop.depthBufferSize() == 24 assert desktop.samples() == 0, "v1 does not pay for multisampling" configure_surface_format("gles30") embedded = QSurfaceFormat.defaultFormat() assert embedded.renderableType() == QSurfaceFormat.RenderableType.OpenGLES assert (embedded.majorVersion(), embedded.minorVersion()) == (3, 0) with pytest.raises(ValueError): configure_surface_format("vulkan") # type: ignore[arg-type] def test_the_widget_holds_cpu_state_and_touches_no_context(qt_app): """Constructing the widget must not need a current context or a GPU.""" from orisys_app_shared.scene3d.contracts import DisplaySettings from orisys_studio.scene3d.camera import OrbitCamera from orisys_studio.scene3d.gl_backend import GLRenderer from orisys_studio.scene3d.widget import Sensor3DWidget widget = Sensor3DWidget(GLRenderer(), OrbitCamera()) try: assert isinstance(widget, Sensor3DWidget) widget.set_settings(DisplaySettings(display_gain=3.0)) assert widget._settings.display_gain == 3.0 # Nothing has been presented yet, and no context was needed to get here. assert widget._frame is None finally: widget.deleteLater() def test_no_scene3d_entry_point_is_still_a_stub(qt_app): """The stage-0 floor, kept as a gate: nothing left says "not implemented". Every stage of the plan replaces stubs with the real thing, and a stub that survives its stage is worse than a missing feature: it looks implemented. This fails if a ``raise NotImplementedError`` comes back into either scene3d package, and it exercises the last entry points that used to be stubs (stage 4's panel) so "implemented" is also checked by calling them. """ from pathlib import Path import orisys_studio.scene3d from orisys_app_shared.scene3d.contracts import ( CameraPreset, DisplaySettings, FieldDescriptor, FrameIdentity, RenderStats, diagnostic, ) 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 roots = [Path(orisys_studio.scene3d.__file__).resolve().parent] import orisys_app_shared.scene3d as shared_scene3d roots.append(Path(shared_scene3d.__file__).resolve().parent) for root in roots: for path in sorted(root.rglob("*.py")): text = path.read_text(encoding="utf-8") assert "NotImplementedError" not in text or "__init__" in path.name, path if path.name == "__init__.py": # The package docstring may mention the word; code may not. assert "raise NotImplementedError" not in text, path camera = OrbitCamera() widget = Sensor3DWidget(GLRenderer(), camera) coordinator = ThreeDCoordinator(widget, composer=object(), executor=None, max_fps=30) # type: ignore[arg-type] panel = ThreeDPanel(coordinator, widget) try: # Stage 1 is real: the camera works with no context at all. camera.set_preset( CameraPreset("default", (0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0), 0.7) ) assert camera.matrices((100, 100)).view.shape == (4, 4) # Stage 3 is real too: the coordinator's schedule exists with no Qt event. assert coordinator.state == "disabled" assert coordinator.generation == 0 assert coordinator.superseded_frames == 0 coordinator.close() # idempotent, and safe before any scene is active # Stage 4: the panel is the operator's view of all of the above. panel.set_state("ready") assert panel.state_label.text() panel.set_descriptor(FieldDescriptor("flow_magnitude", "px", "approximate", False)) # The legend states the operator-facing name, not the internal key: the key is # what a recording carries, the label is what a person reads. legend = panel.legend_label.text() assert "flow_magnitude" not in legend, legend assert "光流幅值" in legend or "Optical flow magnitude" in legend, legend panel.set_descriptor(None) panel.set_render_stats(RenderStats(displayed_frames=3, superseded_frames=1)) assert "3" in panel.stats_label.text() panel.set_diagnostic(None) panel.set_diagnostic(diagnostic("field_unavailable", "no field in this frame")) assert "no field in this frame" in panel.legend_label.text() panel.set_scene(None) panel.set_enabled(False) settings = panel.settings() assert settings.max_fps == 30 assert settings.display_gain == 0.0 assert settings.housing_visible is False, ( "the panel's resting state and DisplaySettings' default must agree: the housing " "ships hidden, and the checkbox starts unchecked to match" ) assert settings.color_scale is not None assert settings.color_scale.mode == "automatic" assert DisplaySettings(max_fps=30).max_fps == settings.max_fps del FrameIdentity finally: widget.deleteLater()