Skip to content

femtomc/pelican

Repository files navigation

Pelican

Pelican logo

A constraint-based diagram system designed for AI agents to build, inspect, and verify technical diagrams programmatically.

The problem

When an AI agent generates a diagram, it typically produces an image or markup it cannot inspect. It cannot ask "do these two boxes overlap?" or "is the label centered?" — it writes code, renders, and hopes. If the layout is wrong, it has no geometric feedback to guide corrections.

Pelican gives agents a queryable model of diagram geometry. Every element has a bounding box the agent can read. Every spatial relationship — containment, overlap, alignment, distance — can be checked programmatically. The agent builds a diagram, solves the layout, inspects the results, and fixes problems in a tight loop.

How it works

Pelican separates diagrams into marks (leaf elements with intrinsic size) and relations (layout constraints that position marks relative to each other). A single-pass local propagation solver computes all positions deterministically in linear time.

from pelican import Diagram, Rect, Circle, Text, Stack, Align

d = Diagram()
d.add(Rect("header", width=200, height=40, fill="#4A90D9", rx=6))
d.add(Text("title", "System Overview", font_size=14))
d.add(Circle("node", r=20, fill="#50C878"))

d.add(Stack("layout", ["header", "node"], direction="vertical", spacing=20))
d.add(Align("center_title", ["header", "title"], alignment="center"))
d.solve()

After solving, the agent can inspect every element's geometry:

d.bbox("header")
# {'left': 0.0, 'top': 0.0, 'width': 200.0, 'height': 40.0, ...}

d.bbox("node")
# {'left': 80.0, 'top': 60.0, 'width': 40.0, 'height': 40.0, ...}

And verify spatial properties:

d.disjoint("header", "node")   # True — no overlap
d.distance("header", "node")   # 20.0 — the spacing we requested
d.aligned("header", "node", "center_x")  # True — vertically centered

Rendering produces SVG:

d.to_svg("diagram.svg")  # file
d.to_png("diagram.png")  # file
d.render()                # inline in Jupyter

Constraint model

Each element carries an axis-aligned bounding box with eight properties: left, top, right, bottom, center_x, center_y, width, height. These form two independent 2-variable linear systems (one per axis) — setting any two properties on an axis determines the other two.

Dimension ownership prevents conflicting constraints. When a relation sets a dimension on an element, it claims ownership. If a second relation tries to set the same dimension, Pelican raises an OwnershipError with a diagnostic message naming both relations. The agent reads the error and restructures.

Relation order is the control surface. Relations execute in the order they are added. Position-setting relations (Stack, Grid) should be added before overlay relations (Align). This gives agents explicit, predictable control over layout evaluation.

Marks

Mark What it defines
Rect(id, width, height) Rectangle with optional position, corner radius, fill/stroke
Circle(id, r) Circle with optional center position
Text(id, content) Text with measured width/height (Pillow or heuristic)
Line(id, x1, y1, x2, y2) Line segment between two points
Path(id, d) SVG path data with explicit bbox
LaTeX(id, expression, use_external=False) Safe fallback text by default; trusted input can opt into latex + dvisvgm rendering

Relations

Relation What it constrains
Stack(id, children, direction, spacing) Sequential positioning along an axis
Align(id, children, alignment) Shared alignment on left, center_x, right, top, center_y, bottom, or center
Grid(id, children, cols, row_spacing, col_spacing) Row-major grid layout
Distribute(id, children, axis, spacing) Even distribution along an axis
Contain(id, children, padding) Background rectangle enclosing children with padding
Arrow(id, source, target) Directed connector with arrowhead, clipped to bbox edges

Plots

Pelican includes data visualization primitives that share the same constraint model:

from pelican import Axes, FunctionPlot, ScatterPlot, LinePlot
import math

d = Diagram()
d.add(Axes("ax", x_range=(0, 6.28), y_range=(-1.2, 1.2),
           width=280, height=200, x_label="x", y_label="sin(x)"))
d.add(FunctionPlot("curve", math.sin,
                    x_range=(0, 6.28), y_range=(-1.2, 1.2),
                    width=280, height=200, stroke="#E91E63"))
d.add(Align("overlay", ["ax", "curve"], alignment="center"))
d.solve()

ScatterPlot and LinePlot take x_data/y_data arrays. All plot marks participate in the same layout and verification system as other elements.

Verification

Every spatial predicate returns a boolean. verify() runs batch checks and returns a structured report:

