---
name: Architecture Drawer
slug: architecture-drawer
category: DevOps
description: Converts complex technical descriptions into structured SVG architecture diagrams with automatic layout validation and quality scoring. Also exports to editable PowerPoint presentations.
github: "https://github.com/Andy1314Chen/architecture-drawer/tree/main/plugins/architecture-drawer/skills/architecture-drawer"
language: Python
stars: 29
forks: 1
install: "npx degit https://github.com/Andy1314Chen/architecture-drawer/tree/main/plugins/architecture-drawer/skills/architecture-drawer ~/.claude/skills/architecture-drawer"
installs_to: ~/.claude/skills/architecture-drawer
source_path: plugins/architecture-drawer/skills/architecture-drawer/SKILL.md
collection_size: 1
category_size: 798
added: 2026-08-11T07:21:56.134Z
last_synced: 2026-08-11T07:21:56.134Z
canonical_url: "https://dirskills.com/skills/architecture-drawer"
---

# Architecture Drawer

Converts complex technical descriptions into structured SVG architecture diagrams with automatic layout validation and quality scoring. Also exports to editable PowerPoint presentations.

**Install:**

```bash
npx degit https://github.com/Andy1314Chen/architecture-drawer/tree/main/plugins/architecture-drawer/skills/architecture-drawer ~/.claude/skills/architecture-drawer
```

## README

# SVG Architecture Drawer (Smart Version)

This Skill converts complex technical descriptions into structured SVG architecture diagrams. It integrates layout constraints, collision detection, **connection/arrow connectivity validation**, and quality evaluation, automatically identifying and guiding the correction of layout errors.
The script directory (referred to as `$SKILL` below) is this skill's own
`scripts/` folder. From a generator script that lives next to its artifacts,
resolve it relative to the script's own location (never hard-code an absolute
path, which breaks on other machines):

```python
import os, sys
_HERE = os.path.dirname(os.path.abspath(__file__))
# From evals/<name>/gen.py -> ../../scripts ; adjust depth for your layout.
_SKILL = os.path.normpath(os.path.join(_HERE, "..", "..", "scripts"))
if _SKILL not in sys.path:
    sys.path.insert(0, _SKILL)
```

## Core Workflow: Generate-Evaluate-Correct

```dot
digraph eval_loop {
    "Generate gen.py" -> "evaluate_svg()" [label="run"];
    "evaluate_svg()" -> "score≥100 AND no [FAIL]?" [label="score"];
    "score≥100 AND no [FAIL]?" -> "Done" [label="yes"];
    "score≥100 AND no [FAIL]?" -> "auto_refine(drawer, max_iter=3)" [label="no"];
    "auto_refine(drawer, max_iter=3)" -> "score≥80?" [label="after n iterations"];
    "score≥80?" -> "Manual fix (coordinates/text)" [label="yes · ship"] ;
    "score≥80?" -> "Regenerate gen.py" [label="no · restart"];
}
```

1. **Content Parsing & Planning**:
   - Identify layers, components, and flow direction.
   - Determine canvas dimensions (default 1200x800).

2. **Coding & Layout**:
   - Write a Python script calling `$SKILL/svg_utils.py`.
   - **Required**: use `drawer.check_collisions()` to check overlaps; use the semantic API (below) to register nodes and edges so connections can be auto-validated.

