otko/tests/unit/test_render_style.py
smillmorel 56f1f6c08b feat: route both canvas backends through RenderStyle
Colours that were hard-coded in the renderers now come from the shared
style: nodes, supports, nodal/element loads, section extrusions (+
opacity) and label font size. The PyVista frame LUT becomes a four-slot
palette [beam, truss, link, selected] with the cell scalar carrying the
family slot, so VTK matches the Plotly backend's per-family colouring
that landed earlier. Adds style helpers shared by both backends
(element_family_index, family_palette) and an immutable with_updates().

The style also gains the editable field table (STYLE_FIELDS) the Plot
Properties dialog is built from.
2026-09-16 19:31:30 -04:00

117 lines
4 KiB
Python

"""Unit tests for the shared render style and its editable subset."""
from __future__ import annotations
import dataclasses
import pytest
from otko.core import ElasticBeamColumn, Node, Project, TrussElement, ZeroLengthElement
from otko.views.canvas3d.style import (
EDITABLE_FIELDS,
SELECTED_STATE,
STYLE_FIELDS,
RenderStyle,
element_family_index,
family_palette,
)
def test_defaults_are_the_documented_palette() -> None:
style = RenderStyle()
assert style.node_color == "#d9d9d9"
assert style.selected_color == "#00ffff"
# opstool-derived element palette.
assert style.element_beam_color == "#0652ff"
assert style.element_truss_color == "#FF8C00"
assert style.element_link_color == "#39FF14"
assert len(style.response_scale_colors) == 10
assert style.response_scale_colors[0] == "#313695" # cold end
assert style.response_scale_colors[-1] == "#a50026" # warm end
def test_with_updates_is_immutable() -> None:
original = RenderStyle()
updated = original.with_updates(node_color="#123456", label_font_size=20)
assert updated is not original
assert updated.node_color == "#123456"
assert updated.label_font_size == 20
# The source instance is untouched (frozen dataclass contract).
assert original.node_color == "#d9d9d9"
assert original.label_font_size == 12
def test_editable_subset_matches_the_field_table() -> None:
style = RenderStyle()
assert RenderStyle.EDITABLE == EDITABLE_FIELDS
assert set(style.editable_values()) == set(EDITABLE_FIELDS)
# Every advertised field must exist on the dataclass with the right kind.
kinds = {name: kind for name, _label, kind in STYLE_FIELDS}
for name in EDITABLE_FIELDS:
assert hasattr(style, name), name
assert kinds[name] in {"color", "float", "int"}
def test_response_colorscale_is_an_evenly_spaced_mapping() -> None:
style = RenderStyle()
scale = style.response_colorscale()
assert len(scale) == len(style.response_scale_colors)
assert scale[0] == (0.0, style.response_scale_colors[0])
assert scale[-1] == (1.0, style.response_scale_colors[-1])
positions = [position for position, _color in scale]
assert positions == sorted(positions)
@pytest.mark.parametrize(
("element", "expected"),
[
(ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), 0),
(TrussElement(id=2, nodes=(1, 2), area=0.01, material_id=1), 1),
(ZeroLengthElement(id=3, nodes=(1, 1), material_ids=(1,), dofs=(1,)), 2),
],
)
def test_element_family_index(element: object, expected: int) -> None:
assert element_family_index(element) == expected
def test_family_palette_order_and_selected_slot() -> None:
style = RenderStyle(node_color="#010101")
palette = family_palette(style)
assert palette == [
style.element_beam_color,
style.element_truss_color,
style.element_link_color,
style.selected_color,
]
assert len(palette) - 1 == SELECTED_STATE
def test_editable_values_round_trip_through_replace() -> None:
style = RenderStyle()
values = style.editable_values()
assert RenderStyle(**values) == style
# And the persisted shape is JSON-friendly (str/int/float only).
import json
assert json.loads(json.dumps(values)) == values
def test_style_is_a_frozen_slots_dataclass() -> None:
assert dataclasses.is_dataclass(RenderStyle)
with pytest.raises(dataclasses.FrozenInstanceError):
RenderStyle().node_color = "#000000" # type: ignore[misc]
def test_project_builder_helper_is_unused_but_valid() -> None:
"""A style constructed from a project's elements classifies every family."""
project = Project(
ndm=2,
ndf=3,
nodes=[Node(id=1, coords=(0, 0, 0)), Node(id=2, coords=(1, 0, 0))],
elements=[
ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1),
TrussElement(id=2, nodes=(1, 2), area=0.01, material_id=1),
],
)
indices = [element_family_index(el) for el in project.elements]
assert indices == [0, 1]