282 lines
13 KiB
Markdown
282 lines
13 KiB
Markdown
# 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 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.
|
||
- 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.
|
||
- 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.
|
||
- 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.
|
||
- Standalone simulation-result JSON is 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.
|
||
- 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.
|
||
|
||
## 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.
|