fix: stop the Plotly canvas wedging on rapid or overlapping updates

Three defects in the view-preservation path could leave the plot frozen
(no updates, no orbit, stale colours):

1. The merge injected raw _fullLayout objects - including undefined when a
   push landed before the previous react resolved - and plotly validates
   layouts, so one bad value made every later react fail permanently.
   currentView() now deep-copies and validates eye/center/up and each
   range, and mergeView() is wrapped so it can never block an update.
2. Updates were not serialized: overlapping Plotly.react calls on one graph
   div left it unresponsive. otkoUpdate now queues and coalesces (one react
   at a time, latest payload wins), and logs instead of failing silently.
3. A style change re-framed the camera, so re-colouring yanked a view the
   user had orbited. Colour/opacity ride the traces and background/outline
   ride the layout, so set_style is now a non-framing push.

Verified in the browser: a user orbit survives colour changes, 30 rapid
preview updates land on the final value and stay responsive, and deleting
the live camera no longer wedges the plot.
This commit is contained in:
smillmorel 2026-09-16 20:14:05 -04:00
commit 0611153555
3 changed files with 96 additions and 32 deletions

View file

@ -85,7 +85,8 @@ def test_view_presets_and_reset_re_frame(qtbot) -> None: # type: ignore[no-unty
@pytest.mark.gui
def test_style_change_re_frames_for_layout_only_attrs(qtbot) -> None: # type: ignore[no-untyped-def]
def test_style_change_keeps_the_view_but_updates_the_layout(qtbot) -> None: # type: ignore[no-untyped-def]
"""Re-colouring must not snap the camera back to the preset."""
from otko.views.canvas3d.style import RenderStyle
canvas, calls = _canvas_with_captured_js(qtbot)
@ -94,4 +95,7 @@ def test_style_change_re_frames_for_layout_only_attrs(qtbot) -> None: # type: i
canvas.set_style(RenderStyle(show_axis_outline=True))
assert calls[-1].endswith(", false)"), "background/outline live in the layout"
assert calls, "a style change must push"
payload, preserve = _parse_call(calls[-1])
assert preserve is True, "colour changes must not re-frame the view"
assert payload["layout"]["scene"]["xaxis"]["showgrid"] is True # layout applied