2026-09-16 19:31:30 -04:00
|
|
|
"""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
|
2026-09-16 20:05:26 -04:00
|
|
|
assert kinds[name] in {"color", "float", "int", "bool"}
|
2026-09-16 19:31:30 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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]
|