3. **Quality Evaluation**:
   - Call `evaluate_svg(drawer)` from `$SKILL/evaluator.py`.
   - Evaluation dimensions: ① **Containment-aware** element overlap detection (parent-child nesting does not count as a collision); ② boundary checks; ③ canvas coverage; ④ **connection/arrow connectivity** (endpoints must land on registered node borders, 12px tolerance; also detects degenerate zero-length edges and duplicate edges); ⑤ **phantom anchor detection** (nodes referenced by edges but invisible); ⑥ **edge-routes-through-node** (edges must not pass through the interior of a non-endpoint node, 3px inset); ⑦ **edge crossing** (two edge segments intersecting internally); ⑧ **same-kind node minimum spacing** (Euclidean distance between op/junction nodes ≥ 14px); ⑨ **font-size tier detection** (parse SVG to extract all `font-size` values, deduplicate to ≤4 tiers, adjacent tiers must be ≥1.15× apart — prevents accidental micro-steps like 11/12/13/14); ⑩ **palette detection** (accent colors ≤8 warning, ≤12 hard limit; coexistence of very dark L<0.2 and very light L>0.8 accents is flagged as a conflict; background defaults to light); ⑪ **text overflow detection** (parse all `<text>` elements' true geometry, estimate text width by font metrics: overflowing the canvas = FAIL, text wider than its container [smallest rectangle containing the text center] = WARN — closes the blind spot of text drawn via `add_element` that doesn't enter `bboxes` and is invisible to collision/boundary checks); ⑫ **composition quality budget** (ported from fireworks `assess_composition`: ≤2 bends per edge, path stretch ratio ≤1.35, container gutter ≥20px, shortest path segment ≥16px; **text as a measurable obstacle** — edge segments passing through a `<text>` bbox = FAIL, systematically eliminating "text crossed/covered by edges"; gutter is only checked for nodes fully contained within a container, cross-band nodes are not false-flagged). ⑨⑩⑪⑫ **parse the actual SVG** rather than relying on API calls, so they work equally well for raw `add_element` drawing.
   - ⑬ **text-vs-shape & text-vs-text overlap detection** (`check_text_overlaps`): parses every `<text>` bbox (center model, `dominant-baseline="central"`) against all visible circles/rects/polygons/lines/paths AND against other `<text>` — closes the registry blind spot where `bbox=False` text/shapes and `add_element` shapes are invisible to `check_collisions`. Legend/background shapes, the full-canvas bg rect, and rects fully containing the text (intentional in-box labels) are exempt. Like ⑨⑩⑪⑫, this **parses the actual SVG** rather than trusting the API.
   - ⑭ **same-kind peer alignment** (`check_alignment`): two SAME-SIZED same-kind visible nodes that read as a row or column (strong overlap on the perpendicular axis) yet share NEITHER a top/bottom/left/right edge (within 5px) NOR a center line (within 15% of the shorter side) are flagged — the "align to shared edges" layout principle. Differently-sized peers are skipped (a row of varied components legitimately staggers).
   - ⑮ **text-on-fill contrast** (`check_contrast`): WCAG 2 contrast ratio between each `<text>`'s fill and the fill of the smallest `<rect>` containing it — FAIL below 3:1 (large-text floor), WARN below 4.5:1 (AA for normal text) / 3:1 large (≥24px, or ≥18.5px bold). Only text on a **non-neutral (accent)** fill is measured; accent-colored text on a white/neutral canvas (category labels, captions) is a typographic choice, not a fill defect, and is skipped. Replaces the former "manual review recommended" placeholder.

