A 2D PCB viewer for `.kicad_pcb` files, rendered as SVG inside a Hydrogen
webview panel. For when you want to **see** a board without launching
KiCad on the user's desktop.

![BMA400 breakout — front view with copper, silk, and edge cuts](apps/kicad-pcb-viewer/hero.png)

## Why this exists

Adom already has 3D viewers (`.glb` chip models) and gerber viewers
(post-fab artwork). What was missing was the in-between: the editable
KiCad board, viewed flat, with toggleable layers, in the cloud editor.

This app is deliberately 2D — for 3D inspection, use the existing 3D
viewers. Use this when:

- The user pasted a `.kicad_pcb` and you want to confirm it looks sane
  before doing anything else with it.
- You're debugging a board layout and want to see "did the autorouter
  stack everything on F.Cu? Where are the vias?"
- You want to take a layer-isolated screenshot (e.g. board outline only)
  to drop into a doc, a PR, or a chat message.
- You want to verify silkscreen orientation, or check what's actually
  on the back side of a board, without firing up pcbnew.

## Install

```bash
gh release download v0.1.0 --repo adom-inc/kicad-pcb-viewer \
    --pattern kicad-pcb-viewer-linux \
    -D /tmp
sudo install -m 0755 /tmp/kicad-pcb-viewer-linux /usr/local/bin/kicad-pcb-viewer
kicad-pcb-viewer install   # deploys SKILL.md to ~/.claude/skills/kicad-pcb-viewer/
```

Runs on any Python 3.10+ system. No external dependencies — just stdlib.

## Quick start (human-driven)

```bash
kicad-pcb-viewer view path/to/board.kicad_pcb
```

Opens a Hydrogen webview tab in a non-VSCode pane (per `app-creator`
skill — never inside the editor pane).

| Action | Input |
|---|---|
| Pan | Click + drag |
| Zoom | Scroll wheel (toward cursor) |
| Reset to fit | `Fit` button |
| Mirror horizontally | `Flip` button |
| Toggle layer | Click a row in the sidebar |

Coordinate readout (mm) and zoom % live in the bottom-right HUD.

## CLI subcommands

| Command | What it does |
|---|---|
| `view <path> [--port N]` | Start server + open webview tab. Default. |
| `serve <path> [--port N]` | Start server only (no tab). Useful for headless scripting. |
| `shutdown [--port N]` | Stop a running viewer (graceful). |
| `install` | Deploy `SKILL.md` to `~/.claude/skills/kicad-pcb-viewer/`. |
| `version` | Print version. |

Default port: `9100`. The first positional arg is auto-inferred as `view`
if it ends in `.kicad_pcb` or is an existing path, so
`kicad-pcb-viewer board.kicad_pcb` works as shorthand.

## Multi-layer boards

Inner copper layers (`In1.Cu`, `In2.Cu`, `In3.Cu`, `In4.Cu`) get distinct
colors and z-order. The layer panel only lists layers that **actually
have geometry on this board** — empty layers stay hidden, so the panel
doesn't fill up with 30 unused entries.

![BMI270 — 4-layer board with copper pours, inner layers, vias](apps/kicad-pcb-viewer/multilayer.png)

## HTTP API — every action is AI-drivable

Per `app-creator` §7, the same endpoints the UI uses are reachable via
HTTP. This is the property that turns a webview into an AI surface.

| Method | Path | Description |
|---|---|---|
| GET | `/state` | Phase, file path, filename, stats, bbox, layer visibility. |
| GET | `/board` | Full parsed board JSON (footprints, pads, segments, arcs, vias, zones, gr). |
| GET | `/layers` | `{name: {idx, kind, alias, visible}}` for every layer on the board. |
| POST | `/layers` | Body `{name: bool, ...}` — set visibility for one or more layers. |
| POST | `/load` | Body `{path: "..."}` — open a different `.kicad_pcb` without restarting the server. |
| POST | `/fit` | Reset view to fit the board. |
| POST | `/flip` | Toggle horizontal mirror (B-side viewing). Returns `{flipped: bool}`. |
| GET | `/console` | Last 500 forwarded UI-side console messages. |
| POST | `/console` | Append a message (called from the UI). |
| DELETE | `/console` | Clear log. |
| POST | `/eval` | Body `{code: "..."}` — queue a JS snippet, returns `{id}`. UI runs it on next poll. |
| GET | `/eval/pending` | UI poller endpoint — returns `{id, code}` or empty. |
| GET | `/eval/<id>` | Read result of a prior `/eval` post. |
| POST | `/eval/<id>/result` | UI POSTs result back here (internal). |
| POST | `/shutdown` | Graceful exit. |
| GET | `/cmds/pending` | UI poller for fit/flip commands queued by `/fit` and `/flip`. |
| GET | `/` | The UI HTML. |
| GET | `/favicon.svg` | Brand icon. |

## AI recipes

Self-contained scripts an AI can copy-paste. All assume the viewer is
running on port `9100` against some board.

### Recipe 1 — "is this board mostly empty?"

Quick stats check before doing anything else.

```bash
kicad-pcb-viewer serve board.kicad_pcb --port 9100 &
sleep 1
curl -s http://127.0.0.1:9100/state | jq '.stats, .bbox'
# {"footprints":13,"pads":31,"segments":61,"arcs":0,"vias":6,"zones":0,"gr":16}
# {"x":18.8,"y":18.8,"w":10.4,"h":10.4}
curl -s -X POST http://127.0.0.1:9100/shutdown >/dev/null
```

### Recipe 2 — "isolate Edge.Cuts and screenshot"

For a board-outline export. Hide every layer except the edge.

```bash
curl -X POST http://127.0.0.1:9100/layers \
    -H 'Content-Type: application/json' \
    -d '{
      "F.Cu":false,"B.Cu":false,
      "In1.Cu":false,"In2.Cu":false,"In3.Cu":false,"In4.Cu":false,
      "F.SilkS":false,"B.SilkS":false,
      "F.Mask":false,"B.Mask":false,
      "F.Paste":false,"B.Paste":false,
      "F.Fab":false,"B.Fab":false,
      "F.CrtYd":false,"B.CrtYd":false,
      "Edge.Cuts":true
    }'
adom-cli hydrogen screenshot panel --name "PCB: <filename>"
```

### Recipe 3 — "show only the back side"

Hide front-side layers, flip the view, screenshot.

```bash
curl -X POST http://127.0.0.1:9100/layers -H 'Content-Type: application/json' \
    -d '{"F.Cu":false,"F.SilkS":false,"F.Mask":false,"F.Paste":false,"F.Fab":false,"F.CrtYd":false,
         "B.Cu":true,"B.SilkS":true,"Edge.Cuts":true}'
curl -X POST http://127.0.0.1:9100/flip
adom-cli hydrogen screenshot panel --name "PCB: <filename>"
```

### Recipe 4 — "list every footprint and its placement"

Useful for cross-referencing with a BOM or schematic.

```bash
curl -s http://127.0.0.1:9100/board | \
    jq '.footprints[] | {refdes, value, x, y, rot, layer}'
# {"refdes":"U1","value":"BMA400","x":24.0,"y":24.0,"rot":0,"layer":"F.Cu"}
# ...
```

### Recipe 5 — "diff two boards by stats"

```bash
for f in v1.kicad_pcb v2.kicad_pcb; do
    curl -s -X POST http://127.0.0.1:9100/load \
        -H 'Content-Type: application/json' -d "{\"path\":\"$PWD/$f\"}" >/dev/null
    echo "=== $f ==="
    curl -s http://127.0.0.1:9100/state | jq '.stats'
done
```

### Recipe 6 — "swap to a new board without restarting"

The same server can host any board. POST `/load` to swap without
opening a new tab.

```bash
curl -X POST http://127.0.0.1:9100/load \
    -H 'Content-Type: application/json' \
    -d '{"path":"/home/adom/project/foo/foo.kicad_pcb"}'
adom-cli hydrogen webview refresh --name "PCB: <old-filename>"
```

(The tab name keeps the original board's filename — a future version can
update the title on `/load`.)

### Recipe 7 — "inspect the rendered DOM via /eval"

Need to know what's actually visible? Eval a snippet inside the page.

