← 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

Colby Knox ·ec6beb6242 ·13d ago ·parent 10a28fe
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": {