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.
← Commit history
standard 0.5.1, engine 0.2.1: GenCAD keeps one shape per footprint pattern and pad geometry (same name, other pads: NAME_2; a rotated part's micron-off pins share their shape) (fusion-lane #17); regenerated with kicad 1.0.69 (length_tune, silkscreen_prep, component_classes, build_board, test_board, read_sheet_file; panelize note): KiCad 190 of 236
8 files changed
+83−31
README.md+6−3@@ -59,11 +59,11 @@ verb by verb: [docs/coverage.md](docs/coverage.md). | Tier | Verbs | Altium | KiCad | Fusion 360 | |---|---|---|---|---| | **core** | 93 | 93 | 89 | 79 |-| **native** | 46 | 46 | 41 | 29 |-| **checks** | 37 | 33 | 29 | 13 |+| **native** | 46 | 46 | 43 | 29 |+| **checks** | 37 | 33 | 33 | 13 | | **app** | 60 | 60 | 25 | 37 | | **platform** | 19 | 19 | 0 | 0 |-| standard (all but platform) | 236 | 232 | 184 | 158 |+| standard (all but platform) | 236 | 232 | 190 | 158 | <!-- coverage:end --> The **checks** tier is where the other bridges are furthest behind, and it is the cheapest to close: those verbs are@@ -156,6 +156,9 @@ Decisions so far (proposals from the KiCad and Fusion bridges, 2026-10-27; issue ## Status +Version 0.5.1: the engine (0.2.1) writes one GenCAD shape per footprint pattern and pad geometry: two different+footprints of the same name get NAME and NAME_2, and a rotated part's micron-off pins still share their shape+(fusion-lane #17). Version 0.5.0: the engine (0.2.0) carries shared board-file writers, GenCAD, DXF per layer and SVG to regions, from altium-lane's writers (#16, PR #1); its DXF draws the copper actually poured when given `pours`. Version 0.4.5: the engine (0.1.4) lists each layer once in `current_capacity`'s `poursOn` (kicad-lane #15); KiCad now
docs/coverage.md+10−10@@ -1,17 +1,17 @@ # Coverage: every standard verb on each bridge -Altium bridge 0.2.78 (the reference), KiCad bridge 1.0.68, Fusion 360 bridge 1.13.1. 54 verbs came from other bridges' proposals (tools/proposed/); they are marked *proposed*.+Altium bridge 0.2.78 (the reference), KiCad bridge 1.0.69, Fusion 360 bridge 1.13.1. 54 verbs came from other bridges' proposals (tools/proposed/); they are marked *proposed*. `exact` is the same job under the same or an equivalent name; `partial` does part of it or does it differently (`verbs/coverage.json` names what in `note`); `unsupported` answers `not_supported` for the whole job and is not counted; blank is missing. Generated by `tools/generate.py` from `verbs/coverage.json`; do not edit by hand. | Tier | Verbs | Altium | KiCad | Fusion 360 | |---|---|---|---|---| | **core** | 93 | 93 | 89 | 79 |-| **native** | 46 | 46 | 41 | 29 |-| **checks** | 37 | 33 | 29 | 13 |+| **native** | 46 | 46 | 43 | 29 |+| **checks** | 37 | 33 | 33 | 13 | | **app** | 60 | 60 | 25 | 37 | | **platform** | 19 | 19 | 0 | 0 |-| standard (all but platform) | 236 | 232 | 184 | 158 |+| standard (all but platform) | 236 | 232 | 190 | 158 | ## core @@ -126,7 +126,7 @@ Wraps the EDA tool's own feature (its DRC, ERC, Gerbers, pours, Update PCB). Eve | `build_symbol_modes` | Build ONE SchLib component with N display modes (Normal + Alternate 1..N-1) from a symbol_modes spec file on this machine, and return a proof frame. | `altium_build_symbol_modes` | | | | `cleanup_libs` | Remove the factory-default stray every fresh library carries: Component_1 from the current SchLib and an empty (no pads) PCBComponent_1 from the current PcbLib. | `altium_cleanup_libs` | | | | `compile` | Compile a project and return Altium's Messages panel (ERC + connectivity) with counts by class. With no path it compiles Altium's bundled example project. | `altium_compile` | `kicad_compile` | |-| `component_classes` | List, make or remove component classes on the board, each optionally with a room: a placement area that keeps its parts together (fitted around the parts or... | `altium_component_classes` | | |+| `component_classes` | List, make or remove component classes on the board, each optionally with a room: a placement area that keeps its parts together (fitted around the parts or... | `altium_component_classes` | `kicad_component_classes` | | | `create_part_project` *proposed* | Generate a small real project (schematic and board) around one library part, so the part can be shown and tested in every view: schematic, 2D board and 3D... | `altium_create_part_project` | `kicad_make_part_project` (partial) | `fusion_make_test_project` (partial) | | `diff_pairs` | Differential pairs on the current board: list them, define one from two nets (replacing a pair of that name), or remove one. Altium's differential-pair... | `altium_diff_pairs` | `kicad_diff_pairs` (partial) | | | `export_bom` | Bill of materials export: altium_run_outjob with the assembly OutJob and the 'Folder Structure' medium as defaults. | `altium_export_bom` | `kicad_export_bom_csv` (partial) | `fusion_export_bom / fusion_board_bom` |@@ -160,7 +160,7 @@ Wraps the EDA tool's own feature (its DRC, ERC, Gerbers, pours, Update PCB). Eve | `run_erc` | Electrical rules check. Same handler as altium_compile: in Altium, compiling the project IS the ERC. | `altium_run_erc` | `kicad_run_erc` | `fusion_run_erc` | | `sch_update_from_library` *proposed* | Update the placed symbols on a sheet from their libraries (all, or the listed ones) and report which changed, which already matched and which no library has. | `altium_sch_update_from_library` | `kicad_sch_update_from_library` | unsupported | | `set_design_rules` | Write a fab's design rules into the current board: clearance, track width, routing via style, hole size, annular ring, hole-to-hole and board-edge clearance... | `altium_set_design_rules` | `kicad_set_design_rules` | `fusion_set_design_rules` |-| `silkscreen_prep` | Clean the silkscreen off the copper with Altium's own Tools > Silkscreen Preparation: silk clipped clear of every mask opening (pad) by the board's silk... | `altium_silkscreen_prep` | | |+| `silkscreen_prep` | Clean the silkscreen off the copper with Altium's own Tools > Silkscreen Preparation: silk clipped clear of every mask opening (pad) by the board's silk... | `altium_silkscreen_prep` | `kicad_silkscreen_prep` | | | `symbol_mode` | Show one display mode (0 = Normal, 1.. = alternates) of the CURRENT SchLib component in the editor and capture a frame. Drives the editor's own mode toggle,... | `altium_symbol_mode` | | | | `teardrops` | Add (or remove) teardrops with Altium's own Tools > Teardrops: on vias and through-hole pads, SMD pads, track width changes and T-junctions, for the whole... | `altium_teardrops` | `kicad_teardrops` (partial) | | | `uninstall_library` | Remove libraries from Altium's Installed Libraries: the inverse of install_library / install_library_bundle. Library files are never touched. | `altium_uninstall_library` | `kicad_uninstall_library` | |@@ -177,7 +177,7 @@ Engineering computed from the board model, not by the tool: one shared engine, s | `board_features` | Add the furniture every real board needs in one undo step: mounting holes (at points or the four corners), fiducials, keepout areas, and a stitching-via... | `altium_board_features` | `kicad_add_mounting_hole` (partial) | `fusion_board_features` (partial) | | `board_report` | One shareable page per board: every check run against the live board and its project (Altium's DRC, JLCPCB fab limits and assembly tiers, the schematic... | `altium_board_report` | `kicad_board_report` | | | `bom` | The project's bill of materials read straight from Altium: every schematic sheet the .PrjPcb lists (placed parts and their parameters) merged with the board... | `altium_bom` | `kicad_bom` | `fusion_board_bom` (partial) |-| `build_board` | Build a NEW board from a parts list and a netlist: outline (rounded corners allowed), every footprint from one PcbLib placed by its own origin, pad nets... | `altium_build_board` | | |+| `build_board` | Build a NEW board from a parts list and a netlist: outline (rounded corners allowed), every footprint from one PcbLib placed by its own origin, pad nets... | `altium_build_board` | `kicad_build_board` | | | `build_footprint` | Build a complete footprint (pads, silkscreen, courtyard, arcs) in a PcbLib from a kicad2altium spec file on this machine, and return a proof frame. | `altium_build_footprint` | | | | `build_schematic` | Draw a design's schematic from its netlist with the project library's symbols, inside a new project (.PrjPcb with the board and libraries), compile it, and... | `altium_build_schematic` | | `fusion_schematic_plan / fusion_schematic_run` (partial) | | `check_clearance` *proposed* | Measure the gaps between parts, courtyard to courtyard or pad copper to pad copper, for the listed parts or the whole board, and report every pair under a... | `altium_check_clearance` | `kicad_check_clearance` | |@@ -193,20 +193,20 @@ Engineering computed from the board model, not by the tool: one shared engine, s | `impedance` | Controlled impedance from the board's own stack-up: the track width each copper layer needs for a single-ended target (50 ohm default) and the width and gap... | `altium_impedance` | `kicad_impedance` | `fusion_impedance` | | `import_part` | One call imports a whole part: footprint spec + STEP + symbol modes + models + params + save. | `altium_import_part` | | | | `jlcpcb_order` | A JLCPCB order in one call: the board checked against JLCPCB's fab limits, the Gerbers and drill zipped the way JLCPCB's order page takes them, the JLCPCB... | `altium_jlcpcb_order` | `kicad_jlcpcb_order` | |-| `length_tune` | Lengthen a routed net to a target length, or to match another net (a pair's skew, a DDR byte lane): a serpentine in its longest straight track that has room... | `altium_length_tune` | | |+| `length_tune` | Lengthen a routed net to a target length, or to match another net (a pair's skew, a DDR byte lane): a serpentine in its longest straight track that has room... | `altium_length_tune` | `kicad_length_tune` | | | `net_lengths` | Routed length of every net (or the ones named) on the live board, by layer with its vias; the skew inside each differential pair; and the spread of... | `altium_net_lengths` | `kicad_net_lengths` | | | `pin_pad_check` | Do the footprints fit their symbols? Part by part, the symbol's pin numbers (from the sheet files) against the footprint's pad names (from the board): pins... | `altium_pin_pad_check` | `kicad_pin_pad_check` | | | `place_decoupling` *proposed* | Plan positions for decoupling capacitors beside an IC's power pins (each cap just outside the courtyard, its power pad facing the pin), answered as... | | `kicad_place_decoupling` | | | `placement_check` | Review the placement before routing: parts whose courtyards (pads and body, grown by a margin) overlap on the same side, parts off the board or over its... | `altium_placement_check` | `kicad_placement_validate / kicad_score_placement` (partial) | | | `plan_fanout` *proposed* | Plan the fanout of a BGA or QFN/QFP: the pad pitch, dogbone or via-in-pad per pad, the escape layer, surface stubs for the outer ring and a via grid for an... | | `kicad_plan_fanout` | | | `plan_route` *proposed* | Plan a path for one net between two pads without changing the board: a grid search around the existing copper that keeps the clearance rules, on the allowed... | | | `fusion_plan_route` (partial) |-| `read_sheet_file` | Read a schematic sheet straight from its .SchDoc file, without Altium (it need not be installed or running): every part with its designator, library... | `altium_read_sheet_file` | | `fusion_read_sheet_file` |+| `read_sheet_file` | Read a schematic sheet straight from its .SchDoc file, without Altium (it need not be installed or running): every part with its designator, library... | `altium_read_sheet_file` | `kicad_read_sheet_file` | `fusion_read_sheet_file` | | `return_path` | Where signal tracks lose their reference: for every signal net, the length of track on each layer that runs over no pour or plane copper on either... | `altium_return_path` | `kicad_return_path` | `fusion_return_path` | | `review_board` | Design-review checks (14) over one board read; schematic checks with includeSchematic. | `altium_review_board` | `kicad_lint_board` (partial) | | | `route_pair` | Route a differential pair as a pair: one centreline from the source pads' midpoint through waypoints to the destination pads' midpoint, each net offset to... | `altium_route_pair` | `kicad_route_pair` | | | `schematic_review` | Review the schematic before Update PCB: every placed pin (read from the sheet files, with its electrical type) against Altium's compiled netlist. Errors:... | `altium_schematic_review` | `kicad_sch_check / kicad_design_review / kicad_lint_schematic` (partial) | | | `silk_designators` | Place every reference designator the way assembly houses want: one size, upright, beside its own part, clear of every pad (JLCPCB: 0.15 mm), every other... | `altium_silk_designators` | `kicad_silk_designators` | |-| `test_board` | Place one footprint, or a ';' list, from the open PcbLib on ONE board: a NEW board (wMm x hMm outline) when pcbPath is new or omitted, APPENDED to the... | `altium_test_board` | | |+| `test_board` | Place one footprint, or a ';' list, from the open PcbLib on ONE board: a NEW board (wMm x hMm outline) when pcbPath is new or omitted, APPENDED to the... | `altium_test_board` | `kicad_test_board` | | | `thermal_vias` | A thermal via grid in a part's exposed pad (the ground or thermal pad under a QFN, DFN, PowerPAD or regulator): centred, inset from the pad's edge, sized by... | `altium_thermal_vias` | `kicad_thermal_vias` | `fusion_thermal_vias` | | `via_fence` | A via fence for EMC: a row of ground vias on both sides of RF or fast nets' tracks, and/or round the board edge, at a pitch from the frequency (lambda/20)... | `altium_via_fence` | `kicad_via_fence` | `fusion_via_fence` |
engine/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-eda-checks"-version = "0.2.0"+version = "0.2.1" dependencies = [ "roxmltree", "serde_json",
engine/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.2.0"+version = "0.2.1" edition = "2021" description = "The EDA Bridge Standard's checks and board-file writers, computed from the board model JSON: no bridge calls, no tool." license = "MIT"
engine/src/writers/gencad.rs+35−4@@ -48,16 +48,19 @@ pub fn gencad_text(state: &Value, title: &str, unique_pins: bool) -> (String, Va let k = (shape.to_string(), (w * 1000.0).round() / 1000.0, (h * 1000.0).round() / 1000.0, (drill * 1000.0).round() / 1000.0); match pad_defs.iter().position(|p| *p == k) { Some(i) => i, None => { pad_defs.push(k); pad_defs.len() - 1 } } };- // shapes: each footprint pattern once, from its first part+ // shapes: one per footprint pattern and pad geometry. Two parts share a shape only when their pins match in the+ // footprint's frame: two different footprints of the same name (from two libraries) get NAME and NAME_2 (#17) struct Pin { name: String, pad: usize, x: f64, y: f64, rot: f64, through: bool } let mut shapes: Vec<(String, Vec<Pin>, bool)> = Vec::new(); let mut shape_of: Vec<(String, String)> = Vec::new(); // ref -> shape name+ // the same pins within 5 um: the board model keeps positions to 1 um, so a rotated part's pins come back a micron off+ // (BQ25792's R3 against R5: 2.319 against 2.320 mm), and float noise turns 0 into -1e-16+ let same_pins = |a: &[Pin], b: &[Pin]| a.len() == b.len() && a.iter().zip(b).all(|(p, q)| p.name == q.name && p.pad == q.pad+ && (p.x - q.x).abs() < 0.005 && (p.y - q.y).abs() < 0.005 && (p.rot - q.rot).abs() < 0.05); for c in &comps { let rf = c["ref"].as_str().unwrap_or("").to_string(); let pattern = c["pattern"].as_str().unwrap_or("").to_string();- let name = if pattern.is_empty() { format!("SHAPE_{rf}") } else { pattern.clone() };- shape_of.push((rf.clone(), name.clone()));- if shapes.iter().any(|s| s.0 == name) { continue; }+ let base = if pattern.is_empty() { format!("SHAPE_{rf}") } else { pattern.clone() }; let (cx, cy, th) = (f(&c["x"]), f(&c["y"]), f(&c["rotation"]).to_radians()); let bottom = c["side"] == "bottom"; let mut pins = Vec::new();@@ -83,6 +86,15 @@ pub fn gencad_text(state: &Value, title: &str, unique_pins: bool) -> (String, Va let id = pad_name(p["shape"].as_str().unwrap_or("rect"), w, h, f(&p["drill"])); pins.push(Pin { name: nm, pad: id, x: lx, y: ly, rot, through }); }+ // the same pattern with the same pins: reuse that shape; the same name with other pins: a new name+ if let Some(existing) = shapes.iter().find(|(n, ps, _)| (*n == base || n.starts_with(&format!("{base}_"))) && same_pins(ps, &pins)) {+ shape_of.push((rf, existing.0.clone()));+ continue;+ }+ let mut name = base.clone();+ let mut k = 2;+ while shapes.iter().any(|x| x.0 == name) { name = format!("{base}_{k}"); k += 1; }+ shape_of.push((rf, name.clone())); let th_part = pins.iter().any(|p| p.through); shapes.push((name, pins, th_part)); }@@ -251,4 +263,23 @@ mod tests { assert_eq!(r["writtenFrom"], "board model"); assert!(r["text"].as_str().unwrap().starts_with("$HEADER\nGENCAD 1.4\n")); }++ #[test]+ fn same_named_footprints_with_other_pads_are_two_shapes() {+ let pad = |r: &str, n: &str, x: f64, w: f64| json!({"ref": r, "name": n, "x": x, "y": 0.0, "sizeX": w, "sizeY": 1.0, "shape": "rect", "drill": 0.0, "rotation": 0.0, "layer": "F.Cu", "net": ""});+ // R1 and R2: the same "R0603" footprint (R2 rotated 180): one shape. R3: another library's "R0603", other pads: its own+ let st = json!({"components": [{"ref": "R1", "pattern": "R0603", "x": 0.0, "y": 0.0, "rotation": 0.0, "side": "top"},+ {"ref": "R2", "pattern": "R0603", "x": 10.0, "y": 0.0, "rotation": 180.0, "side": "top"},+ {"ref": "R3", "pattern": "R0603", "x": 20.0, "y": 0.0, "rotation": 0.0, "side": "top"}],+ "pads": [pad("R1", "1", -0.8, 0.9), pad("R1", "2", 0.8, 0.9), pad("R2", "1", 10.801, 0.9), pad("R2", "2", 9.2, 0.9),+ pad("R3", "1", 19.0, 1.2), pad("R3", "2", 21.0, 1.2)]});+ let (t, _) = gencad_text(&st, "x", false);+ assert_eq!(t.matches("SHAPE \"R0603\"\nINSERT").count(), 1, "R1 and R2 share one shape");+ assert!(t.contains("SHAPE \"R0603_2\"\nINSERT"), "R3's footprint is its own shape");+ assert!(!t.contains("R0603_3"), "R2 reuses R1's shape");+ // each part points at its shape+ let comp = |r: &str| t.split(&format!("COMPONENT \"{r}\"\n")).nth(1).unwrap().lines().find(|l| l.starts_with("SHAPE ")).unwrap().to_string();+ assert!(comp("R1").starts_with("SHAPE \"R0603\"") && comp("R2").starts_with("SHAPE \"R0603\"") && comp("R3").starts_with("SHAPE \"R0603_2\""));+ } }+
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "eda-bridge-standard",- "version": "0.5.0",+ "version": "0.5.1", "type": "skill", "title": "EDA Bridge Standard: one way for an AI to drive every PCB tool", "description": "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.",
verbs/coverage.json+27−9@@ -1,12 +1,12 @@ {- "standard": "0.5.0",+ "standard": "0.5.1", "reference": { "bridge": "altium", "version": "0.2.78" }, "proposed": 54, "compared": {- "kicad": "1.0.68",+ "kicad": "1.0.69", "fusion360": "1.13.1" }, "tiers": {@@ -1673,7 +1673,10 @@ "verb": "altium_component_classes", "match": "exact" },- "kicad": null,+ "kicad": {+ "verb": "kicad_component_classes",+ "match": "exact"+ }, "fusion360": null }, {@@ -1989,7 +1992,7 @@ "kicad": { "verb": "kicad_panelize", "match": "partial",- "note": "V-score panels of rectangular boards only; no mouse bites or tabs"+ "note": "rectangular boards only (V-score or mouse-bite tabs); a shaped board is refused" }, "fusion360": null },@@ -2235,7 +2238,10 @@ "verb": "altium_silkscreen_prep", "match": "exact" },- "kicad": null,+ "kicad": {+ "verb": "kicad_silkscreen_prep",+ "match": "exact"+ }, "fusion360": null }, {@@ -2377,7 +2383,10 @@ "verb": "altium_build_board", "match": "exact" },- "kicad": null,+ "kicad": {+ "verb": "kicad_build_board",+ "match": "exact"+ }, "fusion360": null }, {@@ -2604,7 +2613,10 @@ "verb": "altium_length_tune", "match": "exact" },- "kicad": null,+ "kicad": {+ "verb": "kicad_length_tune",+ "match": "exact"+ }, "fusion360": null }, {@@ -2691,7 +2703,10 @@ "verb": "altium_read_sheet_file", "match": "exact" },- "kicad": null,+ "kicad": {+ "verb": "kicad_read_sheet_file",+ "match": "exact"+ }, "fusion360": { "verb": "fusion_read_sheet_file", "match": "exact"@@ -2778,7 +2793,10 @@ "verb": "altium_test_board", "match": "exact" },- "kicad": null,+ "kicad": {+ "verb": "kicad_test_board",+ "match": "exact"+ }, "fusion360": null }, {
verbs/standard.json+2−2@@ -1,12 +1,12 @@ {- "standard": "0.5.0",+ "standard": "0.5.1", "reference": { "bridge": "altium", "version": "0.2.78" }, "proposed": 54, "compared": {- "kicad": "1.0.68",+ "kicad": "1.0.69", "fusion360": "1.13.1" }, "tiers": {