otko/AGENTS.md
smillmorel 8f7bc3cf4f fix: keep the Plotly camera and axis ranges through data updates
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.
2026-09-16 20:05:35 -04:00

6.5 KiB
Raw Blame History

AGENTS.md — otko

Pre-alpha SAP2000-style desktop GUI for OpenSeesPy. Python 3.10+; Windows requires 3.12+ (openseespywin==3.8.0.0 has no 3.11 wheel). Solver pinned: openseespy==3.8.0.0.

Install

pip install -e ".[gui,dev]"  # desktop: Qt + PyVista + plotly.js backend + dev tools
pip install -e .              # headless: core + services only, no Qt (scripts, notebooks, web backends)
python -m otko                # launch GUI (src/otko/app.py:run)

scipy/pandas in [gui] extras are phantom deps (not imported as of 2026-06) — do not add imports expecting them.

Architecture (enforced in review — see PR template)

Strict one-way MVVM + services: views → viewmodels → services → core.

  • core/ (entities: project.py, geometry/, materials/, sections/, loads/, analysis/, catalog/): stdlib + numpy + pydantic only. No Qt, no openseespy. Period.
  • services/ (opensees_runner.py, persistence.py, results.py, ...): may use core + h5py + openseespy. No Qt.
  • views/: PySide6/pyvistaqt only. No direct import openseespy — go through a service.
  • views/canvas3d/ (PyVista/VTK, default) and views/canvas_plotly/ (plotly.js in a QWebEngineView) are two backends for the same central 3D view. Both satisfy the CanvasBackend protocol in views/canvas_base.py, share one SelectionState owned by MainWindow, and are swapped live via Options → Canvas Backend (persisted in QSettings under canvas/backend). MainWindow._activate_canvas keeps both widgets in a QStackedWidget — never destroy a canvas mid-session (VTK leaves dangling make-current callbacks). Backend-specific gaps are declared by CanvasCapabilities (e.g. Plotly has no force-diagram overlay or video export yet); gate UI on canvas.capabilities, never on the backend name. canvas_plotly/trace_builder.py is pure (no Qt, no pyvista) and unit-tested headless.
  • views/canvas3d/style.py (RenderStyle) is the single source of truth for colours/sizes on both backends; renderers must read it rather than hard-coding a colour. The STYLE_FIELDS subset is user-editable via Options → Plot Properties… (views/dialogs/plot_properties.py), previews live through MainWindow.set_plot_style(..., persist=False), and persists as JSON in QSettings under plot/props.
  • viewmodels/ bridges core↔Qt (signals, QUndoStack); commands/ holds QUndoCommand subclasses.
  • Rules: public functions need type hints + docstring; new domain entities go through Pydantic validation; ops >50 ms run off the GUI thread (AnalysisWorker in QThread, cancel via isInterruptionRequested(), results cross threads as lightweight ResultsHandle to HDF5).

Runner emits OpenSeesPy commands in fixed order (docs/architecture.md): wipe → model → node → fix → material → section → geomTransf → element → timeSeries → pattern/load → recorder → system/numberer/... → analyze. Never reorder.

Verify (in this order)

ruff check src tests
ruff format src tests          # line-length 100, E501 ignored
mypy src/otko/core src/otko/services
pytest -m "not slow"           # CI gate: lint → this, on 3.10/3.11/3.12 × ubuntu/windows/macos

Focused runs: pytest tests/unit (pure logic, ms), pytest tests/gui -k <name> (pytest-qt, needs display), pytest tests/integration -k <name> (real OpenSeesPy runs). Single test: pytest tests/unit/test_project.py::test_name -q. Markers: gui, slow. Linux GUI tests need xvfb-run -a pytest ... plus system Qt libs (see ci.yml apt list).

Notes: tests/conftest.py auto-ops.wipe()s the OpenSees domain between tests (lazy import so core tests stay Qt/OpenSees-free). Coverage omits views/. Commit style: Conventional Commits (feat:, fix:, ...). pre-commit install runs ruff + mypy (mypy hook scoped to core|services|viewmodels).

Git & Forgejo

Remote is a self-hosted Forgejo, SSH-only:

git remote add origin ssh://git@smill-home.ddns.net/smill/otko.git
  • Web UI / HTTPS clone: https://smill-home.ddns.net/forgejo/smill/otko.git (note the /forgejo subpath — SSH URLs don't have it).
  • Forgejo SSH runs through the system sshd on port 22 (SSH_PORT = 22 in the server's app.ini). If the site ever advertises a :2222 SSH URL again, that config regressed — push/pull with port 22 anyway and flag it.
  • Auth is by SSH key (Forgejo → Settings → SSH Keys). Test: ssh -T git@smill-home.ddns.net should answer Hi there, <user>! ... Forgejo does not provide shell access.
  • Default branch main. Work on short-lived branches (fix/<topic>, docs/<topic>), open a PR against main. Never commit directly to main from an agent session.
  • Repo root is otko/ itself. (On the dev laptop there is also an empty, commit-less parent repo at Sync/coding/ — ignore it; all git work happens inside otko/.)

Examples & persistence

  • examples/*.py are source of truth; examples/*.osmodel are generated artifacts (checked in). Never hand-edit .osmodel — change the script and regen: python examples/cantilever.py (each script saves, reloads, asserts clean round-trip).
  • Projects persist as single Pydantic-validated JSON .osmodel (diffable); analysis output goes to *.osresults.h5 (HDF5, one group per case).
  • Quick smoke: open examples/cantilever.osmodel → run Tip-Load → M3 peaks 50 kN·m at fixed end.

Cloned Dependency Source

Read-only dependency source repositories are available under .slim/clonedeps/repos/ for inspection. Do not edit these clones. The structured manifest is .slim/clonedeps.json.

  • .slim/clonedeps/repos/yexiang92__opstool/ — yexiang92/opstool at v1.0.26; the OpenSeesPy pre/post-processor whose PyVista and Plotly visualization settings (opstool/vis/{pyvista,plotly}/plot_utils.py, plot_resp_base.py, vis_model.py) are the reference for otko's canvas look-and-feel. GPL-3.0, and GPLv3 §13 explicitly permits combining it with otko's AGPL-3.0. Any code actually ported from it must keep the opstool copyright notice and record that it was modified (GPLv3 §5a/b) — add that entry to NOTICE when the port lands.

<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>