4. **Auto-Correction**:
   - If the evaluation score is below **80**, analyze the `[FAIL]` items in the report.
   - Connection issues (`dangles` / `Degenerate edge` / `overlaps`): use `drawer.connect(...)` to let endpoints auto-snap to node borders; avoid manually computing offset coordinates.
   - `phantom` (phantom anchors): the node referenced by an edge is invisible → give it a real fill/stroke, or use the distinct-port pattern to connect to a visible junction.
   - `routes through node`: an edge cuts through an intermediate node → reroute via orthogonal bypass channels, or relay through a junction (see distinct-port pattern), keeping a ≥20px gap from the intermediate node.
   - `cross` (edge crossings): adjust node layout or routing channels so edges don't intersect (reference fireworks' zero-crossing budget).
   - `too close`: same-kind nodes are clustered → increase spacing or enlarge the canvas.
   - Arrow position: `connect()` auto-retracts by `marker_tip_depth` — retraction = `(markerWidth − refX) × stroke_width`, derived from the marker dimensions registered by `arrow_head()`, so the arrow tip lands exactly on the target border (neither poking in nor leaving a gap). For custom markers, pass the real dimensions via `arrow_head(id, color, marker_width=, ref_x=)` — no manual tweaking needed.
   - `font` (font sizes): more than 4 distinct tiers after dedup → converge to 3-4 tiers (title/body/note); near-overlapping tiers (ratio <1.15, e.g. 11/12) → merge into one tier. Recommended modular scale: 20 / 14 / 12 / 10 (all steps ≥1.15). This matches the tier count measured in each ink-graph style.
   - `palette`: accent count >8 → revert op fills to neutral white, or switch to a single-hue scheme (S1 Monochrome Blue, only 5 accents) so layer fills + one unified border carry the palette; >12 → trim to a preset scheme. Luminance conflict (very dark + very light coexist) → unify into one brightness family. Non-light background → apply white by default; dark themes must declare `set_background()`/`bg=`. See `references/design_specs.md` for the 4 preset schemes (S1–S4) and when to use each.
   - Layout issues: adjust component coordinates, spacing, or scale ratio, then regenerate.
   - `text` overflow: text exceeds the canvas → shorten the copy or shift the start point left; text wider than its card/container → shorten, auto-wrap by container width (greedy word-wrap), or widen the container. Note that `<text>` does not enter `bboxes` by default, so collision/boundary checks can't see it — this detection fills that gap.
   - `text overlap` (text on a shape or another text): a label sits on top of a circle/triangle/arc/line or collides with a neighboring label → move the label clear of the shape (place it above/below the icon, not on it) or shorten it. `auto_refine` cannot fix this (no geometry handle for raw `add_element` text) — adjust coordinates manually. This catches overlaps `check_collisions` misses because `bbox=False` text/shapes and `add_element` shapes bypass the collision registry.
   - `contrast` (low text-on-fill contrast): a label doesn't read against its accent card (ratio <3:1 FAIL, <4.5:1 WARN for normal text) → darken/lighten the text fill toward the channel extreme (pure `#000000`/`#ffffff` on a mid-tone card is always safe), or switch the card to a lighter tint of the same hue so a dark label clears AA. Note: accent-colored text on a **neutral** canvas is a deliberate category/heading choice and is not flagged — only labels on accent fills are. `auto_refine` does not touch colors; adjust manually.
   - `alignment` (misaligned same-size peers): two same-sized same-kind nodes in a row/column share no edge/center line → nudge one onto the other's top/bottom (row) or left/right (column) edge, or onto a shared center line. Differently-sized peers are exempt (they legitimately stagger). `auto_refine` does not handle alignment yet — adjust coordinates manually.
   - `composition` gutter (insufficient container margin): node too close to the container edge → push the node toward the container center; or call **`auto_refine(drawer)`** (below) to iteratively auto-correct.
   - **`auto_refine(drawer, target_score=100, max_iter=3)`**: reads the `evaluate_svg` report and auto-corrects programmable issue categories (gutter → nudge node toward container center; too close → spread along the primary axis), looping until the target is met or iterations are exhausted. Returns `(score, report, fixes)`. Complex fixes (dangles/cross/route-through) still need manual intervention — auto_refine only handles geometric micro-adjustments.

## Node & Edge Semantics

For the evaluator to "see" connections, drawing code must register connectable rectangles as **nodes** and connections as **edges**:

- **Register nodes**: `drawer.rect(..., node_id="op1", node_kind="op")` — draws a rectangle and registers a node simultaneously. `node_kind` can be `"op" | "layer" | "block" | "region"`; used for provenance only, not for validation.
- **Circle nodes (junctions/markers)**: `drawer.circle(cx, cy, r, ..., node_id="jn", node_kind="junction")` — draws a visible circle and registers a square Node of side 2r as a snap anchor; endpoints landing at the center register distance 0. Ideal for bus junctions and port markers.
- **Register edges**: prefer `drawer.connect(from_id, from_side, to_id, to_side, ...)` — endpoints are taken from node border midpoints (`"top"|"bottom"|"left"|"right"`), so arrows always land precisely on the node edge. Options: `dashed=True` (dashed, e.g. lowering/bypass flows), `as_curve=True` + `curve_dir="left"|"right"` (curve), `edge_label=` (annotation).
- **Dashed rendering**: `dashed=` is available on **all** primitives — `rect`, `circle`, `line`, `path`, and `connect`. Pass `dashed=True` for the standard `"6,3"` pattern, or `dashed="4,3"` for a custom dash pattern. This replaces the old `extra='stroke-dasharray="..."'` spelling (which still works for backward compatibility).
- **Low-level entry**: `drawer.line(..., register_edge=True)` / `drawer.path(..., register_edge=True, start=..., end=...)` can also manually register edges (for curves, `start/end` are the semantic endpoints; `d` can be any path).
- **`group(transform)` context**: nodes/edges/bboxes drawn inside `with drawer.group("translate(100,50) rotate(30)"):` are registered in **absolute coordinates** via the accumulated affine matrix, so local coordinates inside a group are also validated. Supports chained `matrix()/translate()/scale()/rotate()/skewX()/skewY()`.
- **Advanced shapes** (ported from ink-graph `shapes.md`, local coordinates via `<g transform>`): `drawer.database(x,y,w,h,...)` (cylinder, top ellipse depth=min(8,h*0.12)), `drawer.decision(...)` (diamond, four points around center), `drawer.hexagon(...)` (gateway, 25% corner insets), `drawer.component(...)` (with left-edge double tabs), `drawer.cloud(...)` (multi-lobe cubic curves). All accept `node_id/role/label`, register nodes consistently with `rect()`/`circle()`, and support connection snapping. Text centering uses `dominant-baseline="central"` (exact, replacing the old y+0.35*fs approximation).
- **Semantic role `role=`** (optional): `rect/circle/connect/line/path` all accept `role="node|edge|decoration|legend|background"`. Elements set to `decoration`/`legend`/`background` emit a `data-graph-role` attribute and are **excluded from business checks** (spacing, collision, palette count) — used for decorative layers (rail casings, background textures, legends). Default `node`/`edge` means business elements.
- **Math formulas with sub/superscripts**: `drawer.formula(x, y, markup, font_size=, fill=, anchor=, weight=)` renders genuine `<tspan>` baseline shifts — unlike `text()` (which HTML-escapes content and can only show literal underscores/carets). Markup: `_{...}` → subscript, `^{...}` → superscript; baseline auto-resets between tokens so multiple indices align (e.g. `"F_{k} = MS^{↑}_{k} + g_{k}"`). Default monospace family + bold for an equation look; pass `weight="normal"` for inline annotations. The sub/superscript glyph sizes (~0.72×) are derivative of the parent text size and are **excluded from the font-tier count** (see `check_font_scale`), so formulas do not inflate the 3–4-tier typography budget. (Note: `svg2pptx` concatenates `<tspan>` text flat — use image mode if you need exact subscript fidelity in PowerPoint.)

> **"Invisible anchor" anti-pattern (now forcefully blocked)**: it used to be possible to create `fill="none" stroke="none"` invisible rectangles to cheat the connection validation — the evaluator would pass, but the human eye would see dangling lines. Now `check_phantom_anchors()` detects any node referenced by an edge that is invisible (`fill=none ∧ stroke=none`/opacity=0/zero-size) and flags it as FAIL. When you need a "bus rail" or cross-layer channel, use the distinct-port pattern below to connect to a visible junction.

> **Distinct-port / junction pattern** (cross-layer aggregation, side channels): place visible circular junction nodes at the channel position, connect each real component to the junction with a short solid line `connect(layer, "left", junction, "right")`, then chain them into a rail with dashed lines `connect(junction, "bottom", junction2, "top", dashed=True)`. This way each rail segment lands between real visible nodes — passing validation while remaining clear to humans.

> Tip: container-type large rectangles (Module/Layer) enter bbox collision and coverage stats by default; for interior small elements (text, operation nodes), pass `bbox=False` to avoid false overlap reports or inflated coverage.

