# Architecture Overview

This page covers the internal structure of PyFreeform -- how modules are organized, how entities relate to surfaces, and how SVG rendering works end to end.

## Module Structure

```
pyfreeform/
  core/           # Foundational abstractions
    entity.py       # Entity ABC -- base for all drawable objects
    surface.py      # Surface base class -- Cell, Scene, CellGroup
    binding.py      # Binding dataclass -- immutable positioning config
    pathable.py     # Pathable protocol -- point_at(t) interface
    connection.py   # Connection -- reactive link between entities
    coord.py        # Coord (x, y) NamedTuple
    relcoord.py     # RelCoord (rx, ry) NamedTuple
    tangent.py      # Tangent angle utilities for pathables
    bezier.py       # Parametric curve math (arc length, Bézier fitting, curvature)
    svg_utils.py    # SVG attribute helpers (opacity, fill/stroke, XML escaping)
    protocols.py    # Animatable protocol -- renderer interface for entity inspection

  entities/       # Concrete entity implementations
    dot.py          # Dot (circle)
    line.py         # Line (segment between two points)
    curve.py        # Curve (quadratic Bezier)
    ellipse.py      # Ellipse (with parametric support)
    rect.py         # Rect (rectangle with rotation)
    polygon.py      # Polygon (arbitrary vertices, entity-reference vertices)
    text.py         # Text (with textPath support)
    path.py         # Path (renders any Pathable as smooth SVG)
    point.py        # Point (invisible positional anchor)
    entity_group.py # EntityGroup (composite entity)

  scene/          # Top-level container
    scene.py        # Scene -- owns grids, entities, rendering

  grid/           # Spatial organization
    grid.py         # Grid -- rows x cols of cells
    cell.py         # Cell -- extends Surface, has image data
    cell_group.py   # CellGroup -- multi-cell region

  paths/          # Built-in path shapes (Pathable implementations)
    base.py         # PathShape base class (shared arc_length, to_svg_path_d)
    wave.py         # Wave (sinusoidal wave between two points)
    spiral.py       # Spiral (Archimedean spiral)
    lissajous.py    # Lissajous (parametric Lissajous curve)
    zigzag.py       # Zigzag (triangle wave between two points)

  config/         # Configuration and extensibility
    styles.py       # Style dataclasses (5 classes)
    caps.py         # Cap registry engine (register, resolve, render)
    cap_shapes.py   # Built-in cap shapes (arrow, arrow_in, diamond)
    palette.py      # Color palette utilities

  image/          # Image loading and processing
    image.py        # Image loader
    layer.py        # Layer abstraction (color, brightness, alpha)
    resize.py       # Image resizing utilities

  color.py        # Color parsing and conversion
  display.py      # Jupyter/notebook display helpers
```

## The Surface Protocol

`Surface` is the base class that provides entity management and builder methods. Three classes extend it:

```
Surface (base)
  |-- Scene       # Top-level SVG document, owns grids and connections
  |-- Cell        # Single grid cell, has image data (color, brightness)
  |-- CellGroup   # Rectangular selection of cells
```

### What Surface provides

Every Surface has a rectangular region (`_x`, `_y`, `_width`, `_height`) and a list of entities (`_entities`). It provides position resolution, 12 builder methods, entity management, anchors, connections, custom data, and parametric positioning. See the [Drawing](../api-reference/drawing.md) reference for the complete API.

### Subclass responsibilities

Subclasses call `super().__init__(x, y, width, height)` which sets up geometry and shared storage:

```python
class Cell(Surface):
    def __init__(self, grid, row, col, x, y, width, height):
        super().__init__(x, y, width, height)
        # Surface.__init__ provides:
        #   _x, _y, _width, _height  (geometry)
        #   _entities = []            (entity storage)
        #   _connections = {}         (connection endpoint tracking)
        #   _data = {}                (custom data dictionary)
        self._grid = grid            # Cell-specific state
        self._row = row
        self._col = col
```

!!! note "Surface vs Entity"
    `Surface` and `Entity` are independent hierarchies. A Surface **contains** entities (composition). An Entity **references** its containing surface via `entity.surface`. Both are **connectable** — they share `anchor()`, `connect()`, `connections`, and `data`. The `EntityGroup` is the one entity that also contains other entities, but it does so through SVG `<g>` transforms, not through the Surface protocol.

## Entity Class Hierarchy

`Entity` is the abstract base for everything that can be drawn:

```
Entity (ABC)
  |-- Dot              # Simple circle
  |-- Line             # Segment with cap support
  |-- Curve            # Quadratic Bezier with cap support
  |-- Ellipse          # Oval with parametric support
  |-- Rect             # Rectangle with rotation
  |-- Polygon          # Arbitrary vertices (static or entity-reference)
  |-- Text             # Text with optional textPath
  |-- Path             # Renders any Pathable with cap support
  |-- Point            # Invisible positional anchor
  |-- EntityGroup      # Composite entity (children in <g>)
```

