feat: opstool-derived element palette and single-trace frame colours
Ports opstool's per-family element colours and its diverging response scale; RenderStyle.response_scale_colors now also drives the PyVista force-diagram colouring instead of a hard-coded "coolwarm". The frame renderer drops the two-trace normal/selected workaround: my earlier assumption that plotly cannot colour segments individually was wrong. Scatter3d.line.color accepts an array mapped through a colorscale, so one trace now carries per-element colours (family + selection) and is ready to be coloured by response value later. Attribution recorded in NOTICE per GPLv3 section 5(a)/(b).
This commit is contained in:
parent
40a673c562
commit
17d2ed21d3
5 changed files with 164 additions and 45 deletions
|
|
@ -9,6 +9,7 @@ import numpy as np
|
|||
import pytest
|
||||
|
||||
from otko.services import load_project
|
||||
from otko.views.canvas3d.style import RenderStyle
|
||||
from otko.views.canvas_plotly.trace_builder import (
|
||||
PlotlyTraceBuilder,
|
||||
SceneOptions,
|
||||
|
|
@ -49,16 +50,47 @@ def test_nodes_carry_ids_as_customdata() -> None:
|
|||
assert len(nodes["marker"]["color"]) == len(project.nodes)
|
||||
|
||||
|
||||
def test_selected_element_gets_its_own_trace() -> None:
|
||||
def _color_for_index(trace: dict, index: float) -> str:
|
||||
"""Map a ``line.color`` scalar index to its colour via the colorscale."""
|
||||
scale = sorted(trace["line"]["colorscale"], key=lambda item: item[0])
|
||||
return scale[int(index)][1]
|
||||
|
||||
|
||||
def test_frames_are_one_trace_with_per_element_colours() -> None:
|
||||
"""A single line trace carries the palette through its ``line.color`` array."""
|
||||
project = _load("cantilever")
|
||||
scene = PlotlyTraceBuilder().build(project, SceneOptions(selection_elements=frozenset({2})))
|
||||
normal = _traces(scene, "elements")
|
||||
selected = _traces(scene, "elements-selected")
|
||||
assert len(normal) == 1 and len(selected) == 1
|
||||
# Plotly cannot colour one segment of a single line trace, so the
|
||||
# selected element moves to the highlight trace.
|
||||
assert 2 in selected[0]["customdata"]
|
||||
assert 2 not in normal[0]["customdata"]
|
||||
style = RenderStyle()
|
||||
scene = PlotlyTraceBuilder(style).build(
|
||||
project, SceneOptions(selection_elements=frozenset({2}))
|
||||
)
|
||||
frames = _traces(scene, "elements")
|
||||
assert len(frames) == 1, "selection must not split the frame trace"
|
||||
line = frames[0]["line"]
|
||||
assert isinstance(line["color"], list)
|
||||
assert len(line["color"]) == len(frames[0]["x"])
|
||||
assert line["colorscale"]
|
||||
|
||||
colors = line["color"]
|
||||
customdata = frames[0]["customdata"]
|
||||
|
||||
def index_of(element_id: int) -> set[float]:
|
||||
return {colors[k] for k, value in enumerate(customdata) if value == element_id}
|
||||
|
||||
selected = index_of(2)
|
||||
unselected = index_of(1)
|
||||
assert len(selected) == 1 and len(unselected) == 1
|
||||
assert selected != unselected
|
||||
assert _color_for_index(frames[0], next(iter(selected))) == style.selected_color
|
||||
assert _color_for_index(frames[0], next(iter(unselected))) == style.element_beam_color
|
||||
|
||||
|
||||
def test_element_palette_distinguishes_beam_from_truss() -> None:
|
||||
style = RenderStyle()
|
||||
builder = PlotlyTraceBuilder(style)
|
||||
beam = _traces(builder.build(_load("cantilever"), SceneOptions()), "elements")[0]
|
||||
truss = _traces(builder.build(_load("basic_truss"), SceneOptions()), "elements")[0]
|
||||
assert _color_for_index(beam, beam["line"]["color"][0]) == style.element_beam_color
|
||||
assert _color_for_index(truss, truss["line"]["color"][0]) == style.element_truss_color
|
||||
|
||||
|
||||
def test_selected_node_is_highlighted_by_colour() -> None:
|
||||
|
|
|
|||
Loading…
Reference in a new issue