plotly_hover/plotly_unhover fire continuously while the mouse moves, and
the unhover handler restyled the snap marker unconditionally - even with
snapping off and nothing visible. That meant a full Plotly.restyle plus
redraw per mouse event, which re-fired hover until the page died with
'RangeError: Maximum call stack size exceeded' and the canvas froze
(reported live after opening a model).
The marker is now updated only when its visible state actually changes: no
marker when snapping is off, and a restyle only when the snapped point
differs from the one already shown. Deliberately synchronous - timers are
throttled to about a second by WebEngine when the page is not compositing,
which stalled the preview. Measured: 200 mouse-move events went from 200
restyles to 0; 200 hovers on one grid dot cost a single restyle.
A plotly failure inside the WebEngine page was invisible from Python - the
react promise just rejected and the canvas looked frozen with no evidence.
PlotlyCanvas now installs a QWebEnginePage that forwards
javaScriptConsoleMessage into the Console dock with the right severity
(error/warning/info), so the next silent failure is diagnosable.
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.
Plotly.react resets any scene attribute the incoming layout omits, so every
selection change snapped the camera back to the default and re-fit the
ranges - the pinned framing was meaningless. Data-only pushes now pass
preserveView and html.py merges the live camera and axis ranges into the
incoming layout; only an explicit re-frame sends computed framing.
Also fixes the ordering bug where the framing flag was consumed before the
not-ready early return, which dropped the very first framing on the floor
(show_project runs before loadFinished).
Verified in the browser: user orbit + zoom survive selection and working
plane updates, while reset_camera re-frames. Regression-tested headlessly
by asserting the emitted preserveView flag.
Adds a table-driven dialog over RenderStyle.STYLE_FIELDS (colour swatches,
an opacity spin and a label font size), reachable from Options. Edits
preview immediately on every canvas, Cancel restores the style the dialog
opened with, and only OK persists — as JSON under QSettings plot/props,
reloaded on the next launch. Both canvases gain set_style(); the dialog
deliberately exposes plot_style() rather than style() so QWidget.style()
keeps its Qt meaning.
Both backends share one SelectionState owned by MainWindow and live
side by side in a QStackedWidget — switching is setCurrentWidget, so
no widget is destroyed mid-session (tearing a VTK window down leaves
dangling make-current callbacks). The choice persists in QSettings.
CanvasCapabilities declares per-backend gaps (force diagrams and
video export stay PyVista-only, both are documented and greyed out)
and the UI gates on capabilities rather than the backend name. The
architecture gate now allows the canvas_plotly package.
Auto-fixes (UP037, UP038, F401, I001, SIM105, RUF100) plus hand-fixes:
drop the now-unused `Union` imports, annotate mutable class attributes
with ClassVar, raise ValidationError instead of blind Exception in the
zero-length test, delete two dead locals, and bind the per-iteration
grid-builder closures to their loop variables (B023).
Catch-all for the intermixed residue of the unpushed otko-development
work ported into this tree: combinations/console-dock/quick-guide wiring
across commands, core, services, views and tests; repo-wide ruff-format
normalization; README/CONTRIBUTING updates; and the toolbar default
(both toolbars now open in the top area, quick guide text updated).
Splitting this further would require hunk-level surgery with low
confidence; the preceding commits in this branch isolate the
self-contained features.