Skip to content

Approach A: moi scratch diagram β€” declarative diagrams with ELK auto-layout#27

Open
molefrog wants to merge 1 commit into
claude/scratchpad-visual-design-xkgqd6from
claude/scratchpad-visual-design-xkgqd6-a-autolayout
Open

Approach A: moi scratch diagram β€” declarative diagrams with ELK auto-layout#27
molefrog wants to merge 1 commit into
claude/scratchpad-visual-design-xkgqd6from
claude/scratchpad-visual-design-xkgqd6-a-autolayout

Conversation

@molefrog

@molefrog molefrog commented Jul 6, 2026

Copy link
Copy Markdown
Owner

The bet

For "draw how X works" β€” the bulk of agent drawing β€” the agent should never touch a coordinate. It declares structure; geometry is non-overlapping, aligned, and evenly spaced by construction.

Stacked on #26 (text-metrics foundation). Sibling approaches: B (relative tools), C (see + lint).

What it does

moi scratch diagram --id ts <<'EOF'
{
  "title": "How to expose a Tailscale service",
  "direction": "right",
  "nodes": [
    { "id": "browser", "label": "Browser (internet)", "color": "blue" },
    { "id": "proxy", "label": "Reverse proxy β€” holds the TLS cert for app.yourdomain.com", "color": "yellow", "width": 280 },
    { "id": "svc", "label": "Your service localhost:3000", "color": "green", "fill": "solid" }
  ],
  "groups": [{ "id": "tailnet", "label": "Tailscale network (private)", "color": "grey", "children": ["svc"] }],
  "edges": [
    { "from": "browser", "to": "proxy", "label": "https://app.yourdomain.com", "color": "blue" },
    { "from": "proxy", "to": "svc", "label": "tailnet IP", "elbow": true }
  ]
}
EOF

One command produced the motivating screenshot's diagram with zero overlaps, fitted labels, top-aligned group label, and even layer gaps.

How

  • server/scratchpad-diagram.ts β€” spec validation (unknown-key/reserved-id/cycle checks, errors name the exact entry) β†’ node sizing via fitRectToLabel β†’ ELK layered layout (elkjs, the engine behind D2/Mermaid's ELK mode; INCLUDE_CHILDREN for edges across group boundaries, model-order strategy for stable spec-order output, nested groups with label-aware padding and min-width) β†’ tldraw records in one atomic store.put. Arrows are bound to nodes so they follow later moves; edge labels ride the arrow's richText.
  • elkjs-under-Bun fix: Bun defines self, so elkjs's bundled fake-worker path misdetects its environment. We use elkjs/lib/elk-api.js with a real Worker (unref'd lazy singleton).
  • server/scratchpad-records.ts / scratchpad-style.ts β€” record builders and color/fill parsing extracted from the executor/CLI for reuse (no behavior change).
  • Auto-placement below existing canvas content when --at is omitted; --id prefixes every created shape id (printed on success) so the agent can adjust pieces afterwards.
  • Docs + agent skill (SCRATCHPAD.md, bumped 0.4.0 β†’ 0.5.0) teach: use diagram for any boxes-and-arrows; primitives are for annotations.

Tests

23 new tests: browser-loadable snapshots, no pairwise overlaps, rects β‰₯ measured labels, group containment, correct bindings, auto-place/--at, deterministic ids, 10 validation-error cases. Full suite 342 green; lint/format clean.

Trade-offs

Freeform annotation of the user's sketch isn't a graph; regenerating a diagram the user hand-tweaked can fight them. That's what approaches B/C cover β€” the long-term ship is likely A+B+C merged.

πŸ€– Generated with Claude Code

https://claude.ai/code/session_01A4d4g5CV89L1AanPrEYp7p


Generated by Claude Code

The agent declares nodes/groups/edges/title in a JSON spec and never touches
a coordinate: labels are measured with the real canvas font
(scratchpad-metrics), nodes sized via fitRectToLabel, positions computed by
elkjs's layered algorithm (real-worker API β€” Bun defines `self`, which breaks
the bundled fake-worker entry). Groups become padded hierarchical ELK nodes,
edges land as bound arrows that re-route in the browser, and everything goes
in as one batch with deterministic prefixed ids (<name>-<nodeId>,
<name>-title, <name>-edge-<i>) so the pieces stay addressable.

- lib/types.ts: ScratchDiagramSpec (+ node/group/edge), `diagram` op,
  ScratchOpResult gains `created`
- server/scratchpad-diagram.ts: spec validation (descriptive, actionable
  errors) + compiler; server/scratchpad-records.ts: record builders shared
  with the executor; server/scratchpad-style.ts: color/fill/size parsing
  shared with the CLI
- control assigns diagram ops a d_<uuid8> default name; CLI reads the spec
  from --spec or stdin and prints the created ids
- docs + workspace skill teach: diagram for any boxes-and-arrows structure,
  primitives for annotations/one-offs (skill version 0.5.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A4d4g5CV89L1AanPrEYp7p
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants