skill
EDA Bridge Standard: one way for an AI to drive every PCB tool
Public Made by Adomby adom
The standard every Adom EDA bridge follows so one AI can drive Altium, KiCad, Fusion 360 and any future PCB tool the same way: shared verb names in tiers, the board model JSON, reply and refusal conventions, and a coverage table of each bridge against the Altium reference.
main
The board model
One PCB, the same shape from every EDA tool. Every check in the standard reads it; every bridge returns it from
<prefix>_routing_state. The JSON Schema is schema/board.schema.json; the companion models (stack-up, poured copper,
3D bodies, sheets, netlist) are schema/companions.schema.json.
The reference is the Altium bridge. AI Flow reads the same shape from KiCad and Fusion, which is why it uses KiCad's layer names.
In one picture
{
"units": "mm", "boardId": "C:/proj/Board.PcbDoc", "revision": "476053b2ec164071",
"nets": ["GND", "N1"], "unconnected": 0,
"tracks": [{"start": {"x": 12.7, "y": 11.905}, "end": {"x": 26.3, "y": 11.905}, "width": 0.3, "layer": "F.Cu", "net": "N1"}],
"arcs": [{"center": {"x": 5, "y": 5}, "radius": 1, "startAngle": 0, "endAngle": 90, "width": 0.25, "layer": "F.Cu", "net": "N1"}],
"vias": [{"position": {"x": 7.5, "y": 2.5}, "size": 0.6, "drill": 0.3, "layers": ["F.Cu", "B.Cu"], "net": "GND"}],
"pads": [{"ref": "U1", "name": "8", "x": 3.3, "y": 11.905, "sizeX": 1.5, "sizeY": 0.6, "rotation": 0, "layer": "F.Cu", "drill": 0, "net": "N1", "shape": "rect"}],
"components": [{"ref": "U1", "x": 6, "y": 10, "rotation": 0, "side": "top", "pattern": "SOIC8", "has3d": true}],
"polygons": [{"net": "GND", "layer": "B.Cu", "points": [[0.5, 0.5], [20, 0.5], [20, 19.5], [0.5, 19.5]]}],
"outline": [[1, 0], [39, 0], [40, 1], [40, 19], [39, 20], [1, 20], [0, 19], [0, 1]],
"outlineEdges": [{"kind": "line", "start": [1, 0], "end": [39, 0]}, {"kind": "arc", "start": [39, 0], "end": [40, 1], "center": [39, 1], "radius": 1, "angle1": 270, "angle2": 360}]
}
Rules a bridge must keep
- Millimetres, absolute, in the standard frame: the tool's origin and x axis, y up. A tool whose y points down
(KiCad) negates y and nothing else: same origin, same x, and angles stay counterclockwise as seen on screen, so the
numbers need no change. Altium and Fusion are y up already. No offset from the board origin: the checks compare
against the outline the same reply gives. A bridge whose existing callers read its native frame may keep that as its
default and answer the standard model on request (KiCad:
model: "standard"), as long as its edit verbs take the standard model's coordinates back (KiCad:frame: "standard"). - Copper layers by KiCad's names (
F.Cu,In<n>.Cu,B.Cu,*.Cu). Non-copper layers may use the tool's own name or id (Altium sendsL57for Mechanical 1). - Pad shapes:
round(a circle whensizeX=sizeY, otherwise a stadium with semicircle ends on the long axis; a tool's oval or obround isround),circle,rect,roundrect,octagonal,rotated-rect,other. - A custom-shape pad is its bounding rectangle (
shape: "rect", rotation 0), with its real outline incopperPointswhen the bridge can read it (Fusion can). Checks usecopperPointswhen present. Altium's API gives such a pad no size; reported as 0 x 0, every clearance check walked straight through the Bluetooth Sentinel's 5 mm coin cell contacts. - Net-less copper is reported with an empty net, not dropped: a hatched pour's pieces arrive that way from Altium, and the copper checks count them.
unconnectedis computed from the copper, not taken from the tool's ratsnest (Altium's ratsnest skips parts outside the board; Fusion's draws airwires to track ends 5 to 7.5 um off a pad's anchor but over its copper). Per net, pads, tracks, vias and poured copper join where they touch on a shared layer; every group beyond the first is a connection still to make. The tool's own count may ride along under its own key (altiumRatsnest,fusionAirwires).- Poured copper that reaches nothing else of its net is
isolatedCopper, listed and not counted: an island a pour left behind, or a pour that touches no pad, track or via of its net. KiCad's DRC treats it the same way (a warning, not an unconnected item). A stray track or via still counts: copper that reaches nothing is a connection not made. tracks[].idis the handleedit_trackstakes. It's optional in the model, and a bridge that answersedit_tracksgives one. It can be the tool's own id or one built from the track's position (Fusion's tracks have no id), so it need only hold until the board changes. It never counts towardrevision.revisionchanges when copper, parts or the outline change, and only then: it is the edit verbs' guard. It must be stable for an unchanged board; independent of item order is better (KiCad's is).- The outline as edges, arcs kept. Measuring against a curved edge needs the arc; sample it every 2 degrees (16 points per arc put copper 0.04 mm outside the Sentinel's round edge).
polygonsis the pour as drawn,kind: "zone"(the default) or"graphic"for a filled copper shape. The copper actually poured is the pours model (pour_state): checks that need real copper read that.
The companion models
| Model | Verb | What reads it |
|---|---|---|
| Stack-up | layer_stack |
impedance, current capacity, fab check (copper weight, thickness), copper balance, return paths (planes). Each copper layer carries its board model name in layer (F.Cu, In<n>.Cu, B.Cu); an internal plane is In<n>.Cu by its place in the copper stack, with kind: "plane", and counts as copper everywhere but its splits and antipads |
| Poured copper | pour_state |
copper balance, return paths and their fix plans. Island by island, outline and holes, filled by the even-odd rule: a fractured fill (KiCad: holes joined to the outline by zero-width cuts, holes: [], fractured: true) is the same copper and needs no separating |
| 3D bodies | bodies (Altium: an agent call behind height_check) |
height check, placement check. One box per body (Altium) or per part (Fusion); z from the surface the part sits on |
| Sheet parts with pin types | read_sheet_file (Altium reads the .SchDoc file itself) |
schematic review, pin-to-pad check. Pin types by name (input, io, output, power, supply, not_connected...); Altium's codes are accepted too |
| Netlist | netlist |
schematic review |
| Silkscreen | silk_state |
designator placement, fab check (text height, line width) |
Settled questions
| Question | Answer | Where |
|---|---|---|
| What does "y up" mean on a y-down tool? | Negate y; keep the origin, x and the angles | #1 |
| Do pour islands count as unconnected? | No: isolatedCopper, listed, not counted. Strays still count |
#1, #2 |
| Must fractured fills be split into outline and holes? | No: even-odd filling gives the same copper | #1 |
| Custom pads? | Bounding rectangle, plus copperPoints when known |
#2 |
| Units in replies? | mm, and a unit-named field says mm (xMm); see conventions |
#2 |
| What is an internal plane called? | In<n>.Cu by its place in the copper stack, kind: "plane" in the stack-up; a tool's own id may ride along (layerId), never in its place |
#8 |
# The board model
One PCB, the same shape from every EDA tool. Every check in the standard reads it; every bridge returns it from
`<prefix>_routing_state`. The JSON Schema is `schema/board.schema.json`; the companion models (stack-up, poured copper,
3D bodies, sheets, netlist) are `schema/companions.schema.json`.
The reference is the Altium bridge. AI Flow reads the same shape from KiCad and Fusion, which is why it uses KiCad's
layer names.
## In one picture
```json
{
"units": "mm", "boardId": "C:/proj/Board.PcbDoc", "revision": "476053b2ec164071",
"nets": ["GND", "N1"], "unconnected": 0,
"tracks": [{"start": {"x": 12.7, "y": 11.905}, "end": {"x": 26.3, "y": 11.905}, "width": 0.3, "layer": "F.Cu", "net": "N1"}],
"arcs": [{"center": {"x": 5, "y": 5}, "radius": 1, "startAngle": 0, "endAngle": 90, "width": 0.25, "layer": "F.Cu", "net": "N1"}],
"vias": [{"position": {"x": 7.5, "y": 2.5}, "size": 0.6, "drill": 0.3, "layers": ["F.Cu", "B.Cu"], "net": "GND"}],
"pads": [{"ref": "U1", "name": "8", "x": 3.3, "y": 11.905, "sizeX": 1.5, "sizeY": 0.6, "rotation": 0, "layer": "F.Cu", "drill": 0, "net": "N1", "shape": "rect"}],
"components": [{"ref": "U1", "x": 6, "y": 10, "rotation": 0, "side": "top", "pattern": "SOIC8", "has3d": true}],
"polygons": [{"net": "GND", "layer": "B.Cu", "points": [[0.5, 0.5], [20, 0.5], [20, 19.5], [0.5, 19.5]]}],
"outline": [[1, 0], [39, 0], [40, 1], [40, 19], [39, 20], [1, 20], [0, 19], [0, 1]],
"outlineEdges": [{"kind": "line", "start": [1, 0], "end": [39, 0]}, {"kind": "arc", "start": [39, 0], "end": [40, 1], "center": [39, 1], "radius": 1, "angle1": 270, "angle2": 360}]
}
```
## Rules a bridge must keep
- **Millimetres, absolute, in the standard frame: the tool's origin and x axis, y up.** A tool whose y points down
(KiCad) negates y and nothing else: same origin, same x, and angles stay counterclockwise as seen on screen, so the
numbers need no change. Altium and Fusion are y up already. No offset from the board origin: the checks compare
against the outline the same reply gives. A bridge whose existing callers read its native frame may keep that as its
default and answer the standard model on request (KiCad: `model: "standard"`), as long as its edit verbs take the
standard model's coordinates back (KiCad: `frame: "standard"`).
- **Copper layers by KiCad's names** (`F.Cu`, `In<n>.Cu`, `B.Cu`, `*.Cu`). Non-copper layers may use the tool's own
name or id (Altium sends `L57` for Mechanical 1).
- **Pad shapes:** `round` (a circle when `sizeX` = `sizeY`, otherwise a stadium with semicircle ends on the long axis;
a tool's oval or obround is `round`), `circle`, `rect`, `roundrect`, `octagonal`, `rotated-rect`, `other`.
- **A custom-shape pad is its bounding rectangle** (`shape: "rect"`, rotation 0), with its real outline in
`copperPoints` when the bridge can read it (Fusion can). Checks use `copperPoints` when present. Altium's API gives
such a pad no size; reported as 0 x 0, every clearance check walked straight through the Bluetooth Sentinel's 5 mm
coin cell contacts.
- **Net-less copper is reported with an empty net**, not dropped: a hatched pour's pieces arrive that way from Altium,
and the copper checks count them.
- **`unconnected` is computed from the copper**, not taken from the tool's ratsnest (Altium's ratsnest skips parts
outside the board; Fusion's draws airwires to track ends 5 to 7.5 um off a pad's anchor but over its copper). Per
net, pads, tracks, vias and poured copper join where they touch on a shared layer; every group beyond the first is a
connection still to make. The tool's own count may ride along under its own key (`altiumRatsnest`,
`fusionAirwires`).
- **Poured copper that reaches nothing else of its net is `isolatedCopper`**, listed and not counted: an island a pour
left behind, or a pour that touches no pad, track or via of its net. KiCad's DRC treats it the same way (a warning,
not an unconnected item). A stray track or via still counts: copper that reaches nothing is a connection not made.
- **`tracks[].id` is the handle `edit_tracks` takes.** It's optional in the model, and a bridge that answers `edit_tracks`
gives one. It can be the tool's own id or one built from the track's position (Fusion's tracks have no id), so it
need only hold until the board changes. It never counts toward `revision`.
- **`revision` changes when copper, parts or the outline change**, and only then: it is the edit verbs' guard. It must
be stable for an unchanged board; independent of item order is better (KiCad's is).
- **The outline as edges, arcs kept.** Measuring against a curved edge needs the arc; sample it every 2 degrees (16
points per arc put copper 0.04 mm outside the Sentinel's round edge).
- **`polygons` is the pour as drawn**, `kind: "zone"` (the default) or `"graphic"` for a filled copper shape. The copper
actually poured is the pours model (`pour_state`): checks that need real copper read that.
## The companion models
| Model | Verb | What reads it |
|---|---|---|
| Stack-up | `layer_stack` | impedance, current capacity, fab check (copper weight, thickness), copper balance, return paths (planes). Each copper layer carries its board model name in `layer` (`F.Cu`, `In<n>.Cu`, `B.Cu`); an internal plane is `In<n>.Cu` by its place in the copper stack, with `kind: "plane"`, and counts as copper everywhere but its splits and antipads |
| Poured copper | `pour_state` | copper balance, return paths and their fix plans. Island by island, outline and holes, filled by the even-odd rule: a fractured fill (KiCad: holes joined to the outline by zero-width cuts, `holes: []`, `fractured: true`) is the same copper and needs no separating |
| 3D bodies | `bodies` (Altium: an agent call behind `height_check`) | height check, placement check. One box per body (Altium) or per part (Fusion); z from the surface the part sits on |
| Sheet parts with pin types | `read_sheet_file` (Altium reads the .SchDoc file itself) | schematic review, pin-to-pad check. Pin types by name (`input`, `io`, `output`, `power`, `supply`, `not_connected`...); Altium's codes are accepted too |
| Netlist | `netlist` | schematic review |
| Silkscreen | `silk_state` | designator placement, fab check (text height, line width) |
## Settled questions
| Question | Answer | Where |
|---|---|---|
| What does "y up" mean on a y-down tool? | Negate y; keep the origin, x and the angles | [#1](https://wiki.adom.inc/adom/eda-bridge-standard/issues/1) |
| Do pour islands count as unconnected? | No: `isolatedCopper`, listed, not counted. Strays still count | [#1](https://wiki.adom.inc/adom/eda-bridge-standard/issues/1), [#2](https://wiki.adom.inc/adom/eda-bridge-standard/issues/2) |
| Must fractured fills be split into outline and holes? | No: even-odd filling gives the same copper | [#1](https://wiki.adom.inc/adom/eda-bridge-standard/issues/1) |
| Custom pads? | Bounding rectangle, plus `copperPoints` when known | [#2](https://wiki.adom.inc/adom/eda-bridge-standard/issues/2) |
| Units in replies? | mm, and a unit-named field says mm (`xMm`); see conventions | [#2](https://wiki.adom.inc/adom/eda-bridge-standard/issues/2) |
| What is an internal plane called? | `In<n>.Cu` by its place in the copper stack, `kind: "plane"` in the stack-up; a tool's own id may ride along (`layerId`), never in its place | [#8](https://wiki.adom.inc/adom/eda-bridge-standard/issues/8) |