Files
BondGraph/BEdit/AGENTS.md
2026-07-22 11:38:07 +02:00

330 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md
This file is the working guide for AI coding agents contributing to BEdit.
Its instructions apply to the entire repository.
## Project purpose
BEdit is a Python/PySide6 graphical editor for hierarchical block and bond-graph
models. A document contains reusable components, typed and oriented ports,
connections, nested graphs, and editable vector icons.
The application is evolving quickly. Prefer small, coherent changes that improve
the architecture without introducing compatibility layers unless compatibility is
explicitly requested.
## Architecture
The main boundary is strict:
```text
src/bedit/
├── __main__.py
├── core/ # Pure Python; must not import PySide6
│ ├── model.py # Document, component, graph, port, icon data
│ ├── port_types.py # Port type definitions and compatibility
│ ├── serializer.py # JSON persistence
│ ├── libraries.py # Library file discovery and parsing
│ └── simulation/ # Qt-free composition and OpenModelica interface code
└── gui/ # All Qt-dependent code
├── app.py # QApplication startup and palette
├── main_window.py # Top-level UI orchestration
├── preferences.py # Explicit disk-backed QSettings factory
├── controllers/ # Document controller and undo commands
├── dialogs/ # Dialog behavior
├── graphics/ # Workspace, icon editor, vector rendering
├── models/ # Qt tree models and repository adapters
└── generated/ # Generated UI/resource Python; never hand-edit
```
Dependency direction:
- `bedit.core` may depend only on the Python standard library.
- `bedit.gui` may depend on `bedit.core` and PySide6.
- `bedit.core` must never import `bedit.gui` or PySide6.
- Data parsing and validation belong in `core`.
- Signals, widgets, painting, Qt models, undo integration, and settings UI belong
in `gui`.
Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
`gui` subpackage rather than the package root.
## Important behavior and design decisions
- Components may contain nested graph or text implementations.
- Ports retain stable IDs. Display names, types, orientation, positions, and icon
anchors may change without changing IDs.
- Parameters belong to `Component` rather than a particular implementation kind,
so graph and text components share stable-ID name/type/value records.
- Port orientation is presented as one unified list in the UI, while the model
indexes inputs and outputs separately for connection semantics.
- Port types are registered in `core/port_types.py`. Only compatible types may be
connected. `signal` is currently the only type.
- Port connector type and signal value type are separate. Signal ports and
parameters carry editable value type, quantity, unit, row/column dimensions,
and description metadata. Editable quantity/unit suggestions live in
`core/physical_types.py`; unlisted values remain valid.
- Port removal or reorientation must be rejected when it would invalidate an
existing connection.
- Connections reference port IDs, never port names.
- Connection junctions are explicit typed graph objects. Splitting a connection
creates one incoming and one outgoing segment; the junction can source further
branches without overlapping full connection paths.
- Graph interaction has separate Pointer and Connect modes. Port hints are only
visible in Connect mode. Connecting two blocks opens the compatible port-pair
chooser; explicit port clicks determine its default selection. Connections
use one freely angled polyline format with absolute `properties.waypoints`.
Semantic ports choose compatibility, while each rendered endpoint is the
intersection of the owning hitbox and its center-to-adjacent-route-point ray.
- Connection appearance is configured per port type in
`gui/graphics/connection_styles.py`, including color, width, pen style, and
source/target arrowheads. Do not scatter those constants through painters.
- Graph annotations are `box`, `line`, or `text` objects in `Graph.annotations`.
They use integer layers below or above graph layer 0. Annotation lines reuse
connection polyline and absolute `properties.waypoints` semantics. Graph
annotations and icon elements share `ShapeOptionsDialog` and `shape_pen` so
their style fields and rendering must remain aligned.
- Icon editing uses a fixed 128×128 coordinate space.
- The visible/selectable component hitbox is calculated from vector elements,
not from the complete 128×128 icon canvas.
- New component icons default to a centered 64×64 shape.
- Vector elements include rectangles, circles, ellipses, lines, triangles, and
text. Shape styling and geometry are stored in document JSON.
- Icon port anchors are stored in `Port.properties["iconPosition"]`.
- Workspace grid size, workspace snapping size, and icon grid size are separate
persisted settings.
- Settings use `gui.preferences.application_settings()` and are stored under the
explicit `BEdit/BEdit` identity. Do not create anonymous `QSettings()` objects.
- Avoid the QSettings group name `general`; Qt treats `General` specially in INI
files. Autosave keys live under `autosave/`.
- User-visible document edits should participate in undo/redo.
- Component clipboard data is shared by the graph, document tree, and library
tree. Pasting into the document node creates roots; pasting into a graph node
creates children at an origin-normalized position. Always clone pasted trees
with fresh IDs, preserve connections between jointly copied graph blocks, and
assign unique sibling names.
- The text-definition editor uses OpenModelica highlighting and completion from
`src/bedit/data/syntax/openmodelica.json`. Keep keywords, types, built-ins, and
named BEdit `$name$` macro completions editable there; arbitrary `$name$`
expressions are highlighted as BEvalues. Highlight colors and bold/italic
styles are persisted under `syntax/<category>/` in application settings.
- Text component sources may contain private Modelica declarations in
`source.declarations`. The text-definition editor exposes declarations above
`source.initialEquations` and `source.equations`, with the same highlighting
and completion in all three fields. The composer emits each in its matching
Modelica section.
- Application-wide messages use `core.application_log.get_logger()`. The main
window installs the Qt log-panel handler; core code must only use standard
Python logging and must not import the GUI handler.
- File → Reload Simulation Code (`Ctrl+F5`) reloads modules under
`bedit.core.simulation`, replaces the shared application/controller service,
and preserves the previous instance attributes where possible.
- Modelica composition lives in `core/simulation/composer.py`; the simulation
service only owns application state and delegates composition. Ports with
`multipleConnections` are emitted as Modelica arrays. Their size is inferred
per component instance from graph connections and exposed while compiling as
`$portname_N$`; array connection endpoints receive stable one-based indices in
graph connection order.
- Simulation → Export Model composes through `Simulation.compose_source()` without
building the model, then writes the generated source as a `.mo` file.
- OpenModelica integration belongs in `core/simulation/openmodelica.py`. Its
persistent worker and OMC session start lazily on the first queued request.
Never perform OMPython work directly on the Qt GUI thread. Result and error
callbacks run on background threads and must use a Qt signal before touching UI.
One lazy temporary working directory is shared by all requests in the session.
Explicit application shutdown closes OMC and removes that directory plus the
current session's OMPython log and port files; `__del__` is only a fallback.
- Simulation runs start an ephemeral localhost TCP listener before launching the
generated model through OMC's `system()` function. OpenModelica's newline-delimited
`xmltcp` status and message records are parsed in the core and forwarded through
callbacks; the simulation service retains the latest progress for polling.
- The application owns one reusable `SimulationWindow`. Starting a run clears its
progress, log, and future result views. Extend graph presentation through its
Designer-owned `resultsLayout` and the
`clear_result_views()`/`load_result_views()` hooks.
- Simulation-window geometry, dock/toolbar state, and central-results visibility
persist under `simulationWindow/` through `application_settings()`.
- Standalone simulation results are modeled in `core/simulation/results.py`.
Its versioned schema retains model status, messages, metadata, and plottable
traces so the simulation window can open results without an active document.
Human-readable `.json` uses JSON, while the default `.ber` format uses the same
compressed MessagePack approach as `.beb` documents.
- After a successful OpenModelica run, `<model>_res.csv` is parsed on the worker
before temporary-directory cleanup. `SimulationResults.data` stores every CSV
column as a numeric array, including `time`, for later plotting and persistence.
- The simulation window's dockable Signals tree derives hierarchy from dot-separated
result-column names and bracketed array indices (`a[1]` becomes `a → 1`). Leaf
items retain the exact full column name in `UserRole`; plotting code should
consume `SimulationWindow.selected_signal_names()`.
- Simulation graph tabs persist as `SimulationResults.graphs`; every graph has a
stable ID, editable title, and its own `traces` list. Runtime graph widgets belong
in `GraphWorkspacePage.plot_layout`, not in the serialized core model. The
Signals tree checkboxes edit the active graph's traces, and each page embeds a
Matplotlib QtAgg canvas. Each graph persists its own `x_axis` signal (default
`time`), selectable from the Signals tree context menu.
- Embedded Matplotlib canvases provide cursor-centered wheel zoom and direct
left-button drag panning without activating navigation-toolbar modes. Their
custom navigation toolbar restores an explicit data-derived home view.
- Rerunning the same composed model retains graph tabs, ordering, titles, X axes,
and surviving trace settings while replacing numeric data. Missing signals are
pruned from traces/X-axis selection and newly returned columns appear in the tree.
- The optional OpenModelica executable is persisted as
`simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must
not read `QSettings`. An empty path uses OMPython/PATH discovery, while an
explicit `.../bin/omc` path is converted to the OpenModelica home directory.
- Builds enable OpenModelica's `--unitChecking`. Completed simulation results
read per-column units from OMC's generated `<model>_init.xml`; BEdit must not
infer derivative units itself. This metadata is serialized with `.ber`/JSON
results and displayed by the simulation signal tree and plots.
- Result loading restores variables marked `alias` or `negatedAlias` in OMC's
initialization XML. Values come from OMC's result representative or a fixed
parameter/constant start value; BEdit does not infer aliases from graph edges.
- Compile requests call OMC `checkModel` before `buildModel` and log OMC's check
summary verbatim through the application-wide logger.
## Qt Designer and generated files
Editable Designer sources are in `ui/`. Resources are defined in
`resources/resources.qrc`.
Never hand-edit files in `src/bedit/gui/generated/`. Modify the corresponding
`.ui` or `.qrc` source and regenerate instead.
Use the configured VS Code task **Qt: Build Designer Files**, or run the relevant
commands directly:
```bash
pyside6-rcc resources/resources.qrc \
-o src/bedit/gui/generated/resources_rc.py
pyside6-uic --from-imports ui/main_window.ui \
-o src/bedit/gui/generated/ui_main_window.py
pyside6-uic --from-imports ui/settings_dialog.ui \
-o src/bedit/gui/generated/ui_settings_dialog.py
pyside6-uic --from-imports ui/component_options_dialog.ui \
-o src/bedit/gui/generated/ui_component_options_dialog.py
pyside6-uic --from-imports ui/port_options_dialog.ui \
-o src/bedit/gui/generated/ui_port_options_dialog.py
pyside6-uic --from-imports ui/shape_options_dialog.ui \
-o src/bedit/gui/generated/ui_shape_options_dialog.py
pyside6-uic --from-imports ui/icon_editor_dialog.ui \
-o src/bedit/gui/generated/ui_icon_editor_dialog.py
pyside6-uic --from-imports ui/text_definition_editor.ui \
-o src/bedit/gui/generated/ui_text_definition_editor.py
pyside6-uic --from-imports ui/simulation_settings_dialog.ui \
-o src/bedit/gui/generated/ui_simulation_settings_dialog.py
pyside6-uic --from-imports ui/simulation_window.ui \
-o src/bedit/gui/generated/ui_simulation_window.py
pyside6-uic --from-imports ui/graph_parameters_dialog.ui \
-o src/bedit/gui/generated/ui_graph_parameters_dialog.py
pyside6-uic --from-imports ui/parameter_options_dialog.ui \
-o src/bedit/gui/generated/ui_parameter_options_dialog.py
```
When adding a promoted/custom widget in Designer, its header must use the real
Python module path, for example `bedit.gui.graphics.workspace`.
Substantial windows and dialogs must have a Designer `.ui` source. Python classes
bind behavior and data but must not reconstruct or replace those layouts at
runtime. A tiny generic prompt with one field and OK/Cancel may remain code-only.
## Editing conventions
- Preserve stable document IDs and existing connections.
- Validate candidate document changes before pushing an undo command.
- Keep model serialization symmetrical: additions to `to_dict()` require matching
handling in `from_dict()` and cloning where relevant.
- Use descriptive domain names; avoid generic `utils.py` modules.
- Prefer focused classes and helpers over growing `main_window.py` further.
- Shared vector calculations that do not require Qt belong in `core`; Qt painter
code belongs in `gui/graphics`.
- Do not silently swallow malformed document data. Raise a useful `ValueError` in
`core`, then present it through the GUI layer.
- Preserve unrelated user changes. The worktree may already be dirty.
- Do not delete or overwrite library/document JSON unless the requested workflow
explicitly calls for it.
## Document and library files
- Documents use the `bedit-document` JSON format.
- Documents can be stored as human-readable `.bedit.json`/`.json` through
`JsonDocumentSerializer`, or as compressed MessagePack `.beb` through
`BebDocumentSerializer`. UI document I/O dispatches via `DocumentSerializer`.
- `test.bedit.json` is a useful manually created example during development.
- Library documents use the same recursive document model.
- Library parsing belongs in `core/libraries.py`; Qt change notifications belong
in `gui/models/library_repository.py`.
- Package library data, if present, belongs under `src/bedit/data/libraries/`.
## Validation
There is not yet a complete automated test suite. For every change, run at least:
```bash
python3 -m compileall -q src
git diff --check
```
Run imports with the source tree explicitly available:
```bash
PYTHONPATH=src python3 -c "import bedit.core; import bedit.gui.app"
```
Verify the backend remains Qt-free after core changes:
```bash
PYTHONPATH=src python3 - <<'PY'
import sys
import bedit.core
assert not any(name.startswith("PySide6") for name in sys.modules)
PY
```
For model changes, add a focused in-memory round-trip check using
`GraphDocument.to_dict()` and `GraphDocument.from_dict()`. For controller changes,
exercise undo and redo when applicable.
GUI smoke tests may fail in headless environments because the system Qt platform
theme tries to access a display even with an offscreen platform. Do not claim an
interactive GUI test passed unless a display-capable environment was actually
used. Compilation, imports, and pure model/controller checks remain useful.
If Ruff is installed, also run:
```bash
ruff check src
```
## Running the application
Install the package in editable mode or run it from the source tree:
```bash
PYTHONPATH=src python3 -m bedit
```
The installed GUI entry point is `bedit.gui.app:main`.
## Completion checklist
Before handing off a change:
1. Confirm the `core`/`gui` dependency boundary is intact.
2. Confirm generated files were not hand-edited.
3. Check serialization and cloning for model changes.
4. Check undo/redo for document mutations.
5. Run compilation and `git diff --check`.
6. Report any GUI behavior that could not be tested interactively.
7. Update README or this file if the architecture or workflow changed.