feat(plotly): opstool-style contour, loads, supports, ghost and animation
Bring the Plotly canvas in line with opstool's visualisation recipes. Camera / interaction: - frame the camera in plotly's normalized scene units, using the model bounds (grid excluded); a data-unit eye rendered the model as a speck - use turntable dragmode so Z stays up and the horizon stays level - align mouse bindings with the PyVista/VTK backend and document them in Help → Mouse Controls Visualisation: - colour deformed / modal shapes by response with a shared colourbar - scale load arrows by |F|, tint per load pattern, hover the magnitude, and fix cones rendering as oversized fins (sizemode scaled, not absolute) - draw DOF-accurate support glyphs (ported from opstool; see NOTICE) - add an undeformed-reference overlay on both canvas backends - play mode shapes with plotly frame animation in-page Tests: builder unit tests plus GUI regression tests for gestures, the deformed push, the undeformed reference and animation payloads.
This commit is contained in:
parent
1b0620a392
commit
d3878f23b3
26 changed files with 1687 additions and 153 deletions
|
|
@ -55,6 +55,14 @@ def test_framing_push_is_not_marked_preserve_view(qtbot) -> None: # type: ignor
|
|||
assert "camera" in scene
|
||||
assert "range" in scene["xaxis"]
|
||||
assert scene["xaxis"]["autorange"] is False
|
||||
# plotly's camera lives in normalized scene units: the model is centred on
|
||||
# the origin and the eye is only a few units out (not a data-unit distance,
|
||||
# which parked the camera hundreds of units away and shrank the model).
|
||||
camera = scene["camera"]
|
||||
assert camera["center"] == {"x": 0.0, "y": 0.0, "z": 0.0}
|
||||
assert (
|
||||
camera["eye"]["x"] ** 2 + camera["eye"]["y"] ** 2 + camera["eye"]["z"] ** 2
|
||||
) ** 0.5 < 20.0
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
|
|
@ -121,3 +129,67 @@ def test_js_console_messages_reach_the_log(qtbot) -> None: # type: ignore[no-un
|
|||
("warning", "[web] plot.html:8 careful"),
|
||||
("info", "[web] plotly:9 hello"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def test_deformed_push_colours_by_scalar_and_ghosts_the_reference(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||
"""The canvas wiring drives the builder's contour + ghost overlays."""
|
||||
import numpy as np
|
||||
|
||||
from otko.services.deformation import DeformationSource
|
||||
from otko.views.canvas3d.model_renderer import RendererMode
|
||||
|
||||
canvas, calls = _canvas_with_captured_js(qtbot)
|
||||
project = load_project(EXAMPLES / "cantilever.osmodel")
|
||||
canvas.set_project(project)
|
||||
ids = [node.id for node in project.nodes]
|
||||
source = DeformationSource(
|
||||
displacements=np.zeros((len(ids), 3)) + np.linspace(0, 1, len(ids))[:, None],
|
||||
node_id_to_row={nid: i for i, nid in enumerate(ids)},
|
||||
)
|
||||
|
||||
canvas.set_mode(RendererMode.DEFORMED, source)
|
||||
canvas.set_show_undeformed(True)
|
||||
calls.clear()
|
||||
canvas.render()
|
||||
|
||||
payload, _preserve = _parse_call(calls[-1])
|
||||
names = {trace.get("name") for trace in payload["data"]}
|
||||
assert "undeformed-reference" in names
|
||||
nodes = next(t for t in payload["data"] if t.get("name") == "nodes")
|
||||
assert nodes["marker"]["showscale"] is True
|
||||
assert nodes["marker"]["colorbar"]["title"]["text"] == "|u|"
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def test_animation_push_carries_prebuilt_frames(qtbot) -> None: # type: ignore[no-untyped-def]
|
||||
"""``animate`` must hand plotly a frame list, not drive Python re-renders."""
|
||||
import numpy as np
|
||||
|
||||
from otko.services.deformation import DeformationSource
|
||||
from otko.views.canvas_plotly.trace_builder import SceneOptions
|
||||
|
||||
canvas, calls = _canvas_with_captured_js(qtbot)
|
||||
project = load_project(EXAMPLES / "cantilever.osmodel")
|
||||
canvas.set_project(project)
|
||||
canvas._scene = canvas._builder.build(project, SceneOptions())
|
||||
|
||||
ids = [node.id for node in project.nodes]
|
||||
options = [
|
||||
SceneOptions(
|
||||
deformation=DeformationSource(
|
||||
displacements=np.full((len(ids), 3), float(k)),
|
||||
node_id_to_row={nid: i for i, nid in enumerate(ids)},
|
||||
)
|
||||
)
|
||||
for k in (1.0, 2.0, 3.0)
|
||||
]
|
||||
canvas.animate(options, duration_ms=25, loops=4)
|
||||
|
||||
assert calls and calls[-1].startswith("window.otkoAnimate(")
|
||||
body = calls[-1][len("window.otkoAnimate(") :]
|
||||
literal, end = json.JSONDecoder().raw_decode(body)
|
||||
payload = json.loads(literal)
|
||||
assert len(payload["frames"]) == 3
|
||||
assert payload["frames"][0]["data"], "frames must carry trace data"
|
||||
assert body[end:].startswith(", 25, 4, true")
|
||||
|
|
|
|||
Loading…
Reference in a new issue