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.54: 3D models checked for which way is up; missing 3D named; the AI told to look
10 files changed
+233−19
Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.1.53"+version = "0.1.54" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.1.53"+version = "0.1.54" dependencies = [ "serde", "serde_json",@@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.1.53"+version = "0.1.54" dependencies = [ "roxmltree", "serde",@@ -42,7 +42,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.1.53"+version = "0.1.54" dependencies = [ "aiflow-board", "serde",@@ -51,7 +51,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.1.53"+version = "0.1.54" dependencies = [ "aiflow-board", "aiflow-grid",@@ -61,7 +61,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.1.53"+version = "0.1.54" dependencies = [ "aiflow-board", "serde",@@ -70,7 +70,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.1.53"+version = "0.1.54" dependencies = [ "aiflow-board", "serde",@@ -79,7 +79,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.1.53"+version = "0.1.54" dependencies = [ "aiflow-board", "aiflow-copper",@@ -89,7 +89,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.1.53"+version = "0.1.54" dependencies = [ "aiflow-board", "aiflow-grid",@@ -99,7 +99,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.1.53"+version = "0.1.54" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.1.53"+version = "0.1.54" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+10@@ -42,6 +42,16 @@ spell out a step. Read `docs/fusion-lane.md` once; the Fusion bridge must be 1.1 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`).
bin/adom-aiflow⋯ 1 unchanged line ⋯
docs/fusion-lane.md+21@@ -53,6 +53,27 @@ behind the flow's back is refused (`stale_board`), never edited blind. 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. +### Which way is up: every 3D model is checked++A STEP model carries no "up" axis. Many are authored Y-up; the board and Fusion are Z-up. KiCad hides the+difference because each footprint carries its model's transform (`(model ... (rotate (xyz -90 0 0)) (offset ...))`)+and KiCad applies it when it shows the part. A Fusion package is bound to the raw STEP, so that transform+must be applied first, or the part lies on its side: on the buck molecule (2026-10-06) the 47 uF can lay on+its flank and the 5 x 5 mm inductor stood on edge, both Y-up models with a `rotate -90` in their footprints.++`fusion library` therefore, for every model, before binding:+1. applies the footprint's own model transform (KiCad's convention: rotate X, then Y, then Z, each by the+ negated angle; then the offset in mm);+2. measures the result and compares the two board-plane extents with the footprint's courtyard. If another+ axis fits decisively better than Z, a model with no footprint transform is turned upright and seated on+ z = 0; a model whose footprint did give a transform is left alone and named as a part to check;+3. binds the corrected copy (`<run>/fusion/models/<model>-zup.step`) and names every part it turned in its+ reply. Parts bound under older rules are bound again on the next `fusion library`.++The check is a backstop, not a proof. Look at the board in 3D before calling it done: `fusion_show_3d_board`,+then `fusion_take_screenshot` (Windows window capture cannot see Fusion's 3D viewport and shows a stale+frame), and every part must sit on its pads as its datasheet drawing shows.+ A failing `analyze` prints `why:` lines with the cause of each field-screen neck and the spec change that fixes it: a via whose copper ring is the narrowest copper (`viaPower`), or another net's track cutting a patch pour off from its pin (`planeUnder`).
docs/release-0.1.54.mdadded+8@@ -0,0 +1,8 @@+# AI Flow 0.1.54++- **Every 3D model is checked for which way is up.** A STEP carries no "up" axis, and many are authored Y-up while the board and Fusion are Z-up. KiCad hides this because each footprint carries its model's transform (`(model ... (rotate ...) (offset ...))`); the Fusion binding used the raw STEP, so on the buck molecule the 47 uF can lay on its flank and the inductor stood on edge. `fusion library` now applies the footprint's own transform (KiCad's convention), checks every model against its footprint's courtyard, turns a model with no transform upright when its height is clearly not on Z, binds the corrected copy, and names every part it turned. Parts bound under the old rules are bound again on the next `fusion library`.+- **The AI is told to look.** The library and schematic replies say to check the board in 3D with `fusion_take_screenshot` (Windows window capture cannot see Fusion's 3D viewport) before calling it done. The skill says why: the AI does not do this on its own.+- **Parts that lost their 3D are named.** `fusion schematic` counts the board parts whose 3D binding Fusion dropped when they were added live (they show as flat placeholder boxes) and says how to restore it.+- Docs: `docs/fusion-lane.md`, "Which way is up".++Known, next: restoring a dropped 3D binding automatically; the library name clashing with an earlier run of the same project; the blank-clip check flagging a mostly black Fusion board view; pre-board step clips on a Fusion project filming a KiCad window; the schematic's molecule-interface column overlapping the first group.
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.1.53",+ "version": "0.1.54", "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.53",+ "version": "0.1.54", "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+10@@ -42,6 +42,16 @@ spell out a step. Read `docs/fusion-lane.md` once; the Fusion bridge must be 1.1 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`).
tools/fusion-project.py+170−3@@ -213,6 +213,154 @@ def symbol_choice(comp, fp_text, sym_path): return 'own' +# -------------------------------------------------------------- 3D model orientation+# A STEP model is often authored Y-up (the convention of many mechanical tools and every glTF), and+# nothing in the file says which axis is up. Bound as published, a Y-up part lies on its side on the+# board: the electrolytic can on its flank, the inductor on edge. So every model is checked against its+# own footprint before it is bound: the two axes that lie on the board must span the footprint, and the+# one left over is the height. A model whose height is not on Z is turned upright and re-seated at z=0.++_NUM = r'[-+0-9.Ee]+'++def step_extents(text):+ pts = [tuple(map(float, m)) for m in re.findall(r"CARTESIAN_POINT\s*\(\s*'[^']*'\s*,\s*\(\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*\)" % (_NUM, _NUM, _NUM), text)]+ if not pts: return None+ return [min(p[i] for p in pts) for i in range(3)], [max(p[i] for p in pts) for i in range(3)]++def _blocks(text, head):+ """Every balanced (head ...) s-expression in text."""+ out, i = [], 0+ while True:+ i = text.find('(' + head, i)+ if i < 0: return out+ depth, j = 0, i+ while j < len(text):+ depth += (text[j] == '(') - (text[j] == ')')+ if depth == 0: break+ j += 1+ out.append(text[i:j + 1]); i = j + 1++def footprint_span(text):+ """The part's room on the board, in mm: its courtyard (leads included), else its fab drawing, else its pads."""+ def outline(layer):+ pts = []+ for head in ('fp_line', 'fp_rect', 'fp_poly', 'fp_arc'):+ for blk in _blocks(text, head + ' ') + _blocks(text, head + '\n') + _blocks(text, head + '\t') + _blocks(text, head + '\r'):+ if '"%s"' % layer not in blk: continue+ pts += [(float(x), float(y)) for x, y in re.findall(r'\((?:start|end|mid|xy)\s+(%s)\s+(%s)\)' % (_NUM, _NUM), blk)]+ for blk in _blocks(text, 'fp_circle'):+ if '"%s"' % layer not in blk: continue+ c = re.search(r'\(center\s+(%s)\s+(%s)\)' % (_NUM, _NUM), blk); e = re.search(r'\(end\s+(%s)\s+(%s)\)' % (_NUM, _NUM), blk)+ if c and e:+ cx, cy = float(c.group(1)), float(c.group(2)); r = math.hypot(float(e.group(1)) - cx, float(e.group(2)) - cy)+ pts += [(cx - r, cy - r), (cx + r, cy + r)]+ return pts+ pts = outline('F.CrtYd') or outline('F.Fab')+ if not pts:+ for blk in _blocks(text, 'pad '):+ a = re.search(r'\(at\s+(%s)\s+(%s)' % (_NUM, _NUM), blk); sz = re.search(r'\(size\s+(%s)\s+(%s)\)' % (_NUM, _NUM), blk)+ if a and sz:+ x, y, w, h = float(a.group(1)), float(a.group(2)), float(sz.group(1)), float(sz.group(2))+ pts += [(x - w / 2, y - h / 2), (x + w / 2, y + h / 2)]+ if not pts: return None+ xs, ys = [q[0] for q in pts], [q[1] for q in pts]+ return max(xs) - min(xs), max(ys) - min(ys)++def up_axis(ext, span):+ """Which model axis is the height: the one whose removal leaves the two that best match the footprint.+ Returns (up, misfit of that choice, misfit with Z as the height, extents)."""+ mn, mx = ext; d = [mx[i] - mn[i] for i in range(3)]+ fw, fh = sorted(span)+ def miss(up):+ a, b = sorted(d[i] for i in range(3) if i != up)+ return abs(a - fw) / max(fw, 0.1) + abs(b - fh) / max(fh, 0.1)+ best = min((2, 1, 0), key=miss)+ # Z stays the height unless another axis fits the footprint decisively better+ if best != 2 and not (miss(2) > 0.35 and miss(best) < 0.5 * miss(2)): best = 2+ return best, miss(best), miss(2), d++def _rot(up):+ # rotation that carries the model's up axis onto +Z (right-handed, determinant +1)+ if up == 1: return lambda x, y, z: (x, -z, y) # +90 about X: +Y -> +Z+ if up == 0: return lambda x, y, z: (-z, y, x) # -90 about Y: +X -> +Z+ return lambda x, y, z: (x, y, z)++def make_upright(text, up):+ """Rotate every point and direction so `up` becomes Z, then seat the model on z=0. A rigid motion of+ all CARTESIAN_POINTs and DIRECTIONs moves STEP B-rep geometry consistently (radii and lengths keep)."""+ R = _rot(up)+ def sub(kind, shift):+ def f(m):+ x, y, z = R(float(m.group(2)), float(m.group(3)), float(m.group(4)))+ if shift: z -= shift+ return "%s('%s',(%.9g,%.9g,%.9g))" % (kind, m.group(1), x, y, z)+ return f+ pat = lambda kind: r"%s\s*\(\s*'([^']*)'\s*,\s*\(\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*\)\s*\)" % (kind, _NUM, _NUM, _NUM)+ t = re.sub(pat('DIRECTION'), sub('DIRECTION', 0), text)+ t = re.sub(pat('CARTESIAN_POINT'), sub('CARTESIAN_POINT', 0), t)+ zmin = step_extents(t)[0][2]+ t = re.sub(pat('CARTESIAN_POINT'), sub('CARTESIAN_POINT', 0) if abs(zmin) < 1e-6 else (lambda m: "CARTESIAN_POINT('%s',(%.9g,%.9g,%.9g))" % (m.group(1), float(m.group(2)), float(m.group(3)), float(m.group(4)) - zmin)), t)+ return t++def kicad_model_transform(fp_text):+ """The footprint's own (model ...) entry: rotate (deg) and offset (mm), or None. KiCad applies these+ when it shows the model; a Fusion package gets the raw STEP, so they must be applied here."""+ i = fp_text.find('(model')+ if i < 0: return None+ blk = fp_text[i:i + 600]+ rot = re.search(r'\(rotate\s*\(xyz\s+(%s)\s+(%s)\s+(%s)\s*\)' % (_NUM, _NUM, _NUM), blk)+ off = re.search(r'\(offset\s*\(xyz\s+(%s)\s+(%s)\s+(%s)\s*\)' % (_NUM, _NUM, _NUM), blk)+ return [float(v) for v in rot.groups()] if rot else [0.0, 0.0, 0.0], [float(v) for v in off.groups()] if off else [0.0, 0.0, 0.0]++def _matrix(rx, ry, rz):+ # KiCad: p' = Rz(-rz) . Ry(-ry) . Rx(-rx) . p (angles in degrees, applied X first)+ def R(axis, deg):+ c, s_ = math.cos(math.radians(-deg)), math.sin(math.radians(-deg))+ return {'x': [[1, 0, 0], [0, c, -s_], [0, s_, c]], 'y': [[c, 0, s_], [0, 1, 0], [-s_, 0, c]], 'z': [[c, -s_, 0], [s_, c, 0], [0, 0, 1]]}[axis]+ mul = lambda A, B: [[sum(A[i][k] * B[k][j] for k in range(3)) for j in range(3)] for i in range(3)]+ return mul(R('z', rz), mul(R('y', ry), R('x', rx)))++def transform_step(text, M, off=(0, 0, 0)):+ """Apply a rigid motion to every CARTESIAN_POINT and DIRECTION (B-rep geometry moves with them)."""+ ap = lambda v: [sum(M[i][k] * v[k] for k in range(3)) for i in range(3)]+ pat = lambda kind: r"%s\s*\(\s*'([^']*)'\s*,\s*\(\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*\)\s*\)" % (kind, _NUM, _NUM, _NUM)+ def sub(kind, add):+ def f(m):+ v = ap([float(m.group(2)), float(m.group(3)), float(m.group(4))])+ return "%s('%s',(%.9g,%.9g,%.9g))" % (kind, m.group(1), v[0] + add[0], v[1] + add[1], v[2] + add[2])+ return f+ t = re.sub(pat('DIRECTION'), sub('DIRECTION', (0, 0, 0)), text)+ return re.sub(pat('CARTESIAN_POINT'), sub('CARTESIAN_POINT', off), t)++def upright_model(step, fp, out_dir):+ """The model as it must sit on the board: the footprint's own transform applied, then checked against+ the footprint (the two board-plane extents must span it). Returns (path, note); note is None when the+ STEP was already right."""+ text = step.read_text(errors='ignore'); fpt = fp.read_text(errors='ignore')+ notes = []+ tr = kicad_model_transform(fpt)+ if tr and (any(abs(a) > 1e-9 for a in tr[0]) or any(abs(o) > 1e-9 for o in tr[1])):+ text = transform_step(text, _matrix(*tr[0]), tr[1])+ notes.append("the footprint's own model transform (rotate %s, offset %s mm) applied, as KiCad does" % (' '.join('%g' % a for a in tr[0]), ' '.join('%g' % o for o in tr[1])))+ ext = step_extents(text); span = footprint_span(fpt)+ if ext and span:+ up, miss, miss_z, d = up_axis(ext, span)+ if up != 2:+ what = 'its height is on %s, not Z (extents %s mm against a %.1f x %.1f mm courtyard)' % ('XYZ'[up], ' x '.join('%.2f' % x for x in d), span[0], span[1])+ if notes: # the footprint gave a transform and the result still lies wrong: say so, do not guess twice+ notes.append(what + ': CHECK this part in 3D; its footprint\'s (model ... (rotate ...)) entry may be wrong')+ else:+ text = make_upright(text, up)+ notes.append(what + ': turned upright and seated on the board')+ if not notes: return step, None+ out_dir.mkdir(parents=True, exist_ok=True)+ out = out_dir / (step.stem + '-zup.step'); out.write_text(text)+ return out, '; '.join(notes)+++ORIENT_RULES = 1 # bump when the orientation rules change: parts bound under older rules are bound again++ def silk_text(t): """Board text a small board can carry: each package's >NAME at 0.8 mm (thick enough for a fab's 0.15 mm line), and its >VALUE on tDocu (51), drawn in the editor but never printed. Fusion copies@@ -259,7 +407,7 @@ def cmd_library(a): if not want: err('no parts: give --parts parts.json ([{mpn, page?, footprint?}]) or a BOM .csv with an mpn column') cache = json.loads(Path(a.numbers).read_text()) if a.numbers else {} lib_dir = STATE / 'library'; (lib_dir / 'parts').mkdir(parents=True, exist_ok=True)- added, kept, unverified = [], [], {}+ added, kept, unverified, turned = [], [], {}, {} seen = set() for w in want: mpn = w['mpn']@@ -300,7 +448,13 @@ def cmd_library(a): missing = [k for k in ('mouser', 'digikey', 'lcsc') if not numbers.get(k)] stock = (meta['component'].get('manufacturer') or '').strip().lower() == 'adom' if missing and not stock: unverified[mpn] = missing + (['(' + '; '.join(sorted(set(notes))) + ')'] if notes else [])- st['parts'][mpn] = {**st['parts'].get(mpn, {}), 'lbr': str(out_lbr), 'step': str(step) if step else None, 'numbers': numbers, 'symbol': choice}+ onote = None+ if step:+ step, onote = upright_model(step, fp, STATE / 'models')+ if onote: turned[mpn] = onote+ prev = st['parts'].get(mpn, {})+ if prev.get('urn') and prev.get('orient') != ORIENT_RULES: prev = {k: v for k, v in prev.items() if k != 'urn'} # bound before the check: bind again+ st['parts'][mpn] = {**prev, 'lbr': str(out_lbr), 'step': str(step) if step else None, 'numbers': numbers, 'symbol': choice, 'orient': ORIENT_RULES, 'orientNote': onote} added.append(mpn) say('part %s: %s symbol, model %s, %s' % (mpn, choice, step.name if step else 'none', 'Adom stock' if stock else 'Mouser %s, DigiKey %s, LCSC %s' % (numbers.get('mouser', '-'), numbers.get('digikey', '-'), numbers.get('lcsc', '-')))) save_state(st)@@ -351,6 +505,11 @@ def cmd_library(a): ok('library %s: %d part(s) (%d added, %d kept), %d with real 3D, one library at %s' % (name, len(st['parts']), len(added), len(kept), len(urns), st['libraryPath']), ['No distributor number yet: ' + '; '.join('%s %s' % (m, ' '.join(v)) for m, v in unverified.items()) + '. Run `fusion library` again when the services are back, or add the numbers to the part\'s wiki page (component.distributor_links).' if unverified else '', '3D not bound yet: %s; run this again to retry them' % ', '.join(unbound) if unbound else '',+ ('3D turned upright before binding: ' + '; '.join('%s (%s)' % (m, n) for m, n in turned.items()) + '.') if turned else '',+ 'Check the 3D yourself before going on: a model can be authored Y-up while Fusion and the board are Z-up, and a part bound that way lies on its side (an electrolytic can on its flank, an inductor on edge). '+ '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.']) @@ -473,11 +632,19 @@ def cmd_schematic(a): 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),- ['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: %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)]) # ---------------------------------------------------------------- export