← Commit history

v1.9.20: make_test_project + screenshot_fusion catalog

John Lauer ·1c41dae614 ·1mo ago ·parent 823e613
6 files changed +194−9
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.19\ No newline at end of file+1.9.20\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.19",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.19/adom-bridge-fusion-v1.9.19.zip",-    "sha256": "88cc7d1c6bdb87b84a5e4f51672221a3eee2ae83f5162d86dcbc107f7ccafe95",-    "size": 361294,+    "version": "1.9.20",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.20/adom-bridge-fusion-v1.9.20.zip",+    "sha256": "ad0886d59240463f882dca229661e046b5d274052cf5e4f0d0e9f8fd5594d442",+    "size": 364613,     "verbPrefixes": [         "fusion_"     ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.19",+  "version": "1.9.20",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
describe.py+2
@@ -202,6 +202,8 @@ _T = [     ("fusion_show_3d_package", "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.", {"name": "str", "libraryPath": "optional str (.lbr)", "packageXml": "optional str (bare <package> fragment)", "stepPath": "optional str (WINDOWS path .step to attach)", "foreground": "optional bool - user's choice to WATCH; default background"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str"}, 300, None, True, {"name": "MYCHIP", "stepPath": "C:/Temp/chip.step"}),     ("fusion_demo_progress", "Poll a running fusion_demo tour: {percent, elapsedSec, estimatedSec, step, stepLabel, done, failed, shots}. Poll about once a second and render a REAL progress bar. When done:true the shots array carries base64 images (pass imagesAs:paths to skip the encoding).", {"demoId": "required str (from fusion_demo mode:tour; omitted = the single running tour)", "imagesAs": "optional str paths|base64|both"}, {"percent": "int", "step": "str", "stepLabel": "str", "done": "bool", "shots": "list"}, 15, None, False, {"demoId": "tour-abc123"}), +    ("fusion_make_test_project", "Create a NEW electronics test project FROM SCRATCH around a library part - the Web Control demo's PROJECT button, and the answer to 'a customer only has a component page'. Generates a minimal test circuit server-side (the part + a generated 330R series resistor with VCC/GND stubs on a small board) as real EAGLE .sch/.brd, then imports the pair as a new electronics design. NO cloud, NO sign-in, no pre-existing content. After it, fusion_show_schematic / fusion_show_2d_board / fusion_show_3d_board all work on the result. Wiring uses the deviceset's own connects (first two pins), so 2-terminal parts get a real circuit and big ICs get a structurally-valid integration proof.", {"libraryPath": "required str (WINDOWS path .lbr; stage from container first)", "deviceset": "optional str (defaults to the library's first deviceset)"}, {"success": "bool", "schPath": "str", "brdPath": "str", "circuit": "str"}, 300, None, True, {"libraryPath": "C:/Temp/IN-S42ATR.lbr"}),+    ("fusion_screenshot_fusion", "Background PrintWindow capture of Fusion's MAIN window (largest same-title window; works occluded/bottom-of-z-order, never raises anything) or a specific dialog by hwnd. Returns savedTo (PNG/WebP on the box; pull with pull_file). (Live since early versions but undescribed until 2026-08-15 - it was invisible to callers AND to the drift check.)", {"hwnd": "optional int (a dialog; default the main window)"}, {"success": "bool", "savedTo": "str", "sizeKB": "int"}, 60, None, False, {}),     ("fusion_export_iges", "Export to IGES.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/board.iges"}),     ("fusion_export_stl", "Export to STL mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.stl"}),     ("fusion_export_3mf", "Export to 3MF mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.3mf"}),
server.py+176−3
@@ -2071,7 +2071,7 @@ ADDIN_COMMAND_MAP = { # timeout in http_server.py should be the source of truth; these are # matched to that so urllib doesn't cut off before the add-in does. ADDIN_COMMAND_TIMEOUTS = {-    "show_symbol": 180, "show_footprint": 180, "show_3d_package": 300,+    "show_symbol": 180, "show_footprint": 180, "show_3d_package": 300, "make_test_project": 300,     "demo_progress": 15,     # getPhysicalProperties runs on Fusion's MAIN thread per component, so a big assembly     # (or assembly_bom with includePhysicalProperties) is slow. Give both real headroom.@@ -5860,6 +5860,177 @@ def _glb_export_async_status(args: dict) -> dict:     return out  +++# ── fusion_make_test_project (John, 2026-08-15): the Web Control demo's PROJECT button.+# "None of our other adom user customers will even have a BQ25792" - a customer's session+# has exactly ONE thing: a component page's .lbr. This verb closes the circle from it:+# it GENERATES a minimal test circuit (the part + a generated series resistor on a small+# board) as real EAGLE .sch/.brd server-side, then imports the pair as a NEW electronics+# design. No cloud, no sign-in, no pre-existing content. The schematic/2D/3D buttons then+# work on the result.++def _mtp_extract(lbr_text, deviceset):+    """Pull the pieces the generator needs out of the .lbr."""+    def block_of(tag, name):+        m = re.search(r'<%s name="%s"[\s>].*?</%s>' % (tag, re.escape(name), tag), lbr_text, re.S)+        return m.group(0) if m else None+    ds = block_of("deviceset", deviceset)+    if not ds:+        return None, "deviceset %r not found in the library" % deviceset+    gate = re.search(r'<gate name="([^"]+)" symbol="([^"]+)"', ds)+    dev = re.search(r'<device name="[^"]*" package="([^"]+)"', ds)+    if not (gate and dev):+        return None, "deviceset %r has no gate/package device mapping" % deviceset+    connects = re.findall(r'<connect gate="[^"]+" pin="([^"]+)" pad="([^"]+)"', ds)+    sym = block_of("symbol", gate.group(2))+    pkg = block_of("package", dev.group(1))+    if not (sym and pkg and connects):+        return None, "library is missing the symbol/package/connects for %r" % deviceset+    layers = re.search(r'<layers>.*?</layers>', lbr_text, re.S)+    return {"deviceset": ds, "symbol": sym, "package": pkg, "connects": connects,+            "symName": gate.group(2), "gateName": gate.group(1), "pkgName": dev.group(1),+            "layers": layers.group(0) if layers else ""}, None+++_MTP_R_SYM = ('<symbol name="ADOMR"><wire x1="-2.54" y1="1.016" x2="2.54" y2="1.016" width="0.254" layer="94"/>'+ '<wire x1="2.54" y1="1.016" x2="2.54" y2="-1.016" width="0.254" layer="94"/>'+ '<wire x1="2.54" y1="-1.016" x2="-2.54" y2="-1.016" width="0.254" layer="94"/>'+ '<wire x1="-2.54" y1="-1.016" x2="-2.54" y2="1.016" width="0.254" layer="94"/>'+ '<pin name="1" x="-5.08" y="0" visible="off" length="short"/>'+ '<pin name="2" x="5.08" y="0" visible="off" length="short" rot="R180"/>'+ '<text x="-2.54" y="1.524" size="1.778" layer="95">&gt;NAME</text>'+ '<text x="-2.54" y="-3.302" size="1.778" layer="96">&gt;VALUE</text></symbol>')+_MTP_R_PKG = ('<package name="ADOMR0603"><description>Resistor 0603</description>'+ '<smd name="1" x="-0.85" y="0" dx="1" dy="1" layer="1"/>'+ '<smd name="2" x="0.85" y="0" dx="1" dy="1" layer="1"/>'+ '<wire x1="-0.4" y1="0.5" x2="0.4" y2="0.5" width="0.127" layer="21"/>'+ '<wire x1="-0.4" y1="-0.5" x2="0.4" y2="-0.5" width="0.127" layer="21"/>'+ '<text x="-1" y="0.8" size="0.8" layer="25">&gt;NAME</text></package>')+_MTP_R_DS = ('<deviceset name="ADOMR" prefix="R"><description>generated test resistor</description>'+ '<gates><gate name="G$1" symbol="ADOMR" x="0" y="0"/></gates>'+ '<devices><device name="" package="ADOMR0603">'+ '<connects><connect gate="G$1" pin="1" pad="1"/><connect gate="G$1" pin="2" pad="2"/></connects>'+ '<technologies><technology name=""/></technologies></device></devices></deviceset>')+_MTP_SCH_LAYERS = ('<layers>'+ '<layer number="91" name="Nets" color="2" fill="1" visible="yes" active="yes"/>'+ '<layer number="92" name="Busses" color="1" fill="1" visible="yes" active="yes"/>'+ '<layer number="93" name="Pins" color="2" fill="1" visible="no" active="yes"/>'+ '<layer number="94" name="Symbols" color="4" fill="1" visible="yes" active="yes"/>'+ '<layer number="95" name="Names" color="7" fill="1" visible="yes" active="yes"/>'+ '<layer number="96" name="Values" color="7" fill="1" visible="yes" active="yes"/>'+ '<layer number="97" name="Info" color="7" fill="1" visible="yes" active="yes"/>'+ '<layer number="98" name="Guide" color="6" fill="1" visible="yes" active="yes"/></layers>')+_MTP_HEAD = ('<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE eagle SYSTEM "eagle.dtd">\n'+ '<eagle version="9.6.2">\n<drawing>\n<settings><setting alwaysvectorfont="no"/></settings>'+ '<grid distance="0.1" unitdist="inch" unit="inch" style="lines" multiple="1" display="no" '+ 'altdistance="0.01" altunitdist="inch" altunit="inch"/>')+++def _mtp_generate(lib_name, ex, part_name):+    """Build the .sch/.brd pair text. Wiring: R1.2 -> the part's FIRST connected pin;+    GND -> its second; VCC -> R1.1. Electrically meaningful for 2-terminal parts,+    structurally valid for anything (the test project proves design integration)."""+    pins = [p for p, _ in ex["connects"]]+    pads = {p: pad for p, pad in ex["connects"]}+    p1, p2 = pins[0], (pins[1] if len(pins) > 1 else pins[0])+    lib = ('<library name="%s"><packages>%s</packages><symbols>%s</symbols><devicesets>%s</devicesets></library>'+           % (lib_name, ex["package"], ex["symbol"], ex["deviceset"]))+    rlib = ('<library name="adomdemo"><packages>%s</packages><symbols>%s</symbols><devicesets>%s</devicesets></library>'+            % (_MTP_R_PKG, _MTP_R_SYM, _MTP_R_DS))+    g = ex["gateName"]+    sch = (_MTP_HEAD + _MTP_SCH_LAYERS ++     '<schematic xreflabel="%F%N/%S.%C%R" xrefpart="/%S.%C%R">'+     '<libraries>' + lib + rlib + '</libraries>'+     '<attributes/><variantdefs/><classes><class number="0" name="default" width="0" drill="0"/></classes>'+     '<parts>'+     '<part name="U1" library="' + lib_name + '" deviceset="' + part_name + '" device=""/>'+     '<part name="R1" library="adomdemo" deviceset="ADOMR" device="" value="330"/>'+     '</parts><sheets><sheet><plain>'+     '<text x="0" y="30.48" size="2.54" layer="97">Adom generated test circuit: ' + part_name + '</text>'+     '</plain><instances>'+     '<instance part="R1" gate="G$1" x="12.7" y="12.7"/>'+     '<instance part="U1" gate="' + g + '" x="38.1" y="12.7"/>'+     '</instances><busses/><nets>'+     '<net name="N1" class="0"><segment>'+     '<wire x1="17.78" y1="12.7" x2="25.4" y2="12.7" width="0.1524" layer="91"/>'+     '<pinref part="R1" gate="G$1" pin="2"/><pinref part="U1" gate="' + g + '" pin="' + p1 + '"/>'+     '</segment></net>'+     '<net name="VCC" class="0"><segment>'+     '<wire x1="7.62" y1="12.7" x2="2.54" y2="12.7" width="0.1524" layer="91"/>'+     '<pinref part="R1" gate="G$1" pin="1"/></segment></net>'+     '<net name="GND" class="0"><segment>'+     '<pinref part="U1" gate="' + g + '" pin="' + p2 + '"/></segment></net>'+     '</nets></sheet></sheets></schematic></drawing></eagle>\n')+    brd = (_MTP_HEAD + ex["layers"] ++     '<board><plain>'+     '<wire x1="0" y1="0" x2="14" y2="0" width="0.2" layer="20"/>'+     '<wire x1="14" y1="0" x2="14" y2="9" width="0.2" layer="20"/>'+     '<wire x1="14" y1="9" x2="0" y2="9" width="0.2" layer="20"/>'+     '<wire x1="0" y1="9" x2="0" y2="0" width="0.2" layer="20"/></plain>'+     '<libraries><library name="' + lib_name + '"><packages>' + ex["package"] + '</packages></library>'+     '<library name="adomdemo"><packages>' + _MTP_R_PKG + '</packages></library></libraries>'+     '<attributes/><variantdefs/><classes><class number="0" name="default" width="0" drill="0"/></classes>'+     '<designrules name="default"/><autorouter/>'+     '<elements>'+     '<element name="R1" library="adomdemo" package="ADOMR0603" value="330" x="3.5" y="4.5"/>'+     '<element name="U1" library="' + lib_name + '" package="' + ex["pkgName"] + '" value="" x="9.5" y="4.5"/>'+     '</elements><signals>'+     '<signal name="N1"><contactref element="R1" pad="2"/><contactref element="U1" pad="' + pads[p1] + '"/>'+     '<wire x1="4.35" y1="4.5" x2="8.5" y2="4.5" width="0.3" layer="1"/></signal>'+     '<signal name="VCC"><contactref element="R1" pad="1"/></signal>'+     '<signal name="GND"><contactref element="U1" pad="' + pads[p2] + '"/></signal>'+     '</signals></board></drawing></eagle>\n')+    return sch, brd+++def _orchestrate_make_test_project(args: dict) -> dict:+    """Generate + import a minimal test project around a library part.++    args: libraryPath (required, Windows path .lbr), deviceset (optional: defaults to+    the library's first deviceset).+    """+    import os+    import tempfile+    lbr_path = (args.get("libraryPath") or "").replace("\\", "/")+    if not lbr_path:+        return {"success": False, "error": "libraryPath (.lbr, Windows path) required.",+                "_hint": "Stage the component page's .lbr onto the box first (send_files)."}+    try:+        lbr_text = open(lbr_path, encoding="utf-8", errors="replace").read()+    except Exception as e:+        return {"success": False, "error": "could not read %s: %s" % (lbr_path, e)}+    deviceset = args.get("deviceset")+    if not deviceset:+        m = re.search(r'<deviceset name="([^"]+)"', lbr_text)+        if not m:+            return {"success": False, "error": "no deviceset found in the library"}+        deviceset = m.group(1)+    ex, err = _mtp_extract(lbr_text, deviceset)+    if err:+        return {"success": False, "error": err}+    lib_name = re.sub(r"[^A-Za-z0-9_-]", "_", os.path.basename(lbr_path).rsplit(".", 1)[0])+    sch_text, brd_text = _mtp_generate(lib_name, ex, deviceset)+    d = os.path.join(tempfile.gettempdir(), "adom-fusion-wrap")+    os.makedirs(d, exist_ok=True)+    base = "ADOM-TEST-" + re.sub(r"[^A-Za-z0-9_-]", "_", deviceset)[:40]+    sch_path = os.path.join(d, base + ".sch").replace("\\", "/")+    brd_path = os.path.join(d, base + ".brd").replace("\\", "/")+    open(sch_path, "w", encoding="utf-8").write(sch_text)+    open(brd_path, "w", encoding="utf-8").write(brd_text)+    res = _handle_new_electronics_from_eagle({"schPath": sch_path, "brdPath": brd_path})+    out = {"success": bool(res.get("success")), "schPath": sch_path, "brdPath": brd_path,+           "deviceset": deviceset, "circuit": "R1 (330, generated 0603) in series with %s; VCC/GND stubs" % deviceset}+    if not res.get("success"):+        out["error"] = "import failed: " + str(res.get("error"))[:200]+        out["_hint"] = str(res.get("_hint") or "")[:200]+        return out+    out["_hint"] = ("Test project imported as a NEW electronics design. Now the whole chain works: "+                    "fusion_show_schematic -> fusion_show_2d_board -> fusion_show_3d_board. "+                    "Generated from NOTHING but the component's .lbr - no cloud, no sign-in.")+    return out++ def _orchestrate_demo(args: dict) -> dict:     """FIRST-TIME-USER DEMO: take a brand-new user from nothing to "wow" in one verb. @@ -6302,7 +6473,7 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) ->         "attach_3d_package", "make_3d_package", "build_library_3d",         "capture_library_views", "generate_package", "export_optimized_glb",         "board_stackup", "save_to_cloud", "start",-        "show_symbol", "show_footprint", "show_3d_package",+        "show_symbol", "show_footprint", "show_3d_package", "make_test_project",     }     if command in _WINDOW_RAISING:         if (args or {}).get("foreground"):@@ -6323,7 +6494,7 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) ->     # The user should launch Fusion themselves; we just report the status.     # Commands that need Fusion running (add-in commands + orchestrated commands)     _OPEN_WITH_SCREENSHOT = {"open_schematic", "open_board", "show_3d_board", "show_2d_board", "show_schematic", "import_electronics"}-    if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views", "cleanup_cloud_files", "generate_package", "export_optimized_glb", "board_stackup", "show_symbol", "show_footprint", "show_3d_package") or command in _OPEN_WITH_SCREENSHOT:+    if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views", "cleanup_cloud_files", "generate_package", "export_optimized_glb", "board_stackup", "show_symbol", "show_footprint", "show_3d_package", "make_test_project") or command in _OPEN_WITH_SCREENSHOT:         # Issue #326: Fusion may be installed under a different Windows user profile than the current         # one, so file-path detection fails even though Fusion is running. Check if it's actually         # running (via tasklist) as a reliable fallback. If running, it's installed somewhere.@@ -6433,6 +6604,8 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) ->         return _merge_dialog_array(_orchestrate_show_footprint(args))     if command == "show_3d_package":         return _merge_dialog_array(_orchestrate_show_3d_package(args))+    if command == "make_test_project":+        return _merge_dialog_array(_orchestrate_make_test_project(args))     if command == "save_lbr":         return _merge_dialog_array(_orchestrate_save_lbr(args))     if command == "attach_3d_package":
tests/run_verb_tests.py+10
@@ -228,6 +228,9 @@ t("fusion_new_electronics_from_eagle", "manual", note="drives native dialogs") # ── stage 6: doc-creating flows LAST (they change the active document) ────── t("fusion_import_step", "weekly", {"_dynamic": "staged_step"}, {"ok": True},   "creates a NEW design doc - must run after every active-doc-dependent test")+t("fusion_make_test_project", "weekly", {"_dynamic": "staged_lbr_mtp"}, {"ok": True, "fields": ["schPath", "brdPath"]},+  "from-scratch project generation around the staged sample part (John's no-BQ25792 rule)")+t("fusion_screenshot_fusion", "daily", {}, {"ok": True, "fields": ["savedTo"]}, "background main-window capture") t("fusion_demo#tour", "weekly", {"mode": "tour", "includeDesign": False, "_pollTour": 300}, {"ok": True},   "the six-content-type showcase (issue #29): async start + progress polls until done") t("fusion_demo_progress#unknown", "negative", {"demoId": "tour-nope"}, {"pred": "must_refuse"})@@ -325,6 +328,13 @@ def stage_dynamic(target, kind):     write_file-in-argv blew Errno 7 on the 245KB STEP, 2026-08-14).     """     try:+        if kind == "staged_lbr_mtp":+            fname = "ADS8588SIPM.fusion.lbr"+            src = str(REPO / "examples" / fname)+            r, _ = call(target, "send_files", {"filePaths": [src], "dest": SCRATCH}, timeout=180)+            if not (r.get("success") or r.get("status") == "ok"):+                return None, "send_files: " + str(r.get("error"))[:80]+            return {"libraryPath": SCRATCH + "/" + fname}, None         if kind in ("staged_step", "staged_lbr"):             fname = "ADS8588SIPM.step" if kind == "staged_step" else "ADS8588SIPM.fusion.lbr"             src = str(REPO / "examples" / fname)