"""Viewport orientation triad is present and survives every re-render path. Before this change the 3D viewport had no X/Y/Z corner marker — users who rotated the camera off the default isometric view lost track of which world axis was which. The triad is now built once during the canvas's scene-furniture setup and lives in screen-space via PyVista's ``vtkOrientationMarkerWidget``, so it is not affected by the regular actor rebuilds in :meth:`ModelRenderer.render` / :meth:`ModelRenderer._teardown_all`. These tests pin the contract: - Triad exists right after ``MainWindow`` construction. - It still exists after a tree refresh (which calls ``ModelRenderer.render`` through ``MainWindow._on_project_changed``). - It still exists after loading an empty project, then a small project with a node — the most common real-world re-render path. - The triad carries the conventional X / Y / Z labels. - It is pinned to the lower-left viewport corner. """ from __future__ import annotations import pytest pytest.importorskip("PySide6") from otko.core import Node @pytest.mark.gui def test_axes_widget_exists_after_window_construction(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.main_window import MainWindow mw = MainWindow() qtbot.addWidget(mw) aw = mw._canvas.renderer.axes_widget assert aw is not None # PyVista binds a vtkOrientationMarkerWidget; the orientation marker # it carries is a vtkAxesActor with X/Y/Z label text. actor = aw.GetOrientationMarker() assert actor.GetXAxisLabelText() == "X" assert actor.GetYAxisLabelText() == "Y" assert actor.GetZAxisLabelText() == "Z" # Pinned to the lower-left viewport patch (normalized 0..1 coords). vp = aw.GetViewport() assert vp[0] == pytest.approx(0.0) assert vp[1] == pytest.approx(0.0) @pytest.mark.gui def test_axes_widget_survives_tree_refresh(qtbot) -> None: # type: ignore[no-untyped-def] """``MainWindow._refresh_tree`` triggers a renderer rebuild via ``_on_project_changed``; the orientation widget must stay put.""" from otko.views.main_window import MainWindow mw = MainWindow() qtbot.addWidget(mw) aw_before = mw._canvas.renderer.axes_widget mw._vm.new_project(ndm=3, ndf=6) mw._vm.project.nodes.extend( [ Node(id=1, coords=(0, 0, 0)), Node(id=2, coords=(3, 0, 0)), ] ) mw._refresh_tree(mw._vm.project) aw_after = mw._canvas.renderer.axes_widget # Same widget instance — proves the marker survived the rebuild. assert aw_after is aw_before actor = aw_after.GetOrientationMarker() assert actor.GetXAxisLabelText() == "X" assert actor.GetYAxisLabelText() == "Y" assert actor.GetZAxisLabelText() == "Z" @pytest.mark.gui def test_axes_widget_survives_empty_then_loaded_project(qtbot) -> None: # type: ignore[no-untyped-def] """The two re-render paths the engineer hits during a session: empty project → first node added → tree refreshed. The triad must still be in the lower-left corner with all three labels.""" from otko.views.main_window import MainWindow mw = MainWindow() qtbot.addWidget(mw) aw_before = mw._canvas.renderer.axes_widget assert aw_before is not None # Empty project — model renderer builds no actors, just the grid. mw._vm.new_project(ndm=3, ndf=6) mw._refresh_tree(mw._vm.project) assert mw._canvas.renderer.axes_widget is aw_before # Small project with one node — exercises the node-actor rebuild path. mw._vm.project.nodes.append(Node(id=1, coords=(1, 2, 3))) mw._refresh_tree(mw._vm.project) assert mw._canvas.renderer.axes_widget is aw_before # Same after a full canvas refresh too — show_project rebuilds actors. mw._canvas.show_project(mw._vm.project) assert mw._canvas.renderer.axes_widget is aw_before actor = mw._canvas.renderer.axes_widget.GetOrientationMarker() assert actor.GetXAxisLabelText() == "X" assert actor.GetYAxisLabelText() == "Y" assert actor.GetZAxisLabelText() == "Z"