report = d.verify([
    {"predicate": "contains", "args": ["outer", "inner"]},
    {"predicate": "disjoint", "args": ["a", "b"]},
    {"predicate": "aligned", "args": ["a", "b"], "axis": "center_x"},
])
report.all_passed   # True/False
report.summary()    # "Verification: 3/3 checks passed"
report.failures()   # list of CheckResult with diagnostic details

Individual checks: d.contains(a, b), d.overlaps(a, b), d.disjoint(a, b), d.distance(a, b), d.aligned(a, b, axis).

Verification uses Shapely for geometric predicates (DE-9IM model). The solver never calls Shapely — it stays fast with arithmetic only.

Inspection

d.state()           # all element bboxes as dicts
d.bbox("elem")      # single element bbox
d.ownership("elem") # which relation owns each dimension
d.snapshot()        # full scenegraph state including hierarchy
d.save_state("diagram-state.json")

clone = d.clone()
reloaded = Diagram.load_state("diagram-state.json")
rebuilt = Diagram.from_json(d.state().to_json())

Diagram.from_state(...), from_json(...), and load_state(...) rebuild a diagram from serialized specs and resolve it again. FunctionPlot roundtrips when its callable is importable (for example math.sin); anonymous lambdas and local closures remain cloneable in-memory but are not reconstructable from serialized state.

Bidirectional Edits

Pelican can expose the provenance of solved geometry and suggest parameter edits that would produce a desired geometric change:

d.solve(trace=True)

expr = d.trace("node", "center_x", resolve=True)
solutions = d.suggest_updates("node", "center_x", 180.0)
chosen = d.apply_suggestion("node", "center_x", 180.0)

d.set_param("layout", "spacing", 24).solve()
d.nudge("node", dx=10, dy=-5)

The Textual TUI also supports inverse nudging with the arrow keys after selecting an element.

Installation

Requires Python 3.12+. Managed with uv.

uv add pelican

Optional extras:

uv add "pelican[text]"   # Pillow for accurate text measurement
uv add "pelican[latex]"  # latextools for LaTeX rendering
uv add "pelican[png]"    # cairosvg for PNG export

For the installable CLI/TUI workflow:

uv tool install pelican
pelican --help

From a local checkout, uv tool install . exposes the same pelican command.

CLI Workflow

Pelican now treats the saved state file as the shared artifact between agents, scripts, and the TUI. The file persists:

  • element specs and build order
  • solved geometry
  • symbolic traces for inverse editing
  • UI metadata like the current TUI selection

Every mutating CLI command re-solves with trace=True before saving, so the state file stays ready for both agent inspection and interactive curation.

pelican init scene.json
pelican add scene.json Rect box --param width=200 --param height=80 --param x=20 --param y=20
pelican add scene.json Text title --param content="System Overview" --param font_size=14
pelican add scene.json Align center_title --child box --child title --param alignment=center
pelican summary scene.json
pelican doctor
pelican bbox scene.json box --json
pelican explain scene.json title center_x 180 --json
pelican suggest scene.json title center_x 180 --json
pelican ops scene.json '[{"op":"set","element_id":"box","param":"x","value":40}]' --diff --dry-run
pelican tui scene.json

Common commands:

  • pelican types lists supported marks, relations, and plots.
  • pelican doctor reports optional capabilities like TUI, PNG export, and external LaTeX support.
  • pelican state <file> prints the full persisted JSON document.
  • pelican add/remove/set mutates the scene file one command at a time.
  • pelican ops <file> ... applies a JSON list of edits in one batch for agent workflows.
  • mutating commands support --dry-run and --diff so agents can preview exact state changes before saving.
  • pelican trace/explain/suggest/apply/nudge drives inverse editing from the CLI.
  • pelican render <file> --svg-out out.svg --png-out out.png exports renders.
  • pelican tui <file> opens the Textual editor on the same file, restores selection state, autosaves by default, supports undo/redo with Ctrl+Z / Ctrl+Y, inverse nudging with the arrow keys, and direct source-param editing with p/P, h/l, H/L, and e.

Dependencies

Required: drawsvg (SVG generation), kiwisolver (linear constraint solving), shapely (geometric verification).

Optional: Pillow (text measurement), cairosvg (PNG export). Trusted LaTeX rendering requires latex and dvisvgm on the system PATH and must be enabled with use_external=True.

Inspirations

Pelican draws on a wide body of work in constraint-based graphics, program synthesis, and diagram systems.

