feat: named case-result load combinations with full GUI support
Snapshots the current development tree, headlined by proper load combinations (user request): a reusable LoadCombination entity of weighted completed static-case results (e.g. 1.2xDead + 1.6xLive). - core: LoadCombination/LoadCombinationItem entities, Project integration (lookup, unique ids, reference validation) - services: combinations.py (linear superposition + envelope), exported via services __init__ - commands: undoable Add/Delete/Update for combinations - GUI: Load Combinations manager dialog, Run-dialog evaluation, envelope display in Results panel, Combinations tab in Table dock - tests: unit coverage (validation, math, error paths) + integration superposition check vs a single factored run
This commit is contained in:
commit
f361fee969
560 changed files with 178701 additions and 0 deletions
107
tests/gui/test_viewport_axes.py
Normal file
107
tests/gui/test_viewport_axes.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"""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"
|
||||
Loading…
Reference in a new issue