app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
← Commit history
AI Flow 0.1.55: Fusion flow in build order; 3D restored on the board; fusion view3d; library-name clash guard; schematic groups clear of fixed parts; Fusion clips; blank-clip fix
13 files changed
+274−93
Cargo.lock+10−10Cargo.toml+1−1SKILL.md+47−31bin/adom-aiflowcrates/adom-aiflow/src/main.rs+12−2crates/aiflow-bridge/src/fusion.rs+1−1crates/aiflow-bridge/src/lib.rs+3docs/fusion-lane.md+2−1docs/release-0.1.55.md+11package.json+1−1page.json+2−4skills/adom-aiflow/SKILL.md+47−31tools/fusion-project.py+137−11Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.1.54"+version = "0.1.55" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.1.54"+version = "0.1.55" dependencies = [ "serde", "serde_json",@@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.1.54"+version = "0.1.55" dependencies = [ "roxmltree", "serde",@@ -42,7 +42,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.1.54"+version = "0.1.55" dependencies = [ "aiflow-board", "serde",@@ -51,7 +51,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.1.54"+version = "0.1.55" dependencies = [ "aiflow-board", "aiflow-grid",@@ -61,7 +61,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.1.54"+version = "0.1.55" dependencies = [ "aiflow-board", "serde",@@ -70,7 +70,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.1.54"+version = "0.1.55" dependencies = [ "aiflow-board", "serde",@@ -79,7 +79,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.1.54"+version = "0.1.55" dependencies = [ "aiflow-board", "aiflow-copper",@@ -89,7 +89,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.1.54"+version = "0.1.55" dependencies = [ "aiflow-board", "aiflow-grid",@@ -99,7 +99,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.1.54"+version = "0.1.55" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.1.54"+version = "0.1.55" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+47−31@@ -33,42 +33,58 @@ The flow is a file, `flows/board.json`: the steps in order (intake, models, plac ## A board in Autodesk Fusion -When the human asks for a board in Fusion, the whole flow below is yours: they should never have to-spell out a step. Read `docs/fusion-lane.md` once; the Fusion bridge must be 1.11.33 or later.+When the human asks for a board in Fusion ("a 12 V to 5 V buck converter using the TPS54202, in Fusion"),+the whole flow below is yours: they should never have to spell out a step. Read `docs/fusion-lane.md`+once; the Fusion bridge must be 1.11.33 or later. Hydrogen stays full screen behind Fusion; keep Fusion+windowed (about 90 % of the screen), never maximized, so the human sees both and a screen recording+catches Fusion's 3D view. -1. **One project library.** `adom-aiflow fusion library --parts <netlist.json> --footprints <kicad lib folder> --name <Project>`- builds ONE library for the project in Fusion: every part from its wiki component page, the Mouser,- DigiKey and LCSC numbers looked up and written onto the part, the chip outline as the symbol for the- parts a reader recognises (regulators, inductors, big capacitors) and standard symbols for passives,- the laser-etched or coloured 3D model bound and rendering. A part added later (the human asks for- one more LED) goes into the same library: run `fusion library` again with it. Never a second library.- Every 3D model is checked for which way is up before it is bound. A STEP is often authored Y-up- while the board and Fusion are Z-up, and the file does not say which: bound as published, a part lies- on its side (the electrolytic can on its flank, the inductor on edge). `fusion library` applies each- footprint's own model transform (the `(model ... (rotate ...) (offset ...))` entry KiCad applies when- it shows the part) and checks every model against its footprint's courtyard; a model with no- transform whose height is not on Z is turned upright. Its reply names every part it turned. You do- not get this check for free from the AI's instincts, so do it on purpose: once the board exists, look- at it in 3D (`fusion_show_3d_board`, then `fusion_take_screenshot`; Windows window capture cannot see- Fusion's 3D viewport) and confirm every part sits on its pads the way its datasheet drawing shows- before you call the library or the board done.-2. **A reference design** (a KiCad board you are reproducing): `adom-aiflow fusion reference --kicad <board.kicad_pcb> --spec <spec.json>`- moves its placement and every spec coordinate into the Fusion board's frame (`moves.json`,- `fixed.json`, `spec.json`).-3. **The schematic, live.** `adom-aiflow fusion schematic --netlist <netlist.json> --spec <spec.json> --fixed <fixed.json> --name <Design>`- starts the design with the fab's rules and the fixed parts, draws the schematic in labelled groups- (one per block of the netlist), checks it pin by pin, and saves the design.-4. **The board.** `start --board board.brd --remote-board fusion:<Design>`, then placement, routing,+1. **The parts, from their wiki component pages.** Design the circuit (aiflow-circuit-design), then find+ each part's component page on the Adom wiki (a reference design on the wiki, such as a molecule page,+ is fair to start from). Show the human the important ones as you go: open the regulator's, the+ inductor's and the bulk capacitor's pages in a Hydrogen webview, a moment each+ (`adom-cli hydrogen webview open-or-refresh --name "Parts" --url <page url> --panel-id <id>`).+2. **One project library in Fusion.** `adom-aiflow fusion library --parts <netlist.json> --footprints <kicad lib folder> --name <Project>`+ builds ONE library for the project: every part from its page, the Mouser, DigiKey and LCSC numbers on+ the part, the chip outline as the symbol for the parts a reader recognises (regulators, inductors, big+ capacitors) and standard symbols for passives, the laser-etched or coloured 3D model bound. A part+ added later goes into the same library: run `fusion library` again with it. Never a second library.+ Every 3D model is checked for which way is up before it is bound (below).+3. **The schematic, live.** `adom-aiflow fusion schematic --netlist <netlist.json> --spec <spec.json> [--fixed <fixed.json>] --name <Design>`+ starts the design with the fab's rules, draws the schematic in labelled groups, checks it pin by pin,+ puts back any 3D model Fusion dropped when a part was added, and saves the design.+4. **Simulate before the board.** Run the circuit in the Adom SPICE dashboard (adom/adom-spice-skillpack;+ ngspice in the container is the default): `adom-spice start`, `adom-spice study <study.json>`,+ `adom-spice run ngspice`, `adom-spice show` (it opens in a Hydrogen webview; press its fullscreen+ button so the human sees it large). Check start-up, ripple and the load step against the+ requirements. When a measurement misses, change the part values in the schematic+ (`fusion_schematic_run` with `CHANGE VALUE`, and the netlist), run the study again, and say what you+ changed and why. Record it: `evidence add --kind simulation --file <result.json>`.+5. **The 2D board.** `start --board board.brd --remote-board fusion:<Design>`, then placement, routing, pours and `measure` as for any board.-5. **Current and heat, and refine until it passes.** `analyze current` / `analyze thermal` with- adom-fields on the live board. A failing screen prints `why:` lines naming the cause and the spec+6. **The board in 3D.** `adom-aiflow fusion view3d` shows the 3D PCB, saves a still from Fusion's own+ renderer and orbits the live view. Read the still: every part must sit flat on its pads.+7. **Current and heat, in Hydrogen.** `analyze current` and `analyze thermal` with adom-fields on the live+ board. Show it to the human in a Hydrogen webview: `adom-fields serve --fields <run>/fields &`, open+ `http://localhost:8874/` as a webview tab named "Adom Fields", then+ `adom-fields ui view --quantity current --parts ghost --camera iso` (the parts ghosted so the fields+ read first) and `adom-fields tour`. A failing screen prints `why:` lines naming the cause and the spec change that fixes it (a via ring: `viaPower`; another net crossing a patch: `planeUnder`). Change the spec, `adom-aiflow fusion reset` (the parts stay, the copper goes), then route, gate, land, pour and analyze again. Repeat until both screens pass, then `finish`.-6. **Ask before exporting.** The human may not want manufacturing files yet. Ask with the- AskUserQuestion tool ("Export the Gerbers, pick and place and the BOM now?", options "Export them" and- "Not now"); on yes, `adom-aiflow fusion export` writes the Gerbers (the JLCPCB CAM job), the pick and- place and the BOM with every distributor number to `<run>/manufacturing/`.+8. **Export when the human asks.** Do not export on your own: say the board is done and that the+ Gerbers, BOM and pick and place are one request away. When they ask, `adom-aiflow fusion export`+ writes the Gerbers (the JLCPCB CAM job), the pick and place and the BOM with every distributor number+ to `<run>/manufacturing/`; answer with each file's path and say they are ready for the fab.++**Which way is up: every 3D model is checked.** A STEP carries no "up" axis, and a model authored Y-up+lies on its side on a Z-up board (an electrolytic can on its flank, an inductor on edge). Adom component+pages publish Z-up, seated, centred STEPs with an identity footprint binding, and `fusion library`+checks every model against its footprint anyway: it applies a footprint's own model transform when there+is one, turns a model with no transform upright when its height is clearly not on Z, and names every part+it touched. You do not get this check for free from your own instincts, so do it on purpose: read the+`fusion view3d` still before you call the board done, and when a part is wrong, fix its component page+(the source), not just this board. ## Before the board: intake, sourcing, evidence
bin/adom-aiflow⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+12−2@@ -798,7 +798,7 @@ fn blank_check(file: &str) -> Option<String> { let secs = media_seconds(file); if secs <= 0.0 { return Some("no frames".into()); } let n = 8usize;- let mut flat = 0usize; let mut total = 0usize;+ let mut flat = 0usize; let mut total = 0usize; let mut moved = 0usize; let mut prev: Option<Vec<u8>> = None; for k in 0..n { let t = secs * (k as f64 + 0.5) / n as f64; let out = std::process::Command::new("ffmpeg").args(["-v", "error", "-ss", &format!("{t:.2}"), "-i", file, "-frames:v", "1", "-vf", "crop=iw*0.6:ih*0.6,scale=96:54", "-f", "rawvideo", "-pix_fmt", "gray", "-"]).output().ok()?;@@ -808,9 +808,16 @@ fn blank_check(file: &str) -> Option<String> { for b in &px { bins[(*b as usize) >> 4] += 1; } total += 1; if bins.iter().max().copied().unwrap_or(0) * 100 > px.len() * 80 { flat += 1; }+ // a mostly dark canvas that changes between samples is being worked on (a Fusion board is black+ // with the copper growing on it), not blank: count the samples that differ from the one before+ if let Some(q) = &prev {+ let changed = px.iter().zip(q.iter()).filter(|(a, b)| (**a as i32 - **b as i32).abs() > 16).count();+ if changed * 200 > px.len() { moved += 1; }+ }+ prev = Some(px); } if total == 0 { return Some("could not read frames".into()); }- if flat * 4 > total * 3 { Some(format!("{flat} of {total} sampled frames are one flat colour in the middle of the window (an unpainted or covered canvas)")) } else { None }+ if flat * 4 > total * 3 && moved < 2 { Some(format!("{flat} of {total} sampled frames are one flat colour in the middle of the window (an unpainted or covered canvas)")) } else { None } } /// A contact sheet: nine frames spread through the clip on one PNG, so the AI looks at what it@@ -3669,7 +3676,10 @@ fn main() { // a Fusion run films Fusion's own window, the board fitted to it first (a remembered // hwnd can be a KiCad window from before the board, and Fusion keeps whatever zoom it had) let fusion_run = aiflow_bridge::fusion::active().is_some();+ // a Fusion project before its board exists (library, schematic): film Fusion, never a KiCad window+ let fusion_project = !fusion_run && dir.join("fusion").join("project.json").is_file(); let hwnd = if name == "fields" { None }+ else if fusion_project { br.fusion_hwnd() } else if fusion_run { let h = br.pcb_editor_hwnd(); let _ = br.raw_call("fusion_view", &json!({"action": "fit", "space": "2d"})); h } else { r.data["captures"].as_array().and_then(|c| c.iter().rev().find_map(|e| e.get("hwnd").and_then(|h| h.as_i64()))).or_else(|| br.pcb_editor_hwnd()) }; let disk = disk_check(&dir, &br, name);
crates/aiflow-bridge/src/fusion.rs+1−1@@ -185,7 +185,7 @@ pub fn translate(verb: &str, args: &Value, doc: &str, call: &dyn Fn(&str, &Value } /// Fusion's main window on the test box.-fn main_hwnd(call: &dyn Fn(&str, &Value) -> Value) -> Option<i64> {+pub fn main_hwnd(call: &dyn Fn(&str, &Value) -> Value) -> Option<i64> { let w = call("desktop_list_windows", &json!({})); let out = w.get("output").and_then(|o| o.as_str()).and_then(|s| serde_json::from_str::<Value>(s).ok()).unwrap_or(w.clone()); let list = out["data"]["windows"].as_array().or(out["windows"].as_array()).cloned().unwrap_or_default();
crates/aiflow-bridge/src/lib.rs+3@@ -187,6 +187,9 @@ impl Bridge { } /// The PCB editor's window handle, if KiCad has one open.+ /// Fusion's main window on the box (a Fusion project films it before the board exists too).+ pub fn fusion_hwnd(&self) -> Option<i64> { fusion::main_hwnd(&|v, a| self.raw_call(v, a)) }+ pub fn pcb_editor_hwnd(&self) -> Option<i64> { let r = self.call("kicad_window_info", &json!({})); let d = r.get("data").cloned().unwrap_or(r.clone());
docs/fusion-lane.md+2−1@@ -48,8 +48,9 @@ behind the flow's back is refused (`stale_board`), never edited blind. | `fusion library --parts <netlist.json> --footprints <folder> --name <Project>` | ONE library for the project: each part from its wiki component page, Mouser, DigiKey and LCSC numbers (the page's own, then the distributor lookups, each checked against the exact MPN), the chip outline as symbol for ICs, inductors and big capacitors, standard symbols for passives, the etched or coloured 3D bound. Run again with new parts and they join the same library; parts already bound are kept. | | `fusion reference --kicad <board.kicad_pcb> --spec <spec.json>` | A reference design's placement and spec coordinates in the Fusion board's frame: `moves.json`, `fixed.json`, `spec.json`. | | `fusion schematic --netlist <netlist.json> --spec <spec.json> --fixed <fixed.json> --name <Design>` | The design started with the fab's rules and the fixed parts (`fusion_board_seed`), the schematic drawn live in labelled groups (`fusion_schematic_plan`, `fusion_schematic_run`), checked pin by pin, saved, and the board written for `start`. |+| `fusion view3d` | The board in Fusion's 3D PCB: a still from Fusion's own renderer saved in the run to check every part sits flat on its pads, then a slow live orbit for the recording. | | `fusion reset` | A refine pass: every track, via and pour cleared from the live board; the parts stay. |-| `fusion export` | Gerbers (the JLCPCB CAM job), pick and place, and the BOM with every distributor number, in `<run>/manufacturing/`. Ask the human first. |+| `fusion export` | Gerbers (the JLCPCB CAM job), pick and place, and the BOM with every distributor number, in `<run>/manufacturing/`. Only when the human asks; answer with each file's path. | On a Fusion run every step's clip films Fusion's main window, the board fitted to the view first. The library sets part names to 0.8 mm and puts values on tDocu, so the silkscreen stays readable on a small board.
docs/release-0.1.55.mdadded+11@@ -0,0 +1,11 @@+# AI Flow 0.1.55++The Fusion lane, in the order a person builds a board: parts from their wiki pages, one library, the schematic, simulation, the 2D board, the 3D board, current and heat, then the files when asked.++- **3D models are put back on the board.** Fusion drops a part's 3D binding when the schematic adds it live, and the 3D PCB then shows flat placeholder boxes. `fusion schematic` now counts them, exports the design, writes every part's model onto its schematic part and board element, and imports it again before saving. Proven on the buck: 20 parts restored, 0 left.+- **`fusion view3d`** shows the board in Fusion's 3D PCB, saves a still from Fusion's own renderer for the AI to check that every part sits flat on its pads, and orbits the live view for the recording. Keep Fusion windowed: a maximized window is presented past the Windows compositor and a screen recording sees a stale frame.+- **No clash with an earlier run's library.** A new project whose library name another run on the same desktop already used gets a dated name, so Fusion never stops on "several libraries named ...".+- **The schematic's groups start clear of the fixed parts' column**, which they used to overlap.+- **Clips film Fusion on a Fusion project** from the first step, not a KiCad window.+- **The blank-clip check** no longer flags a mostly black Fusion board that is being worked on (the samples change).+- **The skill's Fusion flow:** show the key parts' wiki pages in a Hydrogen webview; simulate in the Adom SPICE dashboard (full screen in Hydrogen) and adjust the schematic when a measurement misses; `fusion view3d`; adom-fields in a Hydrogen webview with the parts ghosted; export only when the human asks, answering with each file's path.
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.1.54",+ "version": "0.1.55", "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.54",+ "version": "0.1.55", "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"
skills/adom-aiflow/SKILL.md+47−31@@ -33,42 +33,58 @@ The flow is a file, `flows/board.json`: the steps in order (intake, models, plac ## A board in Autodesk Fusion -When the human asks for a board in Fusion, the whole flow below is yours: they should never have to-spell out a step. Read `docs/fusion-lane.md` once; the Fusion bridge must be 1.11.33 or later.+When the human asks for a board in Fusion ("a 12 V to 5 V buck converter using the TPS54202, in Fusion"),+the whole flow below is yours: they should never have to spell out a step. Read `docs/fusion-lane.md`+once; the Fusion bridge must be 1.11.33 or later. Hydrogen stays full screen behind Fusion; keep Fusion+windowed (about 90 % of the screen), never maximized, so the human sees both and a screen recording+catches Fusion's 3D view. -1. **One project library.** `adom-aiflow fusion library --parts <netlist.json> --footprints <kicad lib folder> --name <Project>`- builds ONE library for the project in Fusion: every part from its wiki component page, the Mouser,- DigiKey and LCSC numbers looked up and written onto the part, the chip outline as the symbol for the- parts a reader recognises (regulators, inductors, big capacitors) and standard symbols for passives,- the laser-etched or coloured 3D model bound and rendering. A part added later (the human asks for- one more LED) goes into the same library: run `fusion library` again with it. Never a second library.- Every 3D model is checked for which way is up before it is bound. A STEP is often authored Y-up- while the board and Fusion are Z-up, and the file does not say which: bound as published, a part lies- on its side (the electrolytic can on its flank, the inductor on edge). `fusion library` applies each- footprint's own model transform (the `(model ... (rotate ...) (offset ...))` entry KiCad applies when- it shows the part) and checks every model against its footprint's courtyard; a model with no- transform whose height is not on Z is turned upright. Its reply names every part it turned. You do- not get this check for free from the AI's instincts, so do it on purpose: once the board exists, look- at it in 3D (`fusion_show_3d_board`, then `fusion_take_screenshot`; Windows window capture cannot see- Fusion's 3D viewport) and confirm every part sits on its pads the way its datasheet drawing shows- before you call the library or the board done.-2. **A reference design** (a KiCad board you are reproducing): `adom-aiflow fusion reference --kicad <board.kicad_pcb> --spec <spec.json>`- moves its placement and every spec coordinate into the Fusion board's frame (`moves.json`,- `fixed.json`, `spec.json`).-3. **The schematic, live.** `adom-aiflow fusion schematic --netlist <netlist.json> --spec <spec.json> --fixed <fixed.json> --name <Design>`- starts the design with the fab's rules and the fixed parts, draws the schematic in labelled groups- (one per block of the netlist), checks it pin by pin, and saves the design.-4. **The board.** `start --board board.brd --remote-board fusion:<Design>`, then placement, routing,+1. **The parts, from their wiki component pages.** Design the circuit (aiflow-circuit-design), then find+ each part's component page on the Adom wiki (a reference design on the wiki, such as a molecule page,+ is fair to start from). Show the human the important ones as you go: open the regulator's, the+ inductor's and the bulk capacitor's pages in a Hydrogen webview, a moment each+ (`adom-cli hydrogen webview open-or-refresh --name "Parts" --url <page url> --panel-id <id>`).+2. **One project library in Fusion.** `adom-aiflow fusion library --parts <netlist.json> --footprints <kicad lib folder> --name <Project>`+ builds ONE library for the project: every part from its page, the Mouser, DigiKey and LCSC numbers on+ the part, the chip outline as the symbol for the parts a reader recognises (regulators, inductors, big+ capacitors) and standard symbols for passives, the laser-etched or coloured 3D model bound. A part+ added later goes into the same library: run `fusion library` again with it. Never a second library.+ Every 3D model is checked for which way is up before it is bound (below).+3. **The schematic, live.** `adom-aiflow fusion schematic --netlist <netlist.json> --spec <spec.json> [--fixed <fixed.json>] --name <Design>`+ starts the design with the fab's rules, draws the schematic in labelled groups, checks it pin by pin,+ puts back any 3D model Fusion dropped when a part was added, and saves the design.+4. **Simulate before the board.** Run the circuit in the Adom SPICE dashboard (adom/adom-spice-skillpack;+ ngspice in the container is the default): `adom-spice start`, `adom-spice study <study.json>`,+ `adom-spice run ngspice`, `adom-spice show` (it opens in a Hydrogen webview; press its fullscreen+ button so the human sees it large). Check start-up, ripple and the load step against the+ requirements. When a measurement misses, change the part values in the schematic+ (`fusion_schematic_run` with `CHANGE VALUE`, and the netlist), run the study again, and say what you+ changed and why. Record it: `evidence add --kind simulation --file <result.json>`.+5. **The 2D board.** `start --board board.brd --remote-board fusion:<Design>`, then placement, routing, pours and `measure` as for any board.-5. **Current and heat, and refine until it passes.** `analyze current` / `analyze thermal` with- adom-fields on the live board. A failing screen prints `why:` lines naming the cause and the spec+6. **The board in 3D.** `adom-aiflow fusion view3d` shows the 3D PCB, saves a still from Fusion's own+ renderer and orbits the live view. Read the still: every part must sit flat on its pads.+7. **Current and heat, in Hydrogen.** `analyze current` and `analyze thermal` with adom-fields on the live+ board. Show it to the human in a Hydrogen webview: `adom-fields serve --fields <run>/fields &`, open+ `http://localhost:8874/` as a webview tab named "Adom Fields", then+ `adom-fields ui view --quantity current --parts ghost --camera iso` (the parts ghosted so the fields+ read first) and `adom-fields tour`. A failing screen prints `why:` lines naming the cause and the spec change that fixes it (a via ring: `viaPower`; another net crossing a patch: `planeUnder`). Change the spec, `adom-aiflow fusion reset` (the parts stay, the copper goes), then route, gate, land, pour and analyze again. Repeat until both screens pass, then `finish`.-6. **Ask before exporting.** The human may not want manufacturing files yet. Ask with the- AskUserQuestion tool ("Export the Gerbers, pick and place and the BOM now?", options "Export them" and- "Not now"); on yes, `adom-aiflow fusion export` writes the Gerbers (the JLCPCB CAM job), the pick and- place and the BOM with every distributor number to `<run>/manufacturing/`.+8. **Export when the human asks.** Do not export on your own: say the board is done and that the+ Gerbers, BOM and pick and place are one request away. When they ask, `adom-aiflow fusion export`+ writes the Gerbers (the JLCPCB CAM job), the pick and place and the BOM with every distributor number+ to `<run>/manufacturing/`; answer with each file's path and say they are ready for the fab.++**Which way is up: every 3D model is checked.** A STEP carries no "up" axis, and a model authored Y-up+lies on its side on a Z-up board (an electrolytic can on its flank, an inductor on edge). Adom component+pages publish Z-up, seated, centred STEPs with an identity footprint binding, and `fusion library`+checks every model against its footprint anyway: it applies a footprint's own model transform when there+is one, turns a model with no transform upright when its height is clearly not on Z, and names every part+it touched. You do not get this check for free from your own instincts, so do it on purpose: read the+`fusion view3d` still before you call the board done, and when a part is wrong, fix its component page+(the source), not just this board. ## Before the board: intake, sourcing, evidence
tools/fusion-project.py+137−11@@ -381,6 +381,19 @@ def cmd_library(a): if not shutil.which(tool): err('%s is not installed (it turns each part page into Fusion library parts)' % tool, ['adom-wiki pkg install %s, then run this again.' % pkg]) st = load_state(); name = st.get('library') or a.name or 'Project' if not re.fullmatch(r'[A-Za-z0-9_-]{1,40}', name): err('library name: letters, digits, _ and - only')+ if not st.get('library'):+ # Fusion remembers every library it has used; a second library with the same name (an earlier run of+ # the same project on this desktop) makes the schematic stop on "several libraries named ...". A new+ # project takes a name no other run on this desktop has; this run keeps it from then on.+ root = box_dir().rsplit('/', 1)[0]+ r = ab('shell_execute', {'command': 'dir /b /s "%s\\%s.lbr"' % (root.replace('/', '\\'), name), 'timeoutSeconds': 30}, timeout=60)+ mine = box_dir().replace('\\', '/').lower().rstrip('/') + '/'+ taken = [l.strip().replace('\\', '/') for l in str(r.get('stdout') or '').splitlines() if l.strip().lower().endswith('.lbr')]+ taken = [l for l in taken if not l.lower().startswith(mine)]+ if taken:+ base = name; name = '%s_%s' % (base, time.strftime('%m%d')); k = 1+ while any(('/%s.lbr' % name).lower() in l.lower() for l in taken): k += 1; name = '%s_%s%s' % (base, time.strftime('%m%d'), chr(96 + k))+ say('another run on this desktop already has a library named %s; this project\'s library is %s' % (base, name)) st['library'] = name want = [] src = json.loads(Path(a.parts).read_text()) if a.parts.endswith('.json') else []@@ -510,7 +523,7 @@ def cmd_library(a): 'This step already applied each footprint\'s own model transform and checked every model against its footprint, but look anyway: once the board exists, ' '`fusion_show_3d_board` then `fusion_take_screenshot` (Windows window capture cannot see Fusion\'s 3D viewport), and every part must sit flat on its pads the way its datasheet drawing shows. ' 'A part that does not: say which, and fix its model or its footprint\'s (model ... (rotate ...)) entry, then run `fusion library` again.',- 'Next: adom-aiflow fusion schematic --netlist <netlist.json> (the design starts from this library). New parts later: run `fusion library` again with them; they join this same library.'])+ 'Next: adom-aiflow fusion schematic --netlist <netlist.json> --spec <spec.json> --name <Design> (the design starts from this library). New parts later: run `fusion library` again with them; they join this same library.']) # ---------------------------------------------------------------- reference@@ -608,6 +621,13 @@ def cmd_schematic(a): parts = [{'ref': c['ref'], 'deviceset': by_ref[c['ref']], 'value': c.get('value'), 'block': c.get('block')} for c in nl['components'] if c['ref'] not in fixed_refs] plan = ab('fusion_schematic_plan', {'libraryPath': st['libraryPath'], 'parts': parts, 'nets': {n: [m for m in ms if m.split('.')[0] not in fixed_refs] for n, ms in nets.items()}}) if not plan.get('groups'): err('schematic plan: %s' % plan.get('error'))+ if fixed_refs:+ # the seed draws the fixed parts (a molecule's pins and contacts) as a column at the sheet's left edge;+ # the planner starts its groups at that same x, so move every group clear of the column+ shift = 45.72+ mv = lambda m: '(%.3fmm %smm)' % (float(m.group(1)) + shift, m.group(2))+ for g in plan['groups']:+ g['commands'] = [re.sub(r'\(([-\d.]+)mm ([-\d.]+)mm\)', mv, c) for c in g['commands']] sheet = {} for g in plan['groups']: say('drawing %s: %s' % (g['group'], ', '.join(g['parts'])))@@ -627,24 +647,129 @@ def cmd_schematic(a): ab('fusion_show_2d_board', {}); time.sleep(3) bi = ab('fusion_board_info', {}); n = ((bi.get('data') or bi).get('summary') or (bi.get('data') or bi)).get('componentCount') if n != len(nl['components']): err('the board has %s of %d parts: schematic/board sync is off' % (n, len(nl['components'])), ['Start from a clean Fusion session (close half-finished imported designs) and run this again.'])+ # 3D on the board: a part added live arrives without the library's 3D binding (Fusion then draws a flat+ # placeholder box in the 3D PCB). Count them and put them back before the design is saved.+ want3d = {m for m, p_ in st['parts'].items() if p_.get('urn')}+ def missing3d():+ cur = ab('fusion_get_app_state', {}); cur = (cur.get('data') or cur).get('activeDocument') or name+ sb = ab('fusion_board_source', {'expectDocument': cur})+ if not sb.get('source'): err('could not read the board to check its 3D: %s' % sb.get('error'), ['Show the board (fusion_show_2d_board) and run this again.'])+ return sorted(re.search(r'name="([^"]+)"', e).group(1) for e in re.findall(r'<element [^>]*>', sb.get('source') or '')+ if (re.search(r'package="([^"]+)"', e) or [None, None])[1] in want3d and 'package3d_urn' not in e)+ lost = missing3d(); restored = []+ if lost:+ restored = restore_3d(st, name, a.project, a.folder)+ ab('fusion_show_2d_board', {}); time.sleep(3)+ lost = missing3d() s = ab('fusion_save_to_cloud', {'name': name, 'projectName': a.project, 'folderPath': a.folder}) if not s.get('success'): ab('fusion_create_cloud_folder', {'folderName': a.folder, 'projectName': a.project}); s = ab('fusion_save_to_cloud', {'name': name, 'projectName': a.project, 'folderPath': a.folder}) src = ab('fusion_board_source', {'expectDocument': name}) if not src.get('source'): err('could not read the board back: %s' % src.get('error')) out = Path(a.board_out); out.write_text(src['source'])- # 3D on the board: a part added live can arrive without the library's 3D binding (Fusion then draws- # a flat placeholder box in the 3D PCB). Count them so the AI looks instead of assuming.- want3d = {m for m, p_ in st['parts'].items() if p_.get('urn')}- no3d = sorted(re.search(r'name="([^"]+)"', e).group(1) for e in re.findall(r'<element [^>]*>', src['source'])- if (re.search(r'package="([^"]+)"', e) or [None, None])[1] in want3d and 'package3d_urn' not in e) dru = Path(a.fixed).parent / 'board.kicad_dru' if a.fixed else None if dru and dru.is_file(): shutil.copy(dru, out.with_suffix('.kicad_dru')) st['design'] = name; save_state(st) ok('schematic drawn live in %d group(s), every pin on its net; the board followed with %d parts; saved as %s; board written to %s' % (len(plan['groups']), n, name, out),- [('3D: %d part(s) on the board carry no 3D binding and show as flat placeholder boxes in the 3D PCB: %s. The library has their models; '- 'Fusion dropped the binding when the part was added. Write each package\'s package3d_urn onto its board element and schematic part '- '(fusion_export_eagle_source both, add the attribute, import as the design) before calling the board done, and confirm in 3D with fusion_take_screenshot.' % (len(no3d), ', '.join(no3d))) if no3d else '',- 'Next: adom-aiflow start --board %s --spec <spec.json> --target %s --remote-board fusion:%s --engine <you> --prompt-time <paste time>' % (out, TARGET or '<box>', name)])+ [('3D: Fusion dropped the 3D models of %d part(s) when they were added; they were put back (the design was imported again with every binding on it): %s.' % (len(restored), ', '.join(restored))) if restored else '',+ ('3D: %d part(s) still show as flat placeholder boxes: %s. Read <run>/fusion/restore3d/ and fusion_check_dialogs.' % (len(lost), ', '.join(lost))) if lost else '',+ 'Look at the board in 3D before going on: `adom-aiflow fusion view3d` renders it with Fusion\'s own renderer (Windows capture cannot see Fusion\'s 3D viewport), and every part must sit on its pads the way its datasheet shows.',+ 'Next: simulate it before the board: the Adom SPICE dashboard (adom-spice start / study / run ngspice / show, full screen in a Hydrogen webview); change values in the schematic if a measurement misses; record it with `evidence add --kind simulation`.',+ 'Then the board: adom-aiflow start --board %s --spec <spec.json> --target %s --remote-board fusion:%s --engine <you> --prompt-time <paste time>' % (out, TARGET or '<box>', name)])+++def restore_3d(st, name, project, folder):+ """Fusion drops a part's 3D binding when the part is added from a library file (the 3D PCB then draws a+ flat placeholder box). The design is exported, every schematic part and board element of a package that+ has a model gets its package3d_urn, the library gets its packages3d, and the design is imported again.+ Returns the parts that were restored."""+ urns = {m: p['urn'] for m, p in st['parts'].items() if p.get('urn')}+ lib = st['library']; bdir = box_dir() + '/design'; loc = STATE / 'restore3d'; loc.mkdir(parents=True, exist_ok=True)+ for ext in ('sch', 'brd'):+ r = ab('fusion_export_eagle_source', {'outputPath': '%s/%s-src.%s' % (bdir, name, ext)})+ if not r.get('savedTo') and not r.get('success'): err('3D restore: could not export the %s: %s' % (ext, r.get('error')))+ ab('pull_file', {'filePaths': ['%s/%s-src.sch' % (bdir, name), '%s/%s-src.brd' % (bdir, name)], 'saveTo': str(loc.resolve())}, timeout=600)+ p3 = '<packages3d>' + ''.join('<package3d name="%s" urn="" wip_urn="%s" locally_modified="yes" type="model"><description>%s</description><packageinstances><packageinstance name="%s"/></packageinstances></package3d>' % (m, u, m, m) for m, u in urns.items()) + '</packages3d>'+ def fix_lib(t):+ def one(mt):+ L = mt.group(0)+ if 'name="%s"' % lib not in L[:300]: return L+ L = re.sub(r'(?s)<packages3d>.*?</packages3d>', '', L)+ L = L.replace('</packages>', '</packages>' + p3, 1)+ def dev(md):+ D = md.group(0); pk = re.search(r'package="([^"]+)"', D.split('>', 1)[0])+ if not pk or pk.group(1) not in urns or 'package3dinstances' in D: return D+ return D.replace('</connects>', '</connects><package3dinstances><package3dinstance package3d_urn="%s"/></package3dinstances>' % urns[pk.group(1)], 1)+ return re.sub(r'(?s)<device [^>]*>.*?</device>', dev, L)+ return re.sub(r'(?s)<library [^>]*>.*?</library>', one, t)+ restored = set()+ brd = fix_lib((loc / (name + '-src.brd')).read_text())+ def el(mt):+ e = mt.group(0); pk = re.search(r'package="([^"]+)"', e)+ if 'package3d_urn' in e or not pk or pk.group(1) not in urns: return e+ restored.add(re.search(r'name="([^"]+)"', e).group(1))+ return e[:-1].rstrip('/') + ' package3d_urn="%s"' % urns[pk.group(1)] + ('/>' if e.endswith('/>') else '>')+ brd = re.sub(r'<element [^>]*>', el, brd)+ sch = fix_lib((loc / (name + '-src.sch')).read_text())+ devpkg = {}+ for md in re.finditer(r'(?s)<deviceset name="([^"]*)".*?</deviceset>', sch):+ for d in re.finditer(r'<device name="([^"]*)" package="([^"]*)"', md.group(0)): devpkg[(md.group(1), d.group(1))] = d.group(2)+ def part(mt):+ e = mt.group(0)+ if 'package3d_urn' in e or 'library="%s"' % lib not in e: return e+ pk = devpkg.get((re.search(r'deviceset="([^"]*)"', e).group(1), (re.search(r'device="([^"]*)"', e) or [None, ''])[1]))+ if pk not in urns: return e+ return e[:-1].rstrip('/') + ' package3d_urn="%s"' % urns[pk] + ('/>' if e.endswith('/>') else '>')+ sch = re.sub(r'<part [^>]*>', part, sch)+ if not restored: return []+ ET.fromstring(brd); ET.fromstring(sch)+ (loc / (name + '.sch')).write_text(sch); (loc / (name + '.brd')).write_text(brd)+ send([loc / (name + '.sch'), loc / (name + '.brd')], bdir + '/restored')+ say('putting the 3D models back on %d part(s): the design is imported again with every binding written on it' % len(restored))+ ab('fusion_close_document', {'save': False}) # the active design (not saved under its name yet)+ ab('fusion_execute_text_command', {'command': 'Commands.Start NewElectronDesignDocumentCommand'})+ for _ in range(20):+ time.sleep(2)+ d_ = ab('fusion_get_app_state', {}); d_ = d_.get('data') or d_+ if d_.get('productType') == 'EcadDesignProductType' and not d_.get('busyCommand'): break+ time.sleep(3)+ r = ab('fusion_new_electronics_from_eagle', {'schPath': bdir + '/restored/' + name + '.sch', 'brdPath': bdir + '/restored/' + name + '.brd'})+ rid = r.get('runId'); s_ = {}+ for _ in range(60):+ time.sleep(6)+ s_ = ab('fusion_eagle_import_status', {'runId': rid})+ if s_.get('stage') in ('done', 'failed'): break+ if s_.get('stage') != 'done': err('3D restore: the import did not finish: %s' % str((s_.get('result') or {}).get('error'))[:300], ['The design before the restore is on the box: %s/%s-src.sch and .brd.' % (bdir, name)])+ return sorted(restored)+++def cmd_view3d(a):+ """The board in 3D, for the human and for the check: Fusion's 3D PCB, an iso view, a still from Fusion's+ own renderer saved in the run (read it: every part must sit flat on its pads), then a slow live orbit+ the step clip and the screen recording catch. Keep Fusion windowed (never maximized): a full-screen+ window is presented past the Windows compositor and a screen recording sees a stale frame."""+ st = load_state(); name = a.design or st.get('design')+ if not name: err('which design? pass --design <name>')+ r = ab('fusion_show_3d_board', {'documentName': name})+ if r.get('success') is False: err('could not show %s in 3D: %s' % (name, r.get('error')), ['Open the design (it must be the active document) and run this again.'])+ time.sleep(a.settle)+ ab('fusion_view', {'action': 'iso'}); ab('fusion_view', {'action': 'fit'}); time.sleep(1.5)+ out = RUN / 'view3d'; out.mkdir(parents=True, exist_ok=True)+ shot = ab('fusion_take_screenshot', {})+ sp = (shot.get('data') or shot).get('fullPath') or shot.get('fullPath')+ still = None+ if sp:+ ab('pull_file', {'filePaths': [sp], 'saveTo': str(out.resolve())}, timeout=300)+ got = out / Path(sp.replace('\\', '/')).name+ if got.is_file():+ still = out / ('board-3d-%s.png' % time.strftime('%H%M%S')); got.rename(still)+ say('orbiting the board once (%d steps)' % a.steps)+ for _ in range(a.steps):+ ab('fusion_view', {'action': 'orbit', 'degrees': 360.0 / a.steps}); time.sleep(a.pause)+ ab('fusion_view', {'action': 'iso'}); ab('fusion_view', {'action': 'fit'})+ ok('%s shown in 3D and orbited once%s' % (name, '; still: %s' % still if still else ''),+ ['Read the still before going on: every part sits flat on its pads, the way its datasheet drawing shows (no part on its side or on edge, no flat placeholder box). The AI does not catch a sideways model by itself; this look is the check.' if still else 'No still came back (fusion_take_screenshot); look at Fusion yourself.',+ 'A part that is wrong: say which; fix its wiki component page (the STEP must be Z-up, seated, centred, with an identity footprint binding) and run `fusion library` again.',+ 'Back to 2D: fusion_show_2d_board {"documentName": "%s"}.' % name]) # ---------------------------------------------------------------- export@@ -696,8 +821,9 @@ def main(): p.add_argument('--board-out', default='board.brd'); p.add_argument('--pause-ms', type=int, default=60) p = sub.add_parser('reset'); p.add_argument('--design') p = sub.add_parser('export'); p.add_argument('--design'); p.add_argument('--out-dir', default=str(RUN / 'manufacturing'))+ p = sub.add_parser('view3d', help='the board in 3D: a still to check and a live orbit'); p.add_argument('--design'); p.add_argument('--steps', type=int, default=24); p.add_argument('--pause', type=float, default=0.6); p.add_argument('--settle', type=float, default=8) a = ap.parse_args()- {'library': cmd_library, 'reference': cmd_reference, 'schematic': cmd_schematic, 'reset': cmd_reset, 'export': cmd_export}[a.cmd](a)+ {'library': cmd_library, 'reference': cmd_reference, 'schematic': cmd_schematic, 'reset': cmd_reset, 'export': cmd_export, 'view3d': cmd_view3d}[a.cmd](a) if __name__ == '__main__':