## Design Specifications
- **Font-size tiers**: use only 3-4 tiers per diagram (title 20 / section header 14 / body 12 / note 10), adjacent tiers ≥1.15× apart (modular type scale). Exceeding this triggers an evaluator warning.
- **Palette**: pick by **information need** (see `references/design_specs.md` and the per-type table in `references/diagram_types.md`) — S2 Categorical when 2–4 classes / branches / roles need hue, S3 Semantic for fixed component types (cloud / network), S4 Duotone for one focal element, S1 Monochrome Blue only as the fallback for pure layering with no categorical role. Do **not** reflex-default to S1 — it makes every diagram blue. All schemes pre-verified (accent ≤12, no luminance clash). Op cards stay `fill="white"`; color lives in layer fills + borders only. Background defaults to white (`SVGDrawer(bg="#FFFFFF")`); only use `set_background()` for dark themes.
- **Color & typography**: see `references/design_specs.md`.
- **Shapes & layout per type**: when the user names a diagram type (architecture / flowchart / ML model / ER / sequence / swimlane / network), apply the matching preset in `references/diagram_types.md` — it maps each semantic role to a primitive (`rect` / `database` / `decision` / `hexagon` / `component` / `cloud`) and gives direction + spacing defaults that pass the evaluator.
- **Stability**: prefer relative layout logic (i.e. compute new component positions based on known component coordinates).
- **Entity escaping**: SVG is strict XML — never use HTML entities in text (`&middot;`/`&mdash;` etc. are rejected by the parser). Use Unicode characters (`·` `—`) or `html.escape` instead.

## Example: Generation with Evaluation

```python
import sys
# Resolve the skill scripts dir relative to this file (see $SKILL note above).
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "scripts"))
from svg_utils import SVGDrawer, save_svg
from evaluator import evaluate_svg
from pathlib import Path
# Write outputs next to this script (see "Output Layout Convention" below).
OUT = Path(__file__).resolve().parent

drawer = SVGDrawer(1200, 800, bg="#FFFFFF")   # white background is the default; change only for dark themes
drawer.arrow_head("arrowhead", "#333")

# Nodes (Scheme S1 Monochrome Blue — L1 tier; see design_specs.md for the full library)
drawer.rect(100, 100, 90, 34, fill="#D5E1EB", stroke="#1B3A5C",
            node_id="a", node_kind="op", bbox=False)
drawer.rect(300, 100, 90, 34, fill="#D5E1EB", stroke="#1B3A5C",
            node_id="b", node_kind="op", bbox=False)

# Edges: endpoints auto-snap to node borders, arrows land precisely on the edge
drawer.connect("a", "right", "b", "left",
               stroke="#1B3A5C", marker_end="arrowhead", edge_label="value")

# Evaluation (includes connection/arrow validation)
score, report = evaluate_svg(drawer)
print(f"Quality Score: {score}")
for line in report:
    print(line)

if score >= 80:
    save_svg(drawer.render(), str(OUT / "diagram.svg"))
else:
    print("Score too low, need adjustment.")
```

## Output Layout Convention

Every diagram generation is **self-contained in its own subdirectory** under `output/`. One diagram = one directory; the generator script and its SVG/PNG/PPTX triplet live together.

```
output/<timestamp>_<name>/
- gen_<name>.py        # generator script (version-controlled source)
- <name>.svg           # -
- <name>.png           #  |- triplet, regenerated in place on each run
- <name>.pptx          # -
```

**Rules:**
- **Script and outputs are co-located.** The `gen_*.py` lives *inside* its `output/<ts>_<name>/` directory — never in the repo root, never scattered away from its artifacts. Deleting the directory removes script + outputs together.
- **Write to the script's own directory, not a fresh timestamped dir per run** — re-running refreshes the triplet in place rather than accumulating duplicate directories:
  ```python
  from pathlib import Path
  OUT = Path(__file__).resolve().parent   # this script's own directory
  NAME = "<name>"
  ```
- **The `<timestamp>_<name>` directory name is frozen at creation** (a born-on date); it is not regenerated on each run.
- **Always emit the full triplet** — SVG (`save_svg`), PNG (`rasterize_svg`, wraps `rsvg-convert`), and PPTX (`svg2pptx.svg_to_pptx`) — so the directory is self-describing.
- **Artifacts are gitignored by extension** (`**/*.svg`, `**/*.png`, `**/*.pptx`); the `gen_*.py` scripts stay version-controlled. Never gitignore the whole output directory — that hides the scripts.

### The save/rasterize helpers

`save_svg()`, `rasterize_svg()`, and `svg2pptx.svg_to_pptx()` write wherever you ask — they create parent directories as needed and impose no layout constraint. A prior version enforced an `output/<task>/` directory at the library boundary (`validate_output_path` / `OutputPathError`); that was removed for the public release because it refused legitimate cross-project and 
