"""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 PyQt5.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 PyQt5.QtGui import QSurfaceFormat from orisys_studio.scene3d.gl_backend import configure_surface_format configure_surface_format("desktop33") desktop = QSurfaceFormat.defaultFormat() assert desktop.renderableType() == QSurfaceFormat.OpenGL assert (desktop.majorVersion(), desktop.minorVersion()) == (3, 3) assert desktop.profile() == QSurfaceFormat.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.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, FrameIdentity 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_the_remaining_stub_says_which_stage_fills_it_in(qt_app): """What is implemented is exercised elsewhere; what is not says so. Stages 1 to 3 landed the camera, the dispatcher, the renderer, the sampler, the composer, the schedule and the adapters. ``ThreeDPanel`` is the one piece left, and it must still fail loudly with the stage that owns it rather than quietly doing nothing. """ from orisys_app_shared.scene3d.contracts import ( CameraPreset, DisplaySettings, FrameIdentity, RenderStats, ) 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 camera = OrbitCamera() renderer = GLRenderer() widget = Sensor3DWidget(renderer, 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 for call, stage in ( (lambda: panel.set_state("ready"), "stage 4"), (lambda: panel.set_render_stats(RenderStats()), "stage 4"), (lambda: panel.set_diagnostic(None), "stage 4"), ): with pytest.raises(NotImplementedError) as excinfo: call() assert stage in str(excinfo.value), (stage, str(excinfo.value)) del DisplaySettings, FrameIdentity finally: widget.deleteLater()