149 lines
6.0 KiB
Python
149 lines
6.0 KiB
Python
"""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_stubs_name_the_stage_that_fills_them_in(qt_app):
|
|
"""What is implemented is exercised elsewhere; what is not says so.
|
|
|
|
Stage 1 landed the camera, the dispatcher and the renderer (see
|
|
``test_scene3d_stage1.py`` and ``test_scene3d_gl_smoke.py``). The remaining
|
|
placeholders must still fail loudly with the stage that owns them rather than
|
|
quietly doing nothing.
|
|
"""
|
|
from orisys_app_shared.scene3d.contracts import (
|
|
CameraPreset,
|
|
DisplaySettings,
|
|
ExpectedGeometry,
|
|
FrameIdentity,
|
|
)
|
|
from orisys_studio.scene3d.adapters import BoxFrameAdapter, FingerFrameAdapter
|
|
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:
|
|
assert coordinator.state == "disabled"
|
|
assert coordinator.generation == 0
|
|
assert coordinator.superseded_frames == 0 # the mailbox is real, not a stub
|
|
|
|
# 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)
|
|
|
|
for call, stage in (
|
|
(lambda: coordinator.activate(None, "live-A"), "stage 3"),
|
|
(lambda: coordinator.set_settings(DisplaySettings()), "stage 3"),
|
|
(lambda: coordinator.set_visible(True), "stage 3"),
|
|
(lambda: coordinator.close(), "stage 3"),
|
|
(lambda: panel.set_state("ready"), "stage 4"),
|
|
(lambda: BoxFrameAdapter().snapshot(None, FrameIdentity("a", 1, 0, 0.0, 1), None),
|
|
"stage 3"),
|
|
(
|
|
lambda: FingerFrameAdapter().snapshot(
|
|
None, None, FrameIdentity("a", 1, 0, 0.0, 1)
|
|
),
|
|
"stage 3",
|
|
),
|
|
):
|
|
with pytest.raises(NotImplementedError) as excinfo:
|
|
call()
|
|
assert stage in str(excinfo.value), (stage, str(excinfo.value))
|
|
del ExpectedGeometry
|
|
finally:
|
|
widget.deleteLater()
|