```bash
curl -X POST http://127.0.0.1:9100/eval \
    -H 'Content-Type: application/json' \
    -d '{"code":"return Array.from(document.querySelectorAll(\".layer-group\")).map(g => ({layer: g.dataset.layer, hidden: g.classList.contains(\"hidden\"), elements: g.children.length}))"}'
# {"id":"a1b2c3"}

# Read result via /console (the UI logs results)
curl -s http://127.0.0.1:9100/console | jq '.messages[-1]'
```

### Recipe 8 — "headless render-to-screenshot pipeline"

Combine `serve`, layer toggles, and `adom-cli hydrogen screenshot` to
build per-layer images for a doc:

```bash
kicad-pcb-viewer view board.kicad_pcb --port 9100 &
sleep 2
for layer in F.Cu B.Cu F.SilkS Edge.Cuts; do
    # Hide everything except this layer + Edge.Cuts (always nice to see outline)
    body=$(jq -nc --arg L "$layer" '
        {"F.Cu":false,"B.Cu":false,"In1.Cu":false,"In2.Cu":false,
         "F.SilkS":false,"B.SilkS":false,"F.Mask":false,"B.Mask":false,
         "F.Paste":false,"B.Paste":false,"F.Fab":false,"B.Fab":false,
         "F.CrtYd":false,"B.CrtYd":false,"Edge.Cuts":true} |
        .[$L] = true')
    curl -s -X POST http://127.0.0.1:9100/layers -H 'Content-Type: application/json' -d "$body" >/dev/null
    sleep 0.5
    adom-cli hydrogen screenshot panel --name "PCB: $(basename board.kicad_pcb)"
done
```

## What it parses

- `(footprint …)` — placement (`at`), rotation, child pads + graphics
- `(pad …)` — `rect`, `roundrect`, `circle`, `oval`, with drill (round or
  oval slot), through-hole and SMD
- `(segment …)`, `(arc …)` — copper traces with width
- `(via …)` — size + drill
- `(zone …)` — filled polygons (post-fill state preferred, falls back to
  outline if `filled_polygon` not present)
- `(gr_line/arc/circle/rect/poly …)` — top-level graphics on any layer
- `(fp_line/arc/circle/rect/poly …)` — footprint-level graphics (silk,
  fab, courtyard)
- `(layers …)` — layer table with index, kind, alias
- Pad-side `(layers …)` lists with `*.Cu` / `*.Mask` / `*.Paste` wildcards
  expanded to the matching concrete layers

## What it doesn't (yet)

- Schematic (`.kicad_sch`) rendering — different format, different scope.
- Net highlighting / cross-probing.
- File watching — call `POST /load` to reload after edits.
- Refdes / value labels overlaid on the board (too cluttered at default
  zoom; could be added as a togglable text overlay layer).
- Bezier curves on `gr_curve` — currently approximated as polylines.

## Pane placement (important)

The viewer ALWAYS opens in a pane that is NOT the VSCode pane. If a
non-VSCode pane already exists, the tab joins it; otherwise the VSCode
pane is split vertically and the tab lands in the new pane.

This rule lives in `skills/app-creator` §4 — it applies to every Adom
app, not just this one. Putting webviews as sibling tabs inside the
VSCode pane covers the editor and is wrong.

## Files in this repo

```
bin/kicad-pcb-viewer    # Python CLI (subcommands: view, serve, install, shutdown, version)
src/parser.py           # s-expression tokenizer + per-layer extractor
src/server.py           # HTTP server, all endpoints, asset loading (source + zipapp)
src/ui.html             # SVG renderer, pan/zoom, layer panel, console fwd, eval poller
docs/icon.svg           # Brand-teal favicon (also the tab icon)
docs/SKILL.md           # AI usage doc deployed by `kicad-pcb-viewer install`
samples/                # BMA400 + BMI270 reference boards (from adom-inc/bosch-molecules)
scripts/build.sh        # Builds dist/kicad-pcb-viewer-linux zipapp (~22 KB)
```

## Repo

[`adom-inc/kicad-pcb-viewer`](https://github.com/adom-inc/kicad-pcb-viewer) — private. Releases attach a single-file
zipapp (`kicad-pcb-viewer-linux`).
