← All Pull Requests

AI Flow 0.2.46: fusion schematic reuses an empty Untitled design (#133) #61

Merged Released in 0.2.46 on 3 Nov 2026 opened by Colby Knox 2026-11-03
Merges aiflow-lane-0.2.46 → main

Closes #133.

fusion schematic starts from an empty Untitled electronics design if one is open, instead of adding another each run. Reuse requires never saved, unmodified, and no schematic or board under that name; anything else gets a new document, as before. Also applies to the 3D restore's import.

Base: 603eb560. Tested: 540 Rust tests and the Python suites pass, with 2 new fake-document tests of the blank check; no CLI command is missing against 0.2.45. Live on bridge 1.13.61: 107 parts with an 80-part block drew as 15 groups with no Fusion errors and every pin on its net, widest position 1194 mm, on 0.2.45. A 5-part run without this change added an Untitled; one with it reused one and added none.

🤖 Generated with Claude Code

Diff Skip to comments

Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.2.45"+version = "0.2.46" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.2.45"+version = "0.2.46" dependencies = [  "aiflow-board",  "serde",@@ -35,7 +35,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.2.45"+version = "0.2.46" dependencies = [  "roxmltree",  "serde",@@ -44,7 +44,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.2.45"+version = "0.2.46" dependencies = [  "aiflow-board",  "serde",@@ -53,7 +53,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.2.45"+version = "0.2.46" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.2.45"+version = "0.2.46" dependencies = [  "aiflow-board",  "serde",@@ -72,7 +72,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.2.45"+version = "0.2.46" dependencies = [  "aiflow-board",  "serde",@@ -81,7 +81,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.2.45"+version = "0.2.46" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.2.45"+version = "0.2.46" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.2.45"+version = "0.2.46" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.2.45"+version = "0.2.46" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
docs/fusion-lane.md+1−1
@@ -90,7 +90,7 @@ or `adom-aiflow fusion view3d`, and every part must sit on its pads as its datas  Fusion 2706 raises its internal error `PlacePartWorker.cpp, Line 23. Condition: assetAdded` on every ADD from a `.lbr` whose XML elements share one line, even for parts with no 3D package (fusion-bridge#135, found by fusion-lane on 2026-10-31). The same library broken into lines adds cleanly. `fusion library` therefore writes the project library with a line break between elements. Only whitespace changes, and only between elements: never inside an element's own text, never inside an attribute value. Fusion bridge 1.13.47+ also rewrites a compact library before loading it, so either side alone is enough. -`fusion schematic` plans the whole sheet before it opens a document. A block too wide or too long for one frame is drawn as several (`<block> 1 of 4`), in netlist order, so it stays inside Fusion's 1950 mm limit and each group stays under the bridge's command limit. Anything still past the limit, such as a position in your `--layout`, stops there in words, before a document opens.+`fusion schematic` plans the whole sheet before it opens a document. It starts from an empty Untitled electronics design if one is already open, instead of adding another; one that was changed, saved or has a schematic or board is left alone. A block too wide or too long for one frame is drawn as several (`<block> 1 of 4`), in netlist order, so it stays inside Fusion's 1950 mm limit and each group stays under the bridge's command limit. Anything still past the limit, such as a position in your `--layout`, stops there in words, before a document opens.  `fusion schematic` keeps every bridge reply whole in `<run>/fusion/schematic/`: the import's `runId`, start reply and final status (`import.json`), and each `fusion_schematic_run` group (`run-NN-<group>.json`). An error names its file. `add_failed` and an `assetAdded` assertion each come with this cause and the fix in words. 
docs/release-0.2.46.mdadded+8
@@ -0,0 +1,8 @@+# AI Flow 0.2.46 (2026-11-03)++Fusion lane (#133):++- **`fusion schematic` no longer adds an Untitled document each run.** The import starts from an empty electronics design, and the real design lands in its own document, so the empty one stayed open every time. A run now starts from an empty Untitled electronics design if one is open. One that's been changed, saved or has a schematic or board is left alone, and a new one is opened instead.+- **The sheet-limit fix from 0.2.45 is now proven live.**++Checked live on Fusion with bridge 1.13.61: 107 parts with an 80-part block drew as 15 groups, with no Fusion errors, every pin on its net and every part on the board, widest position 1194 mm, in about 5.5 minutes. A second run without this change added an Untitled; a third with it reused one and added none.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.45",+  "version": "0.2.46",   "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, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); 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+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.45",+  "version": "0.2.46",   "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, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); 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.",
tools/fusion-project.py+32−2
@@ -1073,6 +1073,30 @@ GROUP_MAX_COMMANDS = 380 FRAME_COMMANDS = 10  +# An electronics design that was never saved or changed and has no schematic or board open under its name. Anything+# else named Untitled may be someone's work and stays as it is.+BLANK_DESIGN = '''import adsk.core+app = adsk.core.Application.get()+docs = [app.documents.item(i) for i in range(app.documents.count)]+result = {'reused': False}+for d in docs:+    if not d.name.startswith('Untitled') or d.dataFile is not None or d.isModified or d.isSaved: continue+    if [p.productType for p in d.products] != ['EcadDesignProductType']: continue+    kin = [x for x in docs if x.name == d.name and x.classType() != d.classType() and 'electron' in x.classType()]+    if kin or any(x.isModified for x in docs if x.name == d.name and 'electron' in x.classType()): continue+    d.activate()+    result = {'reused': True}+    break+'''+++def reuse_blank_design():+    """Bring an empty electronics design to the front if one is open. False (open a new one) when none is, or on any doubt."""+    r = ab('fusion_run_modeling_script', {'script': BLANK_DESIGN})+    res = (r.get('data') or r).get('result')+    return isinstance(res, dict) and res.get('reused') is True++ def symbol_sizes(lib):     """{DEVICESET: (symbol width mm, pins)}, measured the way fusion_schematic_plan does (its first gate's symbol)."""     syms = {s.get('name'): s for s in lib.iter('symbol')}@@ -1175,7 +1199,10 @@ def cmd_schematic(a):     say('starting the design with the fab rules (clearance %.2f mm, edge %.2f mm, 0.15 mm drill) and %d fixed part(s)' % (rules['clearance'], rules['edge'], len(fixed)))     r = ab('fusion_board_seed', {'libraryPath': st['libraryPath'], 'outDir': bdir, 'name': 'Seed', 'title': nl.get('title', name), 'outlineMm': fx['outlineMm'], 'fixed': fixed, 'rules': rules})     if not r.get('success'): err('seed: %s' % r.get('error'), [r.get('_hint')])-    ab('fusion_execute_text_command', {'command': 'Commands.Start NewElectronDesignDocumentCommand'})+    # the import starts from an empty electronics design and lands in its own document, so that one stays open and+    # empty: use one that's already there instead of adding another Untitled each run (issue #133)+    if reuse_blank_design(): say('starting from the empty Untitled electronics design already open')+    else: ab('fusion_execute_text_command', {'command': 'Commands.Start NewElectronDesignDocumentCommand'})     for _ in range(20):  # the import starts from an electronics design that has settled         time.sleep(2)         st_ = ab('fusion_get_app_state', {}); d_ = st_.get('data') or st_@@ -1320,7 +1347,10 @@ def restore_3d(st, name, project, folder, by_ref=None):     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'})+    # the import starts from an empty electronics design and lands in its own document, so that one stays open and+    # empty: use one that's already there instead of adding another Untitled each run (issue #133)+    if reuse_blank_design(): say('starting from the empty Untitled electronics design already open')+    else: 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_
tools/test_fusion_project.py+30−1
@@ -1,5 +1,5 @@ """Unit tests for tools/fusion-project.py helpers (no Fusion, no bridge): python3 -m unittest tools/test_fusion_project.py"""-import importlib.util, os, re, sys, tempfile, unittest+import importlib.util, os, re, sys, tempfile, unittest, unittest.mock import xml.etree.ElementTree as ET from pathlib import Path @@ -275,6 +275,35 @@ class SharedFootprint3D(unittest.TestCase):         self.assertEqual(fp.library_3d_problems(ET.tostring(sch, encoding='unicode')), [])  +class BlankDesign(unittest.TestCase):+    """adom-aiflow#133: only a truly empty electronics design is reused, run against fake documents."""+    class Doc:+        def __init__(self, name, cls='EcadDesignDocument', ptype='EcadDesignProductType', cloud=False, mod=False, saved=False):+            self.name, self.cls, self.dataFile, self.isModified, self.isSaved = name, ('adsk::fusion::' if cls == 'FusionDocument' else 'adsk::electron::') + cls, (object() if cloud else None), mod, saved+            self.products = [type('P', (), {'productType': ptype})()]; self.activated = False+        def classType(self): return self.cls+        def activate(self): self.activated = True++    def run_with(self, docs):+        app = type('A', (), {'documents': type('D', (), {'count': len(docs), 'item': lambda s, i: docs[i]})()})()+        core = type('C', (), {'Application': type('X', (), {'get': staticmethod(lambda: app)})})+        adsk = type(sys)('adsk'); adsk.core = core+        with unittest.mock.patch.dict(sys.modules, {'adsk': adsk, 'adsk.core': core}):+            ns = {}; exec(fp.BLANK_DESIGN, ns)+        return ns['result']['reused'], [d for d in docs if d.activated]++    def test_an_empty_untitled_electronics_design_is_reused(self):+        d = self.Doc('Untitled')+        self.assertEqual(self.run_with([self.Doc('Untitled', 'FusionDocument', 'DesignProductType'), d]), (True, [d]))++    def test_anything_that_may_be_work_is_left_alone(self):+        for docs in ([self.Doc('Untitled', mod=True)], [self.Doc('Untitled', saved=True)], [self.Doc('MyBoard')], [self.Doc('Untitled', cloud=True)],+                     [self.Doc('Untitled', 'FusionDocument', 'DesignProductType')],+                     [self.Doc('Untitled'), self.Doc('Untitled', 'SchematicDocument', 'SchematicProductType')],+                     [self.Doc('Untitled'), self.Doc('Untitled', 'BoardDocument', 'BoardProductType', mod=True)]):+            self.assertEqual(self.run_with(docs), (False, []), [d.name + d.cls for d in docs])++ class SheetLimit(unittest.TestCase):     """adom-aiflow#133: a big block stays inside Fusion's sheet."""     SIZES = {'R': (15.0, 2), 'BIG': (60.0, 150)}

Comments

No comments yet.

Log in to comment.