← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.71\ No newline at end of file+1.13.72\ No newline at end of file
CHANGELOG.md+4
@@ -3,6 +3,10 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.13.72 (2026-11-04)++`fusion_apply_silkscreen` moves and hides a part's name or value when the part is marked smashed but has no text of its own (#144, Glenn Jones). It trusted the flag, skipped SMASH, and MOVE and CHANGE then picked nothing.+ ## 1.13.71 (2026-11-04)  From Glenn Jones's reports on a 4-layer board:
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.13.71",+  "version": "1.13.72",   "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.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.13.71"+version = "1.13.72" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.71"+version = "1.13.72" edition = "2021"  [dependencies]
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.13.71"+DASH_VERSION="1.13.72" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.71",+  "version": "1.13.72",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+2−2
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.71",+  "version": "1.13.72",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -152,4 +152,4 @@   "confirmed_user_skills": [     "fusion-design-efficiency"   ]-}\ No newline at end of file+}
rust/Cargo.lock+5−5
@@ -19,7 +19,7 @@ dependencies = [  [[package]] name = "adom-fusion-bridge"-version = "1.13.71"+version = "1.13.72" dependencies = [  "adom-eda-checks",  "base64",@@ -299,7 +299,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.13.71"+version = "1.13.72" dependencies = [  "serde",  "serde_json",@@ -310,7 +310,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.13.71"+version = "1.13.72" dependencies = [  "regex",  "roxmltree",@@ -322,14 +322,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.13.71"+version = "1.13.72" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.13.71"+version = "1.13.72" dependencies = [  "fusion-core",  "windows-sys 0.59.0",
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"]  # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.13.71"+version = "1.13.72" edition = "2021" license = "MIT" 
rust/bridge/src/palette.rs+3−1
@@ -58,7 +58,9 @@ fn worker() {         let finished = FINISHED_SEQ.load(Ordering::Relaxed) == painted_seq;         let idle_for = if finished { Duration::from_secs(3) } else { Duration::from_secs(25) };         if shown && !HOLD.load(Ordering::Relaxed) && painted_at.elapsed() > idle_for && SEQ.load(Ordering::Relaxed) == painted_seq && !run_in_flight() {-            if send("hide", "", "", None, None)["success"].as_bool() == Some(true) { shown = false; *LAST.lock().unwrap() = None; }+            let r = send("hide", "", "", None, None);+            *LAST_RESULT.lock().unwrap() = Some(("hide".into(), String::new(), r["success"].as_bool() == Some(true), r["error"].as_str().unwrap_or("").chars().take(120).collect(), Instant::now()));+            if r["success"].as_bool() == Some(true) { shown = false; *LAST.lock().unwrap() = None; }             else { painted_at = Instant::now(); }         }     }
rust/bridge/src/routing_script.py+4−1
@@ -1050,7 +1050,10 @@ def apply_silkscreen(app, root, board, meta, args):     for _, item in want:         ref = identifier(item.get('ref'))         if ref not in els: fail('designator_not_found', 'No part %s on the board' % ref)-    to_smash = sorted({it['ref'] for kind, it in want if els[it['ref']].get('smashed') != 'yes'})+    # by the text itself: a part can say smashed="yes" and still have no NAME attribute of its own (a board from a+    # file, or part of one), and then MOVE and CHANGE pick nothing and the text stays where it is (#144)+    def has_own_text(el, kind): return any(a.get('name') == kind and a.get('x') is not None for a in el.findall('attribute'))+    to_smash = sorted({it['ref'] for kind, it in want if not has_own_text(els[it['ref']], kind)})     for ref in to_smash: run("SMASH '%s';" % ref)     if to_smash:         root, board, _ = snapshot(app, {**args, 'expectedRevision': None})
tests/glenn_drill.py+17−7
@@ -60,19 +60,30 @@ if want('142') or want('143'):     check('the zip has the inner layers', any(n.upper().endswith(('.G1', '.G2', '.GP1', '.GP2')) for n in names), names)     if want('143'):         print('--- #143 the panel')-        def panels():-            w = call('desktop_list_windows'); ws = (w.get('data') or w).get('windows') or []-            return [x.get('title') for x in ws if 'driving Fusion' in (x.get('title') or '') or 'ADOM' in (x.get('title') or '')]-        print('    right after the export:', panels()); time.sleep(10); left = panels(); check('no panel 10 s after the verb answered', not left, left)-        call('fusion_export_gerbers', {'outputDir': out + 'b'}, 300); call('fusion_export_gerbers', {'outputDir': out + 'c'}, 300); time.sleep(10); left = panels(); check('nor after two exports in a row', not left, left)+        PV = "import adsk.core\\nui=adsk.core.Application.get().userInterface\\np=ui.palettes.itemById('AdomActivityPalette')\\nresult=bool(p and p.isVisible)"+        call('fusion_export_gerbers', {'outputDir': out + 'b'}, 300); call('fusion_export_gerbers', {'outputDir': out + 'c'}, 300)+        print('    right after two exports in a row:', script(PV)[0]); time.sleep(8); check('panel gone 8 s after the verb answered', script(PV)[0] is False, script(PV))     closeall()     doc = board('GGerb2'); r = call('fusion_export_gerbers', {'outputDir': D + '/g2'}, 300); d = r.get('data') or r; check('a 2-layer board still exports 2', r.get('success') and d.get('layerCount') == 2, {k: d.get(k) for k in ('layerCount', 'error')}); closeall()  if want('141'):     print('--- #141 a document with no tab')-    print('   ', script("import adsk.core\nd=app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType,False)\nresult={'visible':d.isVisible,'n':app.documents.count}"))+    print('   ', script("""+import adsk.core+hit=[]+def walk(fol,depth):+    for k in range(fol.dataFiles.count):+        q=fol.dataFiles.item(k)+        if q.fileExtension=='f3d' and not hit: hit.append(q)+    if depth<2 and not hit:+        for j in range(min(fol.dataFolders.count,6)): walk(fol.dataFolders.item(j),depth+1)+walk(app.data.activeProject.rootFolder,0)+z=app.documents.open(hit[0],False) if hit else None+result={'hidden':z.name if z else None,'visible':z.isVisible if z else None}+"""))     doc = board('GTab'); time.sleep(2)     print('   ', script("result=[(app.documents.item(i).name,app.documents.item(i).isVisible) for i in range(app.documents.count)]"))+    print('   ', script("i=[k for k in range(app.documents.count) if app.documents.item(k).name=='%s'][0]\nvis=sum(1 for k in range(i) if app.documents.item(k).isVisible)\napp.executeTextCommand('UI.SwitchToTab %%d'%%i)\nold=app.activeDocument.name\napp.executeTextCommand('UI.SwitchToTab %%d'%%vis)\nresult={'oldIndexLandsOn':old,'fixLandsOn':app.activeDocument.name}" % doc))     r = st(doc); check('routing_state reads the right board', r.get('success') is not False and r.get('documentName') == doc and len(r.get('elements') or []) == 2, {k: r.get(k) for k in ('errorCode', 'error', 'documentName')})     r = call('fusion_board_source', {'expectDocument': doc}); check('board_source too', r.get('success') is not False, r.get('error'))     print('   ', script("import adsk.core\nfor i in range(app.documents.count):\n    d=app.documents.item(i)\n    if not d.isVisible:\n        d.close(False)\n        break\nresult=app.documents.count")); closeall()@@ -89,7 +100,6 @@ if want('145'):     v = vz(); check('filter changes both, leaves the third', r.get('verified') and v[(5.0, 5.0)][0] == 0.35 and v[(10.0, 5.0)][0] == 0.35 and v[(15.0, 5.0)][0] == 0.4, (r.get('errorCode'), r.get('error'), v))     s = st(doc); r = call('fusion_change_vias', {'expectDocument': doc, 'boardId': s.get('boardId'), 'expectedRevision': s.get('revision'), 'vias': [{'x': 5, 'y': 5, 'diameter': 0.9}]})     v = vz(); check('by position, on a track end, changes the via', r.get('verified') and v[(5.0, 5.0)][1] == 0.9 and v[(10.0, 5.0)][1] == 0.7, (r.get('errorCode'), r.get('error'), v)); check('tracks untouched', len(st(doc).get('tracks') or []) == tr0, tr0)-    vis = script("import xml\nresult=1")     s = st(doc); r = call('fusion_change_vias', {'expectDocument': doc, 'boardId': s.get('boardId'), 'expectedRevision': s.get('revision'), 'vias': [{'x': 40, 'y': 40, 'drill': 0.3}]}); check('no via there: nothing changes', r.get('errorCode') == 'nothing_to_do', r.get('errorCode')); nodialog('after the vias'); closeall()  if want('144'):
tests/test_routing.py+19
@@ -389,6 +389,25 @@ class ChangeViasTests(unittest.TestCase):         self.assertEqual('bad_args', self.call(filter={'drill': 0.2})['errorCode'])  +class SilkSmashTests(unittest.TestCase):+    """#144: a part marked smashed with no text of its own still needs SMASH, or MOVE picks nothing."""+    def test_smashes_by_the_text_not_the_flag(self):+        root = ET.parse(ROOT/'demo/routing/fixture/fusion-routing.brd').getroot(); b = root.find('drawing/board')+        a, c = b.findall('elements/element')[:2]+        a.set('smashed', 'yes')                                                                      # the flag, no text+        c.set('smashed', 'yes'); ET.SubElement(c, 'attribute', name='NAME', x='1', y='1', size='1', layer='25')   # both+        sent = []+        class App:+            def executeTextCommand(self, cmd): sent.append(cmd)+        saved = r.snapshot; r.snapshot = lambda app, args: (root, b, {'boardId': 'b', 'revision': 'r'})+        try:+            r.apply_silkscreen(App(), root, b, {'boardId': 'b', 'revision': 'r'}, {'keepPartial': True, 'names': [{'ref': a.get('name'), 'x': 1, 'y': 2}, {'ref': c.get('name'), 'x': 1, 'y': 2}]})+        finally:+            r.snapshot = saved+        self.assertIn("Electron.run SMASH '%s';" % a.get('name'), sent)+        self.assertNotIn("Electron.run SMASH '%s';" % c.get('name'), sent)++ class SilkUndoTests(unittest.TestCase):     """#144: a failed batch is really undone, new free texts included, or says what's left."""     def setUp(self):