otko/tests/unit/test_diagram_renderer.py

133 lines
4.7 KiB
Python
Raw Normal View History

2026-09-08 02:12:15 -04:00
"""Smoke tests for the force-diagram overlay renderer.
These verify the renderer doesn't crash on representative inputs and
produces an actor when there's data to draw. Geometry/colour assertions
are kept minimal — they'd be brittle and the visual is the spec anyway.
"""
from __future__ import annotations
import os
import numpy as np
import pytest
# Force pyvista off-screen before any pyvista import in this module's chain.
os.environ.setdefault("PYVISTA_OFF_SCREEN", "true")
import pyvista as pv # noqa: E402
from otko.core import ( # noqa: E402
ElasticBeamColumn,
ElasticSection,
Node,
Project,
)
from otko.services.element_forces import ( # noqa: E402
DiagramData,
ForceComponent,
extract_diagram_data,
)
from otko.services.results import StaticResults # noqa: E402
from otko.views.canvas3d.diagram_renderer import DiagramRenderer # noqa: E402
pv.OFF_SCREEN = True
@pytest.fixture
def offscreen_plotter(): # type: ignore[no-untyped-def]
p = pv.Plotter(off_screen=True)
yield p
p.close()
@pytest.fixture
def project_3d() -> Project:
return Project(
nodes=[
Node(id=1, coords=(0.0, 0.0, 0.0)),
Node(id=2, coords=(3.0, 0.0, 0.0)),
Node(id=3, coords=(6.0, 0.0, 0.0)),
],
sections=[ElasticSection(id=1, E=200e9, A=0.01, Iz=1e-5, Iy=1e-5)],
elements=[
ElasticBeamColumn(id=10, nodes=(1, 2), section_id=1),
ElasticBeamColumn(id=20, nodes=(2, 3), section_id=1),
],
)
@pytest.fixture
def static_results() -> StaticResults:
f10 = np.array([[100.0, 5.0, 0.0, 0.0, 0.0, 9.0,
-100.0, -5.0, 0.0, 0.0, 0.0, -9.0]])
f20 = np.array([[-50.0, 2.0, 0.0, 0.0, 0.0, 3.0,
50.0, -2.0, 0.0, 0.0, 0.0, -3.0]])
return StaticResults(case_id=1, case_name="t", n_steps=1,
element_forces={10: f10, 20: f20})
def test_render_axial_creates_actor(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
data = extract_diagram_data(project_3d, static_results, ForceComponent.N)
r.render(project_3d, data, scale=0.01)
assert r._actor is not None
def test_render_shear_creates_actor(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
data = extract_diagram_data(project_3d, static_results, ForceComponent.V2)
r.render(project_3d, data, scale=0.05)
assert r._actor is not None
def test_render_moment_creates_actor(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
data = extract_diagram_data(project_3d, static_results, ForceComponent.M3)
r.render(project_3d, data, scale=0.05)
assert r._actor is not None
def test_render_empty_data_does_not_create_actor(offscreen_plotter, project_3d) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
empty = DiagramData(
component=ForceComponent.N,
element_ids=np.empty(0, dtype=int),
values_i=np.empty(0), values_j=np.empty(0), abs_max=0.0,
)
r.render(project_3d, empty, scale=1.0)
assert r._actor is None
def test_render_zero_magnitude_data_does_not_create_actor(
offscreen_plotter, project_3d,
) -> None: # type: ignore[no-untyped-def]
"""A diagram for a component that's identically zero shouldn't render
an empty mesh + scalar bar — that's misleading visual noise."""
r = DiagramRenderer(offscreen_plotter)
zero_for_two_elems = DiagramData(
component=ForceComponent.T,
element_ids=np.array([10, 20], dtype=int),
values_i=np.zeros(2), values_j=np.zeros(2), abs_max=0.0,
)
r.render(project_3d, zero_for_two_elems, scale=1.0)
assert r._actor is None
def test_clear_removes_actor(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
data = extract_diagram_data(project_3d, static_results, ForceComponent.N)
r.render(project_3d, data, scale=0.01)
assert r._actor is not None
r.clear()
assert r._actor is None
def test_render_replaces_previous_overlay(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
data_n = extract_diagram_data(project_3d, static_results, ForceComponent.N)
r.render(project_3d, data_n, scale=0.01)
first_actor = r._actor
data_v = extract_diagram_data(project_3d, static_results, ForceComponent.V2)
r.render(project_3d, data_v, scale=0.05)
assert r._actor is not first_actor