# SBMLLayout

A clean Python API for attaching, editing, and rendering the
[SBML Layout extension](https://sbml.org/documents/specifications/level-3/version-1/layout/).

## Installation

```bash
pip install SBMLLayout
```

## Why?

Existing tools (SBMLDiagrams, SBMLNetwork) are primarily *visualisation* tools
that happen to expose a layout API as a side-effect. When you need to attach or
manipulate layout geometry programmatically — especially with alias (repeated)
species nodes or full bezier curves — you quickly hit undocumented requirements
of the underlying libsbml Layout package. SBMLLayout takes care of all of that
bookkeeping for you and includes its own skia-based renderer with full bezier
support.

## Quick start

```python
import tellurium as te
from SBMLLayout import Layout

r = te.loada('''
    J1: S1 -> S2; k1*S1
    k1 = 0.1; S1 = 10; S2 = 0
''')

layout = Layout(r.getSBML())
layout.addNode('S1', x=100, y=150, w=60, h=40)
layout.addNode('S2', x=300, y=150, w=60, h=40)
layout.addReaction('J1', center=(200, 170),
                   reactants=['S1'], products=['S2'])

layout.draw()                        # display inline in Jupyter / IDE
layout.draw(output='network.png')    # save PNG  (scale=2 for high-DPI)
layout.draw(output='network.pdf')    # save PDF
layout.draw(output='network.svg')    # save SVG
```

## Alias nodes

Species that appear more than once in a diagram (alias nodes) are
supported as a first-class concept. Call `addNode` multiple times with
the same species id and `alias=True` for each extra copy. Copy indices
are assigned in call order (first call = copy 0, second = copy 1, …).
Use `'A:1'` syntax in reaction lists to route arms to a specific copy.

```python
layout.addNode('A',  x=60,  y=200, w=50, h=40)             # prime  (copy 0)
layout.addNode('A',  x=280, y=300, w=50, h=40, alias=True)  # alias  (copy 1)

layout.addReaction('J1', center=(184, 205),
                   reactants=['S1'], products=['S2', 'A'])      # 'A' = copy 0
layout.addReaction('J2', center=(185, 346),
                   reactants=['S2', 'A:1'], products=['S3'])    # 'A:1' = copy 1
```

## Bezier curves

Pass a `handles` list to `addReaction` to specify cubic bezier control
points for each arm. Each entry is a pair of tuples
`((BP1x, BP1y), (BP2x, BP2y))` where BP1 is near the species node and
BP2 is near the junction. Use `None` for a straight-line arm.

```python
# Bi-uni reaction: S1 + S2 -> S3 with curved reactant arms
layout.addReaction('J1',
    reactants=['S1', 'S2'],
    products=['S3'],
    center=(250, 203),
    handles=[
        ((210, 102), (240, 195)),  # S1 arm: (BP1, BP2)
        ((210, 302), (240, 211)),  # S2 arm: (BP1, BP2)
        None,                       # S3 arm: straight line
    ])
```

## Direct reactions

For uni-uni reactions with no junction node, use `direct=True`. The line
connects the reactant directly to the product with no junction circle.

```python
# Straight direct line
layout.addReaction('J1', reactants=['S1'], products=['S2'], direct=True)

# Bezier direct curve
layout.addReaction('J1', reactants=['S1'], products=['S2'],
    direct=True,
    handles=[((160, 100), (240, 200))])  # single (BP1, BP2) pair
```

## Compartments

```python
layout.addCompartment('cell', x=10, y=10, w=500, h=400)
```

## Method chaining

All `add*` methods return `self`, so calls can be chained:

```python
layout = (Layout(r.getSBML())
    .addNode('S1', x=100, y=150, w=60, h=40)
    .addNode('S2', x=300, y=150, w=60, h=40)
    .addReaction('J1', center=(200, 170),
                 reactants=['S1'], products=['S2']))
layout.draw()
```

## Using the SBML with other tools

`toSBML()` returns a valid SBML Level 3 string that can be loaded into
any tool supporting the SBML Layout package:

```python
sbml = layout.toSBML()

# SBMLDiagrams
import SBMLDiagrams
df = SBMLDiagrams.load(sbml)
df.draw()

# Or save for use elsewhere
with open('model_with_layout.xml', 'w') as f:
    f.write(sbml)
```

## Styling

Pass a `Style` instance to `draw()` to customise the appearance. All
colours are `(R, G, B, A)` tuples with values 0–255.

```python
from SBMLLayout import Layout, Style

s = Style()
s.node_fill       = (200, 230, 255, 255)   # light blue fill
s.node_stroke     = (0,   80,  160, 255)   # dark blue border
s.reaction_stroke = (50,  50,  50,  255)   # dark grey lines
s.node_gap        = 10.0                   # gap between node edge and line
s.junction_visible = False                 # hide junction circles

layout.draw(output='network.png', scale=2, style=s)
```

### Full Style reference

| Attribute | Default | Description |
|-----------|---------|-------------|
| **Canvas** | | |
| `background_color` | `(255,255,255,255)` | Canvas background colour |
| `margin` | `30.0` | Padding around the network in pixels |
| **Compartments** | | |
| `compartment_fill` | `(240,240,255,180)` | Compartment fill colour |
| `compartment_stroke` | `(100,100,200,255)` | Compartment border colour |
| `compartment_stroke_width` | `2.0` | Compartment border width |
| `compartment_corner_radius` | `8.0` | Compartment rounded corner radius |
| **Species nodes** | | |
| `node_fill` | `(250,213,211,255)` | Node fill colour |
| `node_stroke` | `(130,37,31,255)` | Node border colour |
| `node_stroke_width` | `2.0` | Node border width |
| `node_corner_radius` | `6.0` | Node rounded corner radius |
| **Labels** | | |
| `label_color` | `(0,0,0,255)` | Text colour |
| `label_font_size` | `14.0` | Font size in points |
| `label_font_family` | `'Arial'` | Font family name |
| **Reaction lines** | | |
| `reaction_stroke` | `(0,0,0,255)` | Reaction line colour |
| `reaction_stroke_width` | `2.0` | Reaction line width |
| `node_gap` | `6.0` | Gap between node edge and start/end of reaction arms. Set to `0.0` for lines that touch the node edge exactly. Applies uniformly to reactant and product ends on all reaction styles. |
| **Junction circle** | | |
| `junction_fill` | `(130,37,31,255)` | Junction circle fill colour |
| `junction_radius` | `5.0` | Junction circle radius |
| `junction_visible` | `True` | Whether to draw the junction circle |
| **Arrowhead** | | |
| `arrow_fill` | `(0,0,0,255)` | Arrowhead fill colour |
| `arrow_length` | `12.0` | Arrowhead length (tip to base) in pixels |
| `arrow_width` | `7.0` | Arrowhead width at base in pixels |

## API reference

### `Layout(sbml_string)`

Create a new layout for the given SBML Level 3 model string.
Raises `ValueError` if the SBML is invalid or contains no model.

### `addNode(species_id, x, y, w=60, h=30, alias=False)`

Add a species node. `x, y` are the top-left corner of the bounding box.

### `addReaction(reaction_id, reactants, products, center=None, handles=None, direct=False)`

Add a reaction.

| Parameter | Description |
|-----------|-------------|
| `reactants` / `products` | Lists of species refs. `'S1'` = copy 0; `'A:1'` = copy 1 of A. |
| `center` | `(x, y)` junction point. Omit to let the renderer compute a default. |
| `handles` | One `((BP1x,BP1y),(BP2x,BP2y))` pair per arm (reactants first, then products), or `None` for a straight arm. Omit entirely for all-straight lines. |
| `direct` | If `True`, draw a single line/curve from reactant to product with no junction. For a direct bezier supply one handle pair. |

### `addCompartment(compartment_id, x, y, w, h)`

Add a compartment bounding box.

### `draw(output=None, scale=1.0, style=None)`

Render the network. If `output` is omitted, displays inline (Jupyter /
opens in default image viewer). Supported extensions: `.png`, `.pdf`,
`.svg`.

### `toSBML()`

Return a valid SBML Level 3 string with the Layout package embedded.

## Compatibility

The SBML produced by `toSBML()` is compatible with:
- [SBMLDiagrams](https://github.com/sys-bio/SBMLDiagrams)
- [SBMLNetwork](https://github.com/sys-bio/SBMLNetwork)
- Any tool supporting SBML Level 3 Layout package v1

## Note on bezier curves and SBMLDiagrams

SBMLDiagrams does not read bezier curve data from the SBML Layout
extension — it computes its own handle positions after loading. If you
need bezier curves with SBMLDiagrams, use its `setReactionBezierHandles`
API after `SBMLDiagrams.load()`. The SBMLLayout renderer reads and draws
the full two-control-point cubic bezier data correctly.