Frames the view deterministically: Scene now carries padded per-axis bounds
(opstool's pad_ratio 0.15, with unit slack on degenerate axes) plus
axis_overrides() pinning range + autorange=False, so the model is never
flush against the viewport edge. Adds entity hover templates (Node #n /
Element #n with an empty extra tag), and an optional axis outline (grid +
ticks) - off by default so the SAP2000-like clean viewport is unchanged;
the coloured X/Y/Z axis lines always stay as the orientation cue.
The outline is exposed as a bool in the Plot Properties table, so the
table-driven dialog now builds a checkbox for kind=bool.
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.
Colours that were hard-coded in the renderers now come from the shared
style: nodes, supports, nodal/element loads, section extrusions (+
opacity) and label font size. The PyVista frame LUT becomes a four-slot
palette [beam, truss, link, selected] with the cell scalar carrying the
family slot, so VTK matches the Plotly backend's per-family colouring
that landed earlier. Adds style helpers shared by both backends
(element_family_index, family_palette) and an immutable with_updates().
The style also gains the editable field table (STYLE_FIELDS) the Plot
Properties dialog is built from.
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).
MainWindow persists preferences (window layout, canvas backend) through
QSettings("OTKO", "OTKO"), so tests constructing it were reading the
developer's real settings: after switching to the Plotly backend the
PyVista-specific viewport-axis tests failed with "PlotlyCanvas has no
attribute renderer". A session fixture now redirects QSettings into a
temp dir and is skipped when Qt is not installed, keeping the headless
job Qt-free.
opstool v1.0.26 (GPL-3.0) is cloned read-only under
.slim/clonedeps/repos/ to inspect its PyVista/Plotly visualization
settings. GPLv3 section 13 permits combining it with this AGPL-3.0
project. The clone itself is git-ignored; the manifest and the AGENTS.md
pointer are committed.
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.
PlotlyCanvas hosts plotly.js in a QWebEngineView driven over
QWebChannel: figures update with Plotly.react (the camera survives
unless a view preset asks for it), and clicks round-trip as
node/element picks or grid-snap clicks. Assets are written to a temp
dir and loaded from file:// because the ~5 MB bundle is past
setHtml's data-URL limit. render() forwards QWidget's overload so
grab() and painting keep working.
Project → plotly.js figure dicts with no Qt/pyvista/plotly import, so
it is unit-tested in the headless job. Mirrors the PyVista renderer's
geometry (grid, nodes, frames, supports, loads, extrusions, local
axes, labels, deformation) while respecting plotly.js's medium:
pixel-sized markers, None-separated line segments, selection as a
second trace (per-segment line colours are impossible), cone traces
for arrows. Pickable traces carry meta.kind + customdata.
Supplies the offline plotly.js bundle for the new canvas backend. The
GUI CI job now installs the gui extra so the Qt/WebEngine tests run
instead of being import-skipped.
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).
RUF001-003 are ignored repo-wide: × / ≈ / ² are deliberate in the
engineering docs and formulas, which also makes the scattered inline
noqa suppressions redundant. Tests use engineering symbols (A, I, L, E),
`importorskip` before imports and capitalised test names; Qt requires
camelCase signal names and PascalCase event overrides; OpenSees material
fields (cR1, epsU) mirror the solver verbatim. N806 stays exempt only for
the numeric modules that use L / Iz / Iy as symbols.
Reconstructs the workflow stripped during the tree copy to match the
meta-gates: separate type job, headless command over
tests/unit+tests/services, and dedicated GUI / integration jobs.
Tool versions pinned to the pre-commit revisions (ruff 0.4.4,
mypy 1.10.0) so CI and local hooks agree.
The form still passed flat b/h/d/tw/tf/diameter fields, which
ElasticSection (extra='forbid') rejects, so reading any elastic
section crashed. Rebuild the display-only hint as a PipeShape /
AngleShape / RectShape member instead; an incomplete shape degrades
to None rather than raising.
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.
Single source of truth for the package + pinned solver versions in
src/otko/_const.py (hatch dynamic version). scipy/pandas were listed in
[gui] but never imported. mypy strict mode rescoped to
core/services/viewmodels; tomli added for tomllib-less Pythons.