| name | architecture-drawer |
| description | Use when asked to draw system architecture diagrams, generate technical architecture SVGs, or export architecture diagrams to editable PowerPoint presentations. Supports multi-layer diagrams with automatic layout validation and scoring (13-dimension evaluator catches collisions, overlaps, dangles, crossings, palette issues). |
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):
import os, sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_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
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"];
}
-
Content Parsing & Planning:
- Identify layers, components, and flow direction.
- Determine canvas dimensions (default 1200x800).
-
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.
-
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.
-
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.
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,h0.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.35fs 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 (
·/— etc. are rejected by the parser). Use Unicode characters (· —) or html.escape instead.
Example: Generation with Evaluation
import sys
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
OUT = Path(__file__).resolve().parent
drawer = SVGDrawer(1200, 800, bg="#FFFFFF")
drawer.arrow_head("arrowhead", "#333")
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)
drawer.connect("a", "right", "b", "left",
stroke="#1B3A5C", marker_end="arrowhead", edge_label="value")
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:
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 temporary paths.
from svg_utils import save_svg, rasterize_svg
save_svg(content, OUT / "diagram.svg")
rasterize_svg(OUT / "diagram.svg", OUT / "diagram.png", width=1200)
save_svg(content, filename) — writes SVG content to filename, creating parents; returns the resolved path.
rasterize_svg(svg_path, png_path, width) — runs rsvg-convert -w <width>; creates parents; returns the PNG path.
svg2pptx.svg_to_pptx(svg, pptx_path, config=None) — converts to PPTX; creates parents.
Prefer these wrappers over a raw subprocess.run(["rsvg-convert", ...]) so path handling stays uniform.
SVG to PPTX Export (svg2pptx)
After generating an SVG, you can export it to an editable PowerPoint file with one call. The module $SKILL/svg2pptx.py parses SVG elements into native PowerPoint shapes (rectangles, ovals, connectors, text boxes, freeforms) rather than embedding an image — so each element can be individually resized, recolored, and edited in PowerPoint/Keynote/LibreOffice. Inspired by the svg2pptx project.
Two Export Modes
| Mode | Parameter | Effect | Use Case |
|---|
| shapes (default) | mode="shapes" | Each element → an independent editable shape | Architecture diagrams you want to fine-tune in PPT |
| image | mode="image" | Rasterize to PNG and embed (100% visual fidelity) | Complex SVGs for display only, no editing needed |
API
from svg2pptx import svg_to_pptx, PptxConfig, save_pptx
svg_to_pptx(drawer.render(), OUT / "diagram.pptx")
svg_to_pptx(OUT / "diagram.svg", OUT / "diagram.pptx")
save_pptx(drawer, OUT / "diagram.pptx")
svg_to_pptx(OUT / "diagram.svg", OUT / "diagram.pptx",
config=PptxConfig(slide_w=13.333, slide_h=7.5, scale=2.0))
svg_to_pptx(OUT / "diagram.svg", OUT / "diagram.pptx", config=PptxConfig(mode="image"))
from svg2pptx import add_svg_to_slide
add_svg_to_slide(drawer.render(), slide, x=1.0, y=0.5, scale=0.8)
SVG → PPTX Element Mapping
| SVG Element | PPTX Shape | Notes |
|---|
<rect rx=0> | Rectangle | Auto shape; rotates correctly inside a rotated <g> |
<rect rx>0> | Rounded Rectangle | Corner radius auto-mapped; rotation also handled |
<circle> / <ellipse> | Oval | Ellipse/circle; rotation handled correctly |
<line> | Connector (Straight) | Straight-line connector |
<polygon> | Freeform (closed) | Polygon → freeform |
<polyline> | Freeform (open) | Polyline → freeform |
<path> | Freeform | Bezier/Arc auto-flattened to line segments (curve_tolerance controls precision) |
<text> / <tspan> | Text Box | Preserves font/size/color/alignment, CJK works; <tspan> child text is auto-concatenated |
<g transform> | Coordinate transform | Accumulated affine matrix (translate/scale/rotate) applied to all child shapes |
marker-end | Freeform triangle | Arrow auto-rendered: draws a triangle at the segment endpoint based on marker geometry |
fill-opacity | Transparency | Implemented via <a:alpha> XML injection (python-pptx does not support this natively) |
stroke-dasharray | Dashed line | prstDash or custDash XML injection |
Limitations
- Gradients are not supported (the first color is used).
- Filters/effects (blur, shadow) are not supported (shape shadows are disabled by default).
- Bezier curves are flattened to line segments (lower
curve_tolerance = smoother, default 1.0px).
- Font scaling: font size scales proportionally with the fit-to-slide
scale (Pt(fs * scale * 72/96)) — i.e. a large canvas mapped to a small slide shrinks text, and vice versa. The text-to-box ratio always stays consistent. To fix the font size, set scale=1.0 and adjust slide_w/slide_h yourself.
add_svg_to_slide only supports shapes mode (no image rasterization); for image mode use svg_to_pptx.
- Image mode requires
rsvg-convert (installed on this system); shapes mode only requires python-pptx.
Capabilities
All detection/export capabilities parse the actually-rendered SVG (evaluate, don't assert), so they work equally well for raw add_element drawing.
| # | Capability | Description |
|---|
| ① | Containment-aware collision | Parent-child nesting does not count as a collision |
| ② | Phantom anchor detection | Nodes referenced by edges but invisible → FAIL |
| ③ | Edge × edge crossing | Two edge segments intersecting internally |
| ④ | Marker depth auto-derivation | (markerWidth−refX)×stroke |
| ⑤ | Font-size tier detection | Parses SVG, ≤4 tiers, adjacent tiers ≥1.15× apart |
| ⑥ | Palette detection | Accent ≤8/12, background defaults to light |
| ⑦ | Curve bezier/arc sampling | Ported from fireworks path_routes |
| ⑧ | Luminance conflict by channel | Fill/stroke checked separately for dark+light coexistence |
| ⑨ | Transform matrix accumulation | group() context |
| ⑩ | data-graph-role semantic roles | decoration/legend/background skip business checks |
| ⑪ | Coverage bbox union | Sweep-line algorithm |
| ⑫ | Text overflow detection | Parses <text> geometry |
| ⑬ | Composition quality budget | bend≤2/stretch≤1.35/gutter≥20/segment≥16 + text as obstacle |
| ⑭ | Node shape library | database/decision/hexagon/component/cloud |
| ⑮ | Barycenter crossing minimization | Ported from DiagramForge: reorder by barycenter within layers |
| ⑯ | auto_refine auto-correction | Reads eval report, iteratively fixes gutter/spacing by issue code |
| ⑰ | SVG→PPTX export | Native editable shapes + rasterized image dual modes, arrow rendering, Bezier/Arc flattening, transparency/dash injection |
| ⑱ | Text-vs-shape & text-vs-text overlap | Parses rendered SVG: <text> bbox vs visible circles/rects/polygons/lines/paths + text vs text; closes the bbox-registry blind spot (bbox=False/add_element) |
| ⑲ | Formula rendering (sub/superscript) | drawer.formula() emits real <tspan> baseline shifts for _{}/^{} markup; evaluator strips markup in width estimates and counts only <text>-tier font sizes so subscripts don't inflate the tier budget |
References & Acknowledgments
This Skill's geometry/connection detection draws on the following open-source projects (their references and validator implementations were actually studied):
- ink-graph (
qaz1230sp/ink-graph): its references/pitfalls.md #2 (arrow occluded by node → retract endpoint 8px), #3/#10 (edge crossing through node → 20px gap bypass), #17 (fan-out alignment), #26 (marker size proportional to stroke), #29 (fan-out/fan-in + junction dot); its references/shapes.md dominant-baseline="central" centering, its references/layout-rules.md grid/spacing rules. Measured each of its style-*.md at exactly 3-4 font tiers, 4-13 palette colors — empirical basis for the font-tier/palette thresholds. (These files live in the ink-graph repo, not this skill.)
- fireworks-tech-graph (
yizhiyanhua-ai/fireworks-tech-graph): its references/composition-quality-contract.md (executable budget: zero crossings/≤2 bends/≥40px node spacing/≥20px container gutter); its scripts/validate_svg.py find_collisions + segment_hits_bounds (path sampling vs node bbox), data-graph-role semantic roles, transform matrix accumulation, "evaluate, don't assert" (parse actual SVG rather than trusting API calls). (Files live in the fireworks repo, not this skill.)
- svg-animations (
supermemoryai/skills): SMIL/CSS animation basics and stroke-dasharray stroke animation recipes (this Skill does not enable animation yet, reserved for later).
- svg-design (
tryopendata/skills): primitive-first (circles use <circle>), stroke-linecap="round", strict XML with no HTML entities, and other hygiene conventions.
- svg2pptx (
benouinirachid/svg2pptx): architectural blueprint for the PPTX export module. Its "SVG element → PowerPoint native editable shape" philosophy (rect→rectangle, circle→oval, line→connector, path→freeform, text→textbox), Config dataclass design, build_freeform + add_line_segments usage, and Bezier flattening tolerance parameter were all adapted into the self-contained scripts/svg2pptx.py module (which adds arrow marker rendering, fill-opacity transparency injection, stroke-dasharray dash injection, and an image rasterization fallback mode).