Constraint-based diagram systems

  • Bluefish (Pollock et al., UIST 2024) — The most direct architectural influence. Pelican's dimension ownership model, mark/relation separation, and single-pass local propagation solver are adapted from Bluefish's compound scenegraph design. Bluefish showed that local propagation with ownership tracking produces deterministic, debuggable layouts that scale linearly.
  • Penrose (Ye et al., SIGGRAPH 2020) — Demonstrated that mathematical diagrams can be generated from declarative specifications with automatic layout via optimization. Penrose's Bloom API influenced our inspectable geometry model. We chose local propagation over Penrose's L-BFGS optimization for determinism and debuggability, but Penrose's rich constraint library (contains, disjoint, perpendicular, collinear) informed our verification predicates.
  • SketchPad (Sutherland, 1963) and ThingLab (Borning, 1981) — The original constraint-based graphics systems. Established that graphical objects should be defined by constraints between their properties, not by explicit coordinates.

Constraint solvers and propagation

  • DeltaBlue (Freeman-Benson & Maloney, 1990) — The foundational incremental local propagation algorithm. Introduced walkabout strength annotations and the separation of constraint planning from execution.
  • SkyBlue (Sannella, 1994) — Extended DeltaBlue with multi-way constraints and cycle solvers.
  • Cassowary (Badros, Borning & Stuckey, 2001) — Incremental simplex-based linear constraint solver. Pelican uses kiwisolver (a C++ Cassowary implementation) as an optional backend for linear systems.
  • Babelsberg (Felgentreff et al., 2014) — Showed how constraint solving can be integrated into object-oriented programming with cooperating solver backends.
  • HotDrink (Freeman et al.) — Multi-way dataflow constraint systems for reactive GUI programming.

Bidirectional manipulation and program synthesis

  • Sketch-n-Sketch (Chugh et al., PLDI 2016) — The direct inspiration for Pelican's trace system and inverse solver. Sketch-n-Sketch instruments program evaluation to produce run-time traces that record how each output value was computed from source-code literals. When the user directly manipulates the output, trace equations are inverted to infer program updates. Pelican implements this approach: every solved dimension carries a symbolic trace expression, and the inverse solver finds parameter changes by walking the expression tree.
  • Lillicon (Bernstein & Li, TOG 2015) — Synthesized different program representations for the same graphic design based on intended edits, eliminating the need for secondary direct manipulation edits.

AI-assisted diagram generation

  • AIDL (Jones et al., Computer Graphics Forum 2025) — Solver-aided hierarchical DSL for LLM-driven CAD. Demonstrated the key architectural insight that Pelican builds on: separate high-level structural reasoning (what LLMs are good at) from low-level geometric computation (what constraint solvers are good at). In few-shot settings, AIDL outperforms even languages the LLM was trained on.
  • MagicGeo (arXiv 2025) — Training-free geometric diagram generation via LLM autoformalization into constraints with formal solver verification.
  • GeoLoom (arXiv 2025) — Text-to-diagram via formal constraint language and Monte Carlo coordinate optimization.
  • LayoutGPT (Feng et al., NeurIPS 2023) — LLM as visual planner via CSS-style prompts for layout generation.
  • SVGenius (ACM MM 2025) — Benchmark for LLM SVG understanding, editing, and generation.

Visualization grammars and tools

  • Vega / Vega-Lite (UW IDL) — Declarative visualization grammar with automatic layout from high-level specifications.
  • Charticulator (Ren et al., Microsoft Research, 2018) — Transforms chart specifications into mathematical layout constraints solved via sparse linear programming.
  • D3-force — Velocity Verlet integration with pluggable forces, treating links as weak geometric constraints.
  • Manim (3Blue1Brown) — Programmatic math animation engine. Influenced our approach to function plots and mathematical notation.
  • Graphviz — Text-based graph layout (DOT language), demonstrating that agent-friendly diagram tools should accept text input.

Python ecosystem

  • kiwisolver — Fast C++ Cassowary implementation with Python bindings, used by matplotlib internally. Pelican's optional linear constraint backend.
  • Shapely — Computational geometry library (DE-9IM spatial predicates). Powers Pelican's verification layer.
  • drawsvg — SVG generation with Jupyter display support. Pelican's rendering backend.
  • Adaptagrams (Wybrow et al.) — C++ constraint-based layout and connector routing library with Python SWIG bindings. Informed our approach to separation constraints.
  • Gaphas — Python diagramming widget with built-in constraint solver. Demonstrated that constraint-based diagramming is viable in pure Python.

About

Constraint-based technical diagrams for AI agents

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages