Metadata-Version: 2.4
Name: gridquill
Version: 0.1.1
Summary: Reactive grid-based diagrams with nested canvases and editable Excalidraw output
License-Expression: MIT
Keywords: diagrams,excalidraw,reactive,grid,visualization
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Graphics
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: setuptools>=77.0.3; extra == "dev"
Requires-Dist: twine>=6; extra == "dev"
Dynamic: license-file

# Gridquill

Build editable diagrams in Python using reactive coordinates, nested canvases,
elements, signals, captions and connectors. Export to `.excalidraw` for Excalidraw
or `.excalidraw.md` for Obsidian. Python 3.10+; no runtime dependencies.

## Install from this checkout

```console
python -m pip install -e .
```

The package is prepared for PyPI but has not been published.

## Draw a diagram

```python
import gridquill as gq

gq.set_color_scheme(
    element_color=0x1971C2, element_fill=0xA5D8FF,
    signal_color=0xF08C00, signal_fill=0xFFEC99,
)

with gq.canvas("Example") as diagram:
    with gq.canvas("Acquisition", column=2, row=1,
                   background=0xEDF2FF, margin_left=20, margin_right=20,
                   margin_top=20, margin_bottom=20):
        adc = gq.element("ADC", column=0, top=0, length=4)
        fpga = gq.element("FPGA", column=4, top=0, length=4)
        samples = gq.signal("Samples", row=adc.port(2), components=[adc])
        samples.components.append(fpga)
        gq.connector(fpga, samples, inout="in")
        gq.caption("Acquisition", row=-1, column=0)

diagram.save("example.excalidraw.md")
```

## Coordinates and ownership

Literal coordinates are local to their canvas. On save, coordinate references
resolve to positions in the drawing, including all parent offsets. A captured
reference such as `fpga.column` follows later moves and assignments. Sizes and
lengths are distances; coordinate formulas return drawing coordinates.

```python
fpga.column = lambda: adc.column + 5
diagram.resolve()  # resolve without writing a file
```

Geometry fields and span entries accept numbers, reactive arithmetic or
zero-argument functions. Each save evaluates them in passes. Pending dependencies
read as NaN; stalled resolution raises `ReactiveResolutionError` naming the
unresolved fields. Functions must be pure. `float(value)` reads a snapshot of the
last resolved value. Failed resolution leaves an existing output file untouched.

Creating a canvas opens it. Subsequent objects attach automatically; `pop()`
returns to the previous canvas. `with canvas(...)` restores the previous scope
on exit. Use `canvas(activate=False)` for explicit ownership and `append()`.

## Objects

- `element(label, column=..., top=..., bottom=... / length=...)`: vertical box.
  Alternatively use `span=[top, bottom]`. `element.port(index)` returns a reactive
  row, with 0.5 grid-unit spacing; negative indices count from the bottom.
- `signal(label, row=..., left=..., right=... / length=...)`: horizontal box.
  Alternatively use `span=[left, right]` or a mutable `components` list whose
  outermost columns are connected. Endpoint formulas are prepared at save time.
- `caption(label, row=..., column=..., fill=..., length=...)`: automatically sized
  label box; omit `length` to fit the text on each save.
- `connector(element_or_column, signal_or_row, position="auto", inout="out")`:
  chevron marker. With a signal, the default chooses `left` or `right` when its
  span lies entirely on that side of the element's column, including a signal
  ending at the element. The side and arrow direction follow layout changes on
  every save. A signal crossing both sides, or a numeric row, defaults to `left`.
  Set `position="left"` or `"right"` to override. Flow is `in`, `out` or `inout`.
- `chevron(...)`: individual direction stencil.
- `canvas(...)`: group with an optional background, border style and per-side
  margins in pixels. Bounds follow its members automatically.

Default grid size is 80 pixels. `set_color_scheme()` changes global defaults;
explicit object colors take precedence. Assign `None` to inherit again.
`get_color_scheme()` returns a copy; `reset_color_scheme()` restores defaults.

`draw_diagram()` provides the lower-level renderer. To normalize existing
drawings, run `gridquill-normalize drawing.excalidraw.md` (edits the input file).

## Drawing order

Set `z_index` to control overlaps. Higher values appear in front, and each box
and its label stay together:

```python
with gq.canvas() as diagram:
    block = gq.element("FPGA", column=2, span=[0, 4])
    bus = gq.signal("Bus", row=2, span=[0, 4], z_index=1)

bus.z_index = 2  # editable after creation; applied on the next save
diagram.save("layers.excalidraw")
```

Signals, elements, chevrons, captions, connectors and canvases accept `z_index`
(default `0`). Negative and fractional values are allowed, as are reactive
formulas. A canvas adds its `z_index` to its descendants' values, so a connector
or a nested canvas can shift all its members together. Layers compare across
canvas boundaries. Canvas backgrounds remain behind all drawing objects.

Equal values preserve the existing order: signals (including captions), then
elements, then chevrons, in traversal order within each kind. Existing diagrams
therefore keep their drawing order. For `draw_diagram()`, set `z_index` in each
horizontal or vertical item dictionary; connection arrows remain behind boxes.

## Examples and development

```console
python -m pip install -e ".[dev]"
python -m unittest discover -s tests
python examples/canvas_scope_example.py
python examples/recreate_udt_overview.py --output overview.excalidraw.md
python -m build
python -m twine check dist/*
```

Source lives in `src/gridquill`; tests and runnable examples live in `tests` and
`examples`. The firmware-specific CSV layout exporter remains in the firmware
repository because it depends on the separate `fmake` toolchain.

## Publishing

Add author details and actual project URLs to `pyproject.toml`.
Set `__version__` in `src/gridquill/__init__.py` for a release.
Build and validate the distributions using the commands above. When ready,
upload explicitly with `python -m twine upload dist/*` using your PyPI account.
No publishing credentials or upload automation are included.

## License

MIT. See [LICENSE](LICENSE).
