← Commit history

AI Flow 0.1.56: KiCad 10 net references in the Fusion converter; via size as a [size, drill] pair

John Lauer ·696e534ec9 · ·parent 9d1ef26
8 files changed +41−19
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.55"+version = "0.1.56" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.55"+version = "0.1.56" dependencies = [  "serde",  "serde_json",@@ -33,7 +33,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.55"+version = "0.1.56" dependencies = [  "roxmltree",  "serde",@@ -42,7 +42,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.55"+version = "0.1.56" dependencies = [  "aiflow-board",  "serde",@@ -51,7 +51,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.55"+version = "0.1.56" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -61,7 +61,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.55"+version = "0.1.56" dependencies = [  "aiflow-board",  "serde",@@ -70,7 +70,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.55"+version = "0.1.56" dependencies = [  "aiflow-board",  "serde",@@ -79,7 +79,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.55"+version = "0.1.56" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -89,7 +89,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.55"+version = "0.1.56" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -99,7 +99,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.55"+version = "0.1.56" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.55"+version = "0.1.56" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/aiflow-board/src/eagle.rs+7−3
@@ -43,7 +43,11 @@ pub fn to_kicad(text: &str) -> Result<String, String> {     for s in &signals { let name = s.attribute("name").unwrap_or("").to_string(); if !name.is_empty() && !nets.contains_key(&name) { order.push(name.clone()); nets.insert(name, order.len()); } }     let mut pad_net: BTreeMap<(String, String), String> = BTreeMap::new();     for s in &signals { for c in s.children().filter(|x| x.has_tag_name("contactref")) { pad_net.insert((c.attribute("element").unwrap_or("").into(), c.attribute("pad").unwrap_or("").into()), s.attribute("name").unwrap_or("").into()); } }+    // KiCad's numbered net format: a pad names its net, a track or via gives the number only, and a zone+    // gives the number and then (net_name ...). KiCad 10 refuses a track, via or zone written like a pad.     let net = |name: &str| -> String { match nets.get(name) { Some(k) => format!("(net {k} {})", q(name)), None => "(net 0 \"\")".into() } };+    let net_id = |name: &str| -> String { format!("(net {})", nets.get(name).copied().unwrap_or(0)) };+    let zone_net = |name: &str| -> String { format!("(net {}) (net_name {})", nets.get(name).copied().unwrap_or(0), q(name)) };      let mut o = String::new();     o.push_str("(kicad_pcb (version 20241229) (generator \"adom-aiflow-fusion\") (generator_version \"1\")\n  (general (thickness 1.6))\n  (paper \"A4\")\n");@@ -122,11 +126,11 @@ pub fn to_kicad(text: &str) -> Result<String, String> {         let name = s.attribute("name").unwrap_or("");         for w in s.children().filter(|x| x.has_tag_name("wire")) {             let Some(l) = layer(w.attribute("layer")) else { continue };-            let _ = writeln!(o, "  (segment (start {} {}) (end {} {}) (width {}) (layer {l}) {})", n(f(&w, "x1")), n(yy(f(&w, "y1"))), n(f(&w, "x2")), n(yy(f(&w, "y2"))), n(f(&w, "width").max(0.05)), net(name));+            let _ = writeln!(o, "  (segment (start {} {}) (end {} {}) (width {}) (layer {l}) {})", n(f(&w, "x1")), n(yy(f(&w, "y1"))), n(f(&w, "x2")), n(yy(f(&w, "y2"))), n(f(&w, "width").max(0.05)), net_id(name));         }         for v in s.children().filter(|x| x.has_tag_name("via")) {             let drill = f(&v, "drill");-            let _ = writeln!(o, "  (via (at {} {}) (size {}) (drill {}) (layers \"F.Cu\" \"B.Cu\") {})", n(f(&v, "x")), n(yy(f(&v, "y"))), n(f(&v, "diameter").max(drill + 2.0 * ring(drill, via_rv, via_min, via_max))), n(drill), net(name));+            let _ = writeln!(o, "  (via (at {} {}) (size {}) (drill {}) (layers \"F.Cu\" \"B.Cu\") {})", n(f(&v, "x")), n(yy(f(&v, "y"))), n(f(&v, "diameter").max(drill + 2.0 * ring(drill, via_rv, via_min, via_max))), n(drill), net_id(name));         }         for p in s.children().filter(|x| x.has_tag_name("polygonpour") || x.has_tag_name("polygon")) {             let Some(l) = layer(p.attribute("layer")) else { continue };@@ -134,7 +138,7 @@ pub fn to_kicad(text: &str) -> Result<String, String> {             let outline = if p.has_tag_name("polygon") { verts(p) } else { p.children().find(|x| x.has_tag_name("polygonoutlineobjects")).map(verts).unwrap_or_default() };             if outline.is_empty() { continue; }             let pname = p.attribute("name").map(|v| format!(" (name {})", q(v))).unwrap_or_default();-            let _ = write!(o, "  (zone {} (layer {l}){pname} (hatch edge 0.5) (priority {}) (connect_pads (clearance {})) (min_thickness {}) (fill yes (thermal_gap 0.3) (thermal_bridge_width 0.3))\n    (polygon (pts {outline}))", net(name), p.attribute("rank").and_then(|r| r.parse::<i64>().ok()).map(|r| (7 - r).max(0)).unwrap_or(0), n(f(&p, "isolate").max(0.1)), n(f(&p, "width").max(0.1)));+            let _ = write!(o, "  (zone {} (layer {l}){pname} (hatch edge 0.5) (priority {}) (connect_pads (clearance {})) (min_thickness {}) (fill yes (thermal_gap 0.3) (thermal_bridge_width 0.3))\n    (polygon (pts {outline}))", zone_net(name), p.attribute("rank").and_then(|r| r.parse::<i64>().ok()).map(|r| (7 - r).max(0)).unwrap_or(0), n(f(&p, "isolate").max(0.1)), n(f(&p, "width").max(0.1)));             if let Some(fd) = p.children().find(|x| x.has_tag_name("polygonfilldetails")) {                 for sh in fd.children().filter(|x| x.has_tag_name("polygonshape")) {                     if let Some(seg) = sh.children().find(|x| x.has_tag_name("polygonoutlinesegments")) { let _ = write!(o, "\n    (filled_polygon (layer {l}) (pts {}))", verts(seg)); }
crates/aiflow-router/src/rules.rs+16
@@ -82,9 +82,14 @@ impl Rules {         // Escape necks inherit the requested track width. An explicit smaller         // stub must still satisfy the separately declared manufacturing minimum.         r.stub = num("stub").unwrap_or(r.track).max(num("minTrackWidth").unwrap_or(r.track));+        // a via is a size, or a [size, drill] pair (the way wideNets gives widths); either form is read,+        // never silently skipped+        let pair = |k: &str| spec.get(k).and_then(|v| v.as_array()).and_then(|a| Some((a.first()?.as_f64()?, a.get(1).and_then(|d| d.as_f64()))));         if let Some(v) = num("viaSignal") { r.via_small = v; }+        if let Some((v, d)) = pair("viaSignal") { r.via_small = v; if let Some(d) = d { r.drill_small = d; } }         if let Some(v) = num("drillSignal") { r.drill_small = v; }         if let Some(v) = num("viaPower") { r.via = v; }+        if let Some((v, d)) = pair("viaPower") { r.via = v; if let Some(d) = d { r.drill = d; } }         if let Some(v) = num("drillPower") { r.drill = v; }         r     }@@ -117,3 +122,14 @@ mod tests {         assert!(Rules::from_spec(&json!({})).plane_under.is_empty());     } }++#[cfg(test)]+mod via_pair_tests {+    #[test]+    fn via_power_as_pair_or_number() {+        let a = super::Rules::from_spec(&serde_json::json!({"viaPower": [1.0, 0.4]}));+        assert_eq!((a.via, a.drill), (1.0, 0.4));+        let b = super::Rules::from_spec(&serde_json::json!({"viaPower": 0.9, "drillPower": 0.35}));+        assert_eq!((b.via, b.drill), (0.9, 0.35));+    }+}
docs/release-0.1.56.mdadded+4
@@ -0,0 +1,4 @@+# AI Flow 0.1.56++- **KiCad 10 loads a converted Fusion board.** The Fusion-to-KiCad converter wrote every net reference the way a pad does, `(net 1 "GND")`, and KiCad 10.0.3 refused the board ("Failed to load board"), so the DRC gate could not run. Tracks and vias now give the number only, `(net 1)`, and zones give `(net 1) (net_name "GND")`, as KiCad writes them. Checked with KiCad 10.0.3 on the buck molecule's live board.+- **A via size can be a `[size, drill]` pair.** `"viaPower": [1.0, 0.4]` (the way `wideNets` gives widths) was silently ignored and cost a refine pass; it is now read, as is a plain number with `drillPower`. The same for `viaSignal`.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.55",+  "version": "0.1.56",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
page.json+2−4
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.55",+  "version": "0.1.56",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",@@ -44,9 +44,7 @@     "run.jsonl",     "thermal analysis"   ],-  "dependencies": {-    "adom/adom-lbr": "*"-  },+  "dependencies": {},   "scripts": {     "install": "./install.sh",     "uninstall": "./uninstall.sh"