### What Entity provides

Every entity has:

- **Position** (`_position: Coord`) -- the entity's reference point
- **Z-index** (`_z_index: int`) -- layer ordering for rendering
- **Surface reference** (`_surface: Surface | None`) -- back-reference to container
- **Connections** (`_connections: dict[Connection, None]`) -- connections where this entity is an endpoint (insertion-ordered)
- **Data** (`_data: dict[str, Any]`) -- custom data dictionary for user metadata
- **Movement** -- private `_move_to()` / `_move_by()` for pixel movement; public API is `.position`, `.at`, and `move_to_surface()`
- **Binding** -- `.binding` property accepts a `Binding` dataclass (from `core/binding.py`) for relative positioning configuration
- **Relative tracking** -- `.is_relative` is `True` when any property is stored as a fraction (position, size, geometry). Relative entities react to container changes; pixel entities are static. Each property is independently relative or absolute.
- **Transforms** -- `rotate(angle, origin)` and `scale(factor, origin)` are **non-destructive**: they accumulate `_rotation` and `_scale_factor` without modifying geometry or clearing relative state. With `origin`, the entity orbits/scales via `_move_by()`, which preserves relative bindings. SVG rendering applies transforms via `_build_svg_transform()`.
- **Transform properties** -- `.rotation` (degrees), `.scale_factor` (multiplier), `.rotation_center` (pivot point — default: position; overridden per entity type)
- **World-space helpers** -- `_to_world_space(point)` applies scale then rotation around `rotation_center`. Used by `anchor()` and `bounds()`.
- **Fitting** -- `fit_within()` scales to fit a target; `fit_to_surface()` delegates to `fit_within()` using the containing surface's bounds
- **Connectivity** -- `connect()`, `place_beside()`

### Abstract methods every entity must implement

```python
@property
@abstractmethod
def anchor_names(self) -> list[str]:
    """List available anchor names."""

@abstractmethod
def _named_anchor(self, name: str) -> Coord:
    """Return anchor point by name (called by the concrete anchor() method)."""

@abstractmethod
def to_svg(self) -> str:
    """Render to SVG element string."""

@abstractmethod
def bounds(self, *, visual: bool = False) -> tuple[float, float, float, float]:
    """Return (min_x, min_y, max_x, max_y). visual=True includes stroke width."""
```

The public `anchor(spec)` method is **concrete** on the base class — it dispatches string names to `_named_anchor()` and `RelCoord`/tuple values to `_anchor_from_relcoord()` (which resolves against `bounds()` by default). Entities only override `_named_anchor()` for string anchor resolution, and optionally `_anchor_from_relcoord()` for rotation-aware coordinate mapping (e.g., `Rect`).

### Cap/Marker Functions

Entities with stroked paths (Line, Curve, Path, Connection) share cap/marker logic through free functions in `config/caps.py`:

- `collect_markers(cap, start_cap, end_cap, width, color)` -- returns SVG `<marker>` definitions needed for caps
- `svg_cap_and_marker_attrs(cap, start_cap, end_cap, width, color)` -- computes `stroke-linecap` and `marker-start`/`marker-end` attributes

Each stroked entity also provides `effective_start_cap` / `effective_end_cap` properties for resolving per-end overrides.

## SVG Rendering Pipeline

When you call `scene.to_svg()` or `scene.save("art.svg")`, this pipeline executes:

```
scene.to_svg()
    |
    |-- 1. Write SVG header (<svg xmlns=... width=... height=...>)
    |
    |-- 2. Collect definitions (<defs>)
    |   |-- _collect_markers()    # Arrow caps, custom marker caps
    |   |-- _collect_path_defs()  # <path> elements for textPath
    |
    |-- 3. Render background (<rect width="100%" ...>)
    |
    |-- 4. Collect all renderables
    |   |-- Connections  -> (z_index, svg_string)
    |   |-- Entities     -> (z_index, svg_string)
    |   |   |-- scene._entities (direct entities)
    |   |   |-- grid.all_entities() for each grid
    |   |       |-- cell._entities for each cell
    |
    |-- 5. Sort by z_index (stable sort)
    |
    |-- 6. Render in sorted order
    |
    |-- 7. Close </svg>
```

### Step-by-step detail

**Step 2 -- Collecting definitions.** The scene walks every entity and connection looking for marker-based caps (like `"arrow"`) and textPath path definitions. This is how entities can inject shared SVG `<defs>` without duplication:

```python
def _collect_markers(self) -> dict[str, str]:
    markers: dict[str, str] = {}
    for entity in self.entities:
        if hasattr(entity, "get_required_markers"):
            for mid, svg in entity.get_required_markers():
                markers[mid] = svg  # dict deduplicates by ID
    # ... also checks connections
    return markers
```

**Step 4 -- Collecting entities.** The `scene.entities` property aggregates entities from all sources:

```python
@property
def entities(self) -> list[Entity]:
    result = list(self._entities)        # Direct scene entities
    for grid in self._grids:
        result.extend(grid.all_entities())  # All cell entities
    return result
```

**Step 5 -- Z-index sorting.** Python's `sort()` is stable, so entities with the same `z_index` preserve their insertion order:

```python
renderables.sort(key=lambda x: x[0])  # Sort by z_index
```

**Step 6 -- Rendering.** Each entity's `to_svg()` is called exactly once. The returned string is indented and appended to the output:

```python
for _, svg in renderables:
    lines.append(f"  {svg}")
```

### Marker deduplication

The cap system uses deterministic marker IDs based on cap name, color, and size. Two arrows with the same color and width share a single `<marker>` definition:

```python
# From config/caps.py
def make_marker_id(cap_name, color, size, *, for_start=False):
    clean = color.lstrip("#").lower()
    size_str = f"{size:.1f}".replace(".", "_")
    suffix = "-start" if for_start else ""
    return f"cap-{cap_name}-{clean}-{size_str}{suffix}"
```

!!! tip "Extending the pipeline"
    To add a new entity type that needs shared SVG definitions, implement `get_required_markers()` and/or `get_required_paths()` on your entity. The scene's rendering pipeline will automatically discover and deduplicate them.

## Key Design Decisions

### Composition over inheritance

Surfaces contain entities; entities reference their surface. There is no deep inheritance tree.

### Connectable protocol

Both `Entity` and `Surface` are connectable — they share the same connection interface (`anchor(spec)`, `anchor_names`, `connect()`, `add_connection()`, `remove_connection()`, `connections`). The `Connectable` type alias (`Entity | Surface`) captures this union. Anchors accept `AnchorSpec` — a string name, `RelCoord`, or `(rx, ry)` tuple — for arbitrary anchor positioning.

Connections register themselves with both endpoints on construction and remove themselves via `disconnect()`. Each connectable tracks its own connections in an insertion-ordered `dict` (used as an ordered set for deterministic SVG output):

```python
# Entity
self._connections: dict[Connection, None] = {}

# Surface (Cell, CellGroup, Scene)
self._connections: dict[Connection, None] = {}
```

The scene auto-collects all connections at render time by walking entities and grid cells — no explicit adding needed.

### Custom data

Every entity, surface, and connection carries a `data: dict[str, Any]` for user metadata. This provides a consistent way to attach application-specific state:

```python
cell.data["type"] = "source"
dot.data["visited"] = True
conn.data["weight"] = 0.75
```

Cell additionally has typed data properties (`brightness`, `color`, `alpha`) populated from image loading.

### Connection geometry

Connections support three geometry modes controlled by constructor arguments:

- **Line** (default): No pre-computation. Rendered as a direct `<line>` between the live anchor positions.
- **`curvature=`**: A normalized bezier arc is built from shared utilities in `core/bezier.py` (`curvature_control_point` + `quadratic_to_cubic`). The same degree-elevation math is shared with the `Curve` entity — no duplication.
- **`path=`**: The pathable is sampled and fitted into smooth cubic segments via `fit_cubic_beziers()` in `core/bezier.py` (Hermite interpolation).

At render time, curve and path geometries are automatically stretched and rotated (affine transform) to connect the actual anchor endpoints. Pass `visible=False` to create an invisible connection — `to_svg()` returns an empty string but `point_at(t)` still works. Connections accept any `Connectable` endpoint — entity-to-entity, cell-to-cell, or entity-to-cell.

### Entity-reference vertices

Polygon vertices can be static `Coord` values or live entity references (`Entity` or `(Entity, "anchor_name")`). Internally, the Polygon stores a `_vertex_specs` list and resolves references at render time via `_resolve_vertex()`. This gives polygons reactive behavior — when a referenced entity moves, the polygon deforms automatically. The `Point` entity (which renders nothing) exists specifically to serve as an invisible positional anchor for these vertex references.

### Relative coordinate system

Surface builder methods accept named positions (`"center"`, `"top_left"`) or relative tuples `(rx, ry)` where `(0, 0)` is top-left and `(1, 1)` is bottom-right. This keeps cell-level code resolution-independent.

Internally, relative positions are stored as `RelCoord(rx, ry)` — a `NamedTuple` with `rx` and `ry` fields (see `core/relcoord.py`). The `.at` property on every entity returns and accepts `RelCoord` values. The `.binding` property accepts a `Binding` dataclass for full positioning configuration (relative position, path-following, reference entity). Users reposition entities through `.position`, `.at`, or `move_to_surface()` — low-level pixel movement (`_move_to`, `_move_by`) is private.

### Entity.surface back-reference

Every entity knows which surface it lives in via `entity.surface`. This enables `fit_to_surface()` to work without passing the surface explicitly:

```python
dot = cell.add_dot(radius=0.15)
dot.fit_to_surface(0.85)  # Knows its surface, scales to fit
```
