$npx -y skills add neuromechanist/research-skills --skill svg-primitivesUse when the user asks to build a programmatic SVG schematic in Python: flowcharts, boxes and arrows, auto-fit SVG text, mm-precise diagrams, edge-snapped arrows, tangent-correct curve arrowheads, orthogonal or Manhattan routing, deterministic z-order layers, labeled groups, brac
| 1 | # SVG Primitives |
| 2 | |
| 3 | Build mm-precise SVG schematics in Python with three mechanical guarantees: |
| 4 | |
| 5 | 1. **Text never overflows its container** — labeled shapes auto-size to fit measured text bbox + padding. |
| 6 | 2. **Arrowheads stay tangent-correct** — arrows emit `<marker orient="auto">` so the renderer rotates the head along the path's terminal tangent; works on straight lines and cubic Beziers. |
| 7 | 3. **Paint order is deterministic** — layers paint in registration order; connectors visibly pass under boxes without manual reordering. |
| 8 | |
| 9 | The skill ships an end-to-end pytest suite (50+ tests) that renders SVGs and asserts these invariants on the rendered output, so the guarantees are enforced by construction rather than by hand-checking each figure. |
| 10 | |
| 11 | ## When to use this skill |
| 12 | |
| 13 | Reach for `svg-primitives` when: |
| 14 | |
| 15 | - The figure is a **schematic** (boxes, arrows, labels) and you're driving it from Python — e.g. nodes come from a YAML config, or the layout depends on data. |
| 16 | - You need the boxes to **auto-fit** their labels (no hand-tuning widths). |
| 17 | - The figure has **curved arrows** that must point cleanly at their targets. |
| 18 | - You want **deterministic z-order** so connectors sit under shapes without manual element reordering. |
| 19 | - The output will be **composed into a multi-panel figure** as a panel SVG that `scientific-figure/compose.py` loads. |
| 20 | |
| 21 | Reach for a different tool when: |
| 22 | |
| 23 | - The figure is **plotted from numbers** (matplotlib/seaborn/plotnine) → use `[[plot-styling]]`. |
| 24 | - The figure is a **photographic / pictorial substrate** (a brain scene, microscope setup) → use `[[ai-full-figure]]` for the substrate and overlay labels via Arrow/LabeledBox here. |
| 25 | - The figure is **hand-authored SVG** or the patterns are reference material for hand-authoring → use `[[svg-figure]]` (this skill's library-agnostic counterpart). |
| 26 | |
| 27 | ## Quick start |
| 28 | |
| 29 | ```python |
| 30 | from svg_primitives import Canvas, LabeledBox, Arrow |
| 31 | |
| 32 | c = Canvas(width_mm=183, height_mm=80) |
| 33 | boxes = c.layer("boxes") |
| 34 | arrows = c.layer("connectors") |
| 35 | |
| 36 | raw = boxes.add(LabeledBox(x=10, y=20, text="Raw EEG", font_size=7)) |
| 37 | band = boxes.add(LabeledBox.next_to(raw, side="E", gap=10, text="Bandpass\nfilter", font_size=7)) |
| 38 | ica = boxes.add(LabeledBox.next_to(band, side="E", gap=10, text="Independent component\nanalysis", font_size=7)) |
| 39 | |
| 40 | arrows.add(Arrow.connect(raw, band)) # straight, snapped to edges |
| 41 | arrows.add(Arrow.connect(band, ica)) # straight |
| 42 | arrows.add(Arrow.connect(ica, raw, curve="cubic", bow=14, # feedback arc |
| 43 | stroke="#C45146")) # red — gets its own red marker |
| 44 | |
| 45 | c.save("eeg.svg", output_png=True) |
| 46 | ``` |
| 47 | |
| 48 | `examples/eeg_pipeline.py` is the canonical reference; run it to see the full output: |
| 49 | |
| 50 | ```bash |
| 51 | uv run --with drawsvg --with svgpathtools --with Pillow --with fonttools --with cairosvg \ |
| 52 | python plugins/figures/skills/svg-primitives/examples/eeg_pipeline.py |
| 53 | ``` |
| 54 | |
| 55 | ## Primitive reference |
| 56 | |
| 57 | ### `Canvas(width_mm, height_mm, background=None)` |
| 58 | |
| 59 | The root drawing surface. User units equal mm; the `viewBox` is set so coordinates inside the SVG are in mm and font-size is emitted in mm regardless of the input pt size. |
| 60 | |
| 61 | - `.layer(name) -> Layer` — gets or creates a named layer. Subsequent `.layer("boxes")` calls return the same layer. |
| 62 | - `.add_layer(Layer) -> Canvas` — explicit insertion when registration order matters. |
| 63 | - `.save(path, output_png=False, png_width=1800)` — writes the SVG; with `output_png=True`, also writes a sibling PNG via cairosvg. |
| 64 | |
| 65 | Layer paint order is the order layers were first registered. Typical convention: `background` → `connectors` → `boxes` → `labels`. |
| 66 | |
| 67 | ### `Layer(name)` |
| 68 | |
| 69 | An ordered bucket of elements. `.add(element)` appends and returns the element so calls chain. |
| 70 | |
| 71 | ### `LabeledBox(x, y, text, font_size=7, padding=2, ...)` |
| 72 | |
| 73 | Auto-sized rounded rectangle with centered text. The width and height are computed from the measured text bbox (via fontTools) plus `padding` on every side, clamped by `min_width` / `min_height`. Multi-line text (newline-separated) stacks with `line_spacing` em between baselines. |
| 74 | |
| 75 | - `anchor="top-left"` (default) — `x, y` is the top-left corner. |
| 76 | - `anchor="center"` — `x, y` is the centroid. |
| 77 | - `.anchor_point("N"|"S"|"E"|"W") -> complex` — exposed for `Arrow.connect`. |
| 78 | - `.outline_path() -> svgpathtools.Path` — used internally for arrow edge snapping. |
| 79 | - `.next_to(other |