← Commit history
CHANGELOG.md+10
@@ -5,6 +5,16 @@ Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/  ## 1.13.28 (unreleased) +- Three standard verbs that were not_supported now work (eda-bridge-standard#29, every verb):+  - `fusion_plate_vias`: each unplated `<hole>` in a .lbr's footprints becomes a plated pad. One inside an SMD land (a+    heatsink via in an exposed pad) is named N@k after the land and joined to its connects, so it is on the land's+    net; any other is H<k>. Offline, with a .bak and the preflight.+  - `fusion_cleanup_libs`: a .lbr's empty leftovers removed (components with no device, and footprints and symbols+    with nothing drawn that nothing uses).+  - `fusion_sch_update_from_library`, the plan: each placed part's component as the schematic holds it against the+    same-named deviceset in the .lbr files named (gates, pins added/removed/changed, symbol drawing, attributes),+    and the wired pins a new symbol would drop. apply is refused with the plan (Fusion's UPDATE asks per part).+ - `fusion_restart_for_addin` no longer meets Fusion's Unsaved changes box when the add-in cannot save (issue #129).   A save whose add-in connection dropped is retried once after 5 s (the add-in can be unreachable for a moment   right after `bridge_install`). If the add-in still answers its status probe but the save failed, the restart
bridge.json+3
@@ -78,6 +78,7 @@     "fusion_check_recovery",     "fusion_claim_seat",     "fusion_cleanup_cloud_files",+    "fusion_cleanup_libs",     "fusion_clone_symbol",     "fusion_close_all_documents",     "fusion_close_document",@@ -235,6 +236,7 @@     "fusion_plan_fanout",     "fusion_plan_library_import",     "fusion_plan_route",+    "fusion_plate_vias",     "fusion_pour_state",     "fusion_preflight_lbr",     "fusion_prefs_close",@@ -308,6 +310,7 @@     "fusion_sch_text",     "fusion_sch_trace_point",     "fusion_sch_update_from_library",+    "fusion_sch_update_from_library",     "fusion_sch_zoom",     "fusion_schematic",     "fusion_schematic_data",
contracts/describe.json+62
@@ -1221,6 +1221,25 @@       "summary": "PRECISELY delete a list of cloud files by lineage urn - SAFE cleanup of AI-created clutter. Deletes ONLY the exact fileIds given (never name-guessing), so it can't touch a teammate's file in a shared folder. Loops server-side. Get the ids from fusion_aps_browse (item .id). Use this to remove f3d files the bridge created. A big list takes minutes (relay may time out at ~60s while deletes continue server-side - re-browse to confirm).",       "timeoutSeconds": 1800     },+    {+      "example": {+        "dryRun": true,+        "libraryPath": "C:/libs/parts.lbr"+      },+      "input": {+        "dryRun": "bool",+        "libraryPath": ".lbr on the Fusion machine (required)"+      },+      "longRunning": false,+      "name": "fusion_cleanup_libs",+      "output": {+        "backup": "str",+        "data": "{removed: int, items: ['component X' | 'symbol X' | 'footprint X']}"+      },+      "statusVerb": null,+      "summary": "Remove the empty leftovers from a library (the standard's cleanup_libs), offline in a .lbr: components with no device (Altium's factory Component_1 has the same role), and footprints and symbols with nothing drawn that no component uses. Anything with content stays, used or not. Written with a .bak and preflighted; dryRun lists what would go.",+      "timeoutSeconds": 30+    },     {       "example": {},       "input": {@@ -19915,6 +19934,26 @@       "summary": "Plan a path for one net between two pads on the live board without changing it (the standard's plan_route), from the shared checks engine (adom-eda-checks): a bounded orthogonal A* on any copper layers listed (default F.Cu and B.Cu), with through vias between them; pads by their real outline, the board's strictest copper clearance and copper-to-edge rule. Answered as routeNet, fusion_route_net's arguments (boardId and expectedRevision included), to pass on unchanged; found:false with a reason when no path fits. Pours are not obstacles: fusion_route_net undoes a route that cuts another net's pour in two. Fine-pitch pads need gridMm 0.1.",       "timeoutSeconds": 60     },+    {+      "example": {+        "dryRun": true,+        "libraryPath": "C:/libs/parts.lbr"+      },+      "input": {+        "dryRun": "bool",+        "footprintName": "str (optional: one footprint; default all)",+        "libraryPath": ".lbr on the Fusion machine (required)"+      },+      "longRunning": false,+      "name": "fusion_plate_vias",+      "output": {+        "backup": "str",+        "data": "{holes: int, plated: int, fixed: 'Footprint:Pad;...', joinedToLands: int}"+      },+      "statusVerb": null,+      "summary": "Make every drilled hole in a library's footprints a plated pad (the standard's plate_vias), offline in a .lbr. In EAGLE a pad is always plated and an unplated drill is a <hole>, so each hole becomes a pad. A hole inside an SMD land (a heatsink via in an exposed pad) is named after the land (EP@2, EP@3 ...) and joined to that land's connect in every device, so it sits on the land's net; any other hole becomes H1, H2 ... and stays unconnected. The copper ring is left to the board's restring rules, as for any EAGLE via. Written with a .bak and preflighted; dryRun shows the plan.",+      "timeoutSeconds": 30+    },     {       "example": {},       "input": {@@ -21657,6 +21696,29 @@       "summary": "Refused with unsupported, naming the libraries the design embeds: Fusion updates parts from their libraries in its Library Manager, which asks about each part, so there is no command the bridge can answer up front.",       "timeoutSeconds": 60     },+    {+      "example": {+        "library": "C:/libs/parts.lbr"+      },+      "input": {+        "apply": "bool: refused with the plan (not_supported)",+        "expectDocument": "str",+        "filePath": "a .sch/.fsch to read instead of the open design",+        "library": ".lbr on the Fusion machine (or libraries: [.lbr]; required)",+        "symbols": "[deviceset names] (default every part)"+      },+      "longRunning": false,+      "name": "fusion_sch_update_from_library",+      "output": {+        "differs": "int",+        "parts": "[{designator, symbol, library, status: up_to_date|differs|library_not_found, diff?: {gates?, symbols?: {gate: {pinsAdded?, pinsRemoved?, pinsChanged?, drawingChanged?}}, attributes?, device?}, droppedWiredPins?}]",+        "plan": "true",+        "revision": "str"+      },+      "statusVerb": null,+      "summary": "Compare placed parts with their library (the standard's sch_update_from_library), the plan: each part's component as the schematic holds it (Fusion keeps a copy of every library a design uses) against the same-named deviceset in the .lbr files named: gates added or removed, each symbol's pins added, removed or changed (position, length, direction, turn), its drawing, and the device's attributes (MPN ...). Each part is up_to_date, differs or library_not_found; droppedWiredPins names pins the library's symbol drops that are wired on the sheet. apply is refused with the plan, because Fusion's UPDATE asks about each part in a dialog.",+      "timeoutSeconds": 60+    },     {       "example": {         "refs": [
describe.py+12
@@ -458,6 +458,18 @@ _T = [      {"libraryPath": ".lbr on the Fusion machine (required)", "names": "[footprint or component names] (default all)"},      {"ok": "bool", "checked": "int", "errors": "int", "warnings": "int", "infos": "int", "findings": "[{name, kind, rule, severity, detail}]", "unread": "[]", "engine": "str"},      30, None, False, {"libraryPath": "C:/libs/parts.lbr"}),+    ('fusion_plate_vias', "Make every drilled hole in a library's footprints a plated pad (the standard's plate_vias), offline in a .lbr. In EAGLE a pad is always plated and an unplated drill is a <hole>, so each hole becomes a pad. A hole inside an SMD land (a heatsink via in an exposed pad) is named after the land (EP@2, EP@3 ...) and joined to that land's connect in every device, so it sits on the land's net; any other hole becomes H1, H2 ... and stays unconnected. The copper ring is left to the board's restring rules, as for any EAGLE via. Written with a .bak and preflighted; dryRun shows the plan.",+     {"libraryPath": ".lbr on the Fusion machine (required)", "footprintName": "str (optional: one footprint; default all)", "dryRun": "bool"},+     {"data": "{holes: int, plated: int, fixed: 'Footprint:Pad;...', joinedToLands: int}", "backup": "str"},+     30, None, False, {"libraryPath": "C:/libs/parts.lbr", "dryRun": True}),+    ('fusion_cleanup_libs', "Remove the empty leftovers from a library (the standard's cleanup_libs), offline in a .lbr: components with no device (Altium's factory Component_1 has the same role), and footprints and symbols with nothing drawn that no component uses. Anything with content stays, used or not. Written with a .bak and preflighted; dryRun lists what would go.",+     {"libraryPath": ".lbr on the Fusion machine (required)", "dryRun": "bool"},+     {"data": "{removed: int, items: ['component X' | 'symbol X' | 'footprint X']}", "backup": "str"},+     30, None, False, {"libraryPath": "C:/libs/parts.lbr", "dryRun": True}),+    ('fusion_sch_update_from_library', "Compare placed parts with their library (the standard's sch_update_from_library), the plan: each part's component as the schematic holds it (Fusion keeps a copy of every library a design uses) against the same-named deviceset in the .lbr files named: gates added or removed, each symbol's pins added, removed or changed (position, length, direction, turn), its drawing, and the device's attributes (MPN ...). Each part is up_to_date, differs or library_not_found; droppedWiredPins names pins the library's symbol drops that are wired on the sheet. apply is refused with the plan, because Fusion's UPDATE asks about each part in a dialog.",+     {"library": ".lbr on the Fusion machine (or libraries: [.lbr]; required)", "symbols": "[deviceset names] (default every part)", "expectDocument": "str", "filePath": "a .sch/.fsch to read instead of the open design", "apply": "bool: refused with the plan (not_supported)"},+     {"plan": "true", "differs": "int", "parts": "[{designator, symbol, library, status: up_to_date|differs|library_not_found, diff?: {gates?, symbols?: {gate: {pinsAdded?, pinsRemoved?, pinsChanged?, drawingChanged?}}, attributes?, device?}, droppedWiredPins?}]", "revision": "str"},+     60, None, False, {"library": "C:/libs/parts.lbr"}),     ('fusion_update_footprints_from_library', "Compare placed footprints with their library (the standard's update_footprints_from_library), the plan: each part's footprint as the board holds it against the same-named package in the .lbr files named, pad by pad (added, removed, moved, resized, reshaped, drill, turn, layer), each up_to_date, differs or library_not_found. A Fusion design's libraries are cloud items, so the .lbr files are named. apply:true is refused (not_supported) with the plan: Fusion's UPDATE asks about each part in a dialog.",      {"expectDocument": "the board (required)", "libraryPath": ".lbr to compare against", "libraries": "[.lbr] (instead of libraryPath)", "refs": "[ref] (default every part)", "apply": "refused: see above"},      {"plan": "true", "differs": "int", "footprints": "[{ref, footprint, library, status, diff?: {padsAdded, padsRemoved, padsChanged: [{pad, changes}]}, why?}]", "revision": "str"},
rust/bridge/src/main.rs+3−1
@@ -691,7 +691,7 @@ fn main() {                     "fusion_edit_footprint" | "edit_footprint" => native_edit_footprint(&parsed["args"]),                     v @ ("fusion_create_footprint" | "create_footprint" | "fusion_create_symbol" | "create_symbol" | "fusion_read_symbol" | "read_symbol" | "fusion_find_symbol" | "find_symbol"                         | "fusion_copy_footprint" | "copy_footprint" | "fusion_rename_footprint" | "rename_footprint" | "fusion_remove_footprint" | "remove_footprint" | "fusion_xcopy_footprint" | "xcopy_footprint"-                        | "fusion_clone_symbol" | "clone_symbol" | "fusion_rename_symbol" | "rename_symbol" | "fusion_remove_symbol" | "remove_symbol" | "fusion_set_params" | "set_params" | "fusion_footprint_from_land_pattern" | "footprint_from_land_pattern" | "fusion_set_models" | "set_models" | "fusion_add_models" | "add_models" | "fusion_symbol_prims" | "symbol_prims" | "fusion_set_visibility" | "set_visibility" | "fusion_add_symbol_art" | "add_symbol_art" | "fusion_lint_library" | "lint_library") => native_lib_authoring(v.strip_prefix("fusion_").unwrap_or(v), &parsed["args"]),+                        | "fusion_clone_symbol" | "clone_symbol" | "fusion_rename_symbol" | "rename_symbol" | "fusion_remove_symbol" | "remove_symbol" | "fusion_set_params" | "set_params" | "fusion_footprint_from_land_pattern" | "footprint_from_land_pattern" | "fusion_set_models" | "set_models" | "fusion_add_models" | "add_models" | "fusion_symbol_prims" | "symbol_prims" | "fusion_set_visibility" | "set_visibility" | "fusion_add_symbol_art" | "add_symbol_art" | "fusion_lint_library" | "lint_library" | "fusion_plate_vias" | "plate_vias" | "fusion_cleanup_libs" | "cleanup_libs") => native_lib_authoring(v.strip_prefix("fusion_").unwrap_or(v), &parsed["args"]),                     "fusion_preflight_lbr" | "preflight_lbr" => native_preflight_lbr(&parsed["args"]),                     "fusion_not_supported" | "not_supported" => unsupported::answer(&parsed["args"]),                     // Native since Phase 2: the raw authenticated APS GET (never-charge guarded).@@ -1082,6 +1082,8 @@ fn native_lib_authoring(verb: &str, args: &Value) -> Value {             Some(prm) => a::set_visibility(text.as_deref().unwrap(), &s("symbolName").or(s("name")).unwrap_or_default(), &prm, args["visible"].as_bool().unwrap_or(true)),             None => Err("parameter is required (the attribute to show or hide, e.g. MPN)".to_string()),         },+        "plate_vias" => a::plate_vias(text.as_deref().unwrap(), s("footprintName").as_deref()),+        "cleanup_libs" => a::cleanup_libs(text.as_deref().unwrap()),         "set_models" => a::set_models(text.as_deref().unwrap(), &s("symbolName").unwrap_or_default(), &s("footprint").unwrap_or_default()),         "add_models" => {             // a model's footprint from another .lbr is copied in; its pcblib is that library
rust/bridge/src/sch.rs+1−1
@@ -2,7 +2,7 @@ //! thread through the script transport, as the routing verbs do. use serde_json::{json, Value}; const SOURCE: &str = include_str!("sch_script.py");-pub fn owns(cmd: &str) -> bool { matches!(cmd, "sch_items" | "sch_pins" | "sch_trace_point" | "sch_page" | "sch_hierarchy" | "sch_text" | "sch_junction" | "sch_delete_wire" | "sch_connect_pins" | "sch_split_wire" | "sch_bus" | "sch_edit_label" | "sch_move_region" | "sch_fix_connectivity" | "sch_bus_entry" | "sch_connect_to_bus" | "render_schematic" | "sch_reset_fields" | "sch_create" | "sch_sheet_pins" | "sch_update_from_library" | "cross_probe" | "cross_check" | "schematic_data" | "part_parameters" | "doc_params" | "sheet_params" | "sch_edit" | "annotate" | "sch_connect" | "sch_place_part" | "sch_swap_part" | "export_spice" | "simulate" | "sch_zoom" | "jlcpcb_order" | "variants") }+pub fn owns(cmd: &str) -> bool { matches!(cmd, "sch_update_from_library" | "sch_items" | "sch_pins" | "sch_trace_point" | "sch_page" | "sch_hierarchy" | "sch_text" | "sch_junction" | "sch_delete_wire" | "sch_connect_pins" | "sch_split_wire" | "sch_bus" | "sch_edit_label" | "sch_move_region" | "sch_fix_connectivity" | "sch_bus_entry" | "sch_connect_to_bus" | "render_schematic" | "sch_reset_fields" | "sch_create" | "sch_sheet_pins" | "sch_update_from_library" | "cross_probe" | "cross_check" | "schematic_data" | "part_parameters" | "doc_params" | "sheet_params" | "sch_edit" | "annotate" | "sch_connect" | "sch_place_part" | "sch_swap_part" | "export_spice" | "simulate" | "sch_zoom" | "jlcpcb_order" | "variants") } pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {     if cmd == "render_schematic" { return render_schematic(args, pass); }     let script = format!("import json\nexec({})\nresult=handle(app,json.loads({}),{})\n",
rust/bridge/src/sch_script.py+99−1
@@ -2058,6 +2058,97 @@ def variants(app, args):                    _hint='Variants live in the schematic; Fusion keeps the board in step. The variant asked is now the current one (EAGLE makes a new variant current). fusion_board_bom reads the fitted parts of the current variant.')  +def _pins_of(sym):+    return {pn.get('name'): tuple(pn.get(k, '') for k in ('x', 'y', 'length', 'direction', 'rot')) for pn in sym.findall('pin')}+++def _art_of(sym):+    return sorted(ET.tostring(c).decode() for c in sym if c.tag != 'pin')+++def sch_update_from_library(root, args):+    """The standard's sch_update_from_library, the plan: each placed part's component as the schematic+    holds it (its embedded library) against the same-named deviceset in the .lbr files named: gates,+    each symbol's pins (added, removed, moved) and drawing, and the device's attributes. A pin the+    library drops that is wired on the sheet is named in droppedWiredPins (Altium's force case)."""+    paths = args.get('libraries') or ([args['library']] if args.get('library') else []) or ([args['libraryPath']] if args.get('libraryPath') else [])+    if not paths:+        sfail('bad_args', 'library (or libraries: [.lbr]) is required: a Fusion design keeps a copy of each library it uses, so name the .lbr files to compare against')+    libs = []+    for path in paths:+        try:+            lr = ET.parse(path).getroot()+        except Exception as ex:+            sfail('library_unreadable', '%s: %s' % (path, ex))+        lb = lr.find('drawing/library')+        if lb is None:+            sfail('library_unreadable', '%s holds no library' % path)+        libs.append((path, {s.get('name'): s for s in lb.findall('symbols/symbol')}, {d.get('name').lower(): d for d in lb.findall('devicesets/deviceset')}))+    sch = root.find('drawing/schematic')+    lib = Lib(sch)+    wired = set()+    for pr in sch.iter('pinref'):+        wired.add((pr.get('part'), pr.get('gate'), pr.get('pin')))+    want = {str(s).lower() for s in (args.get('symbols') or [])}+    rows, differs = [], 0+    for name, part in sorted(lib.parts.items()):+        dsn, ln = part.get('deviceset') or '', part.get('library') or ''+        if want and dsn.lower() not in want:+            continue+        held = lib.devicesets.get((ln, dsn))+        hit = next(((path, syms, dss[dsn.lower()]) for path, syms, dss in libs if dsn.lower() in dss), None)+        if held is None or hit is None:+            rows.append({'designator': name, 'symbol': dsn, 'library': ln if hit is None else hit[0], 'status': 'library_not_found',+                         'why': 'no deviceset of that name in the libraries given' if hit is None else 'the schematic holds no copy of it'})+            continue+        path, syms, ds = hit+        diff = {}+        g_held = {g.get('name'): g.get('symbol') for g in held.findall('gates/gate')}+        g_lib = {g.get('name'): g.get('symbol') for g in ds.findall('gates/gate')}+        if set(g_held) != set(g_lib):+            diff['gates'] = {'added': sorted(set(g_lib) - set(g_held)), 'removed': sorted(set(g_held) - set(g_lib))}+        dropped = []+        for g in sorted(set(g_held) & set(g_lib)):+            a, b = lib.symbols.get((ln, g_held[g])), syms.get(g_lib[g])+            if a is None or b is None:+                continue+            pa, pb = _pins_of(a), _pins_of(b)+            d = {}+            if set(pb) - set(pa):+                d['pinsAdded'] = sorted(set(pb) - set(pa))+            if set(pa) - set(pb):+                d['pinsRemoved'] = sorted(set(pa) - set(pb))+                dropped += ['%s.%s' % (g, x) if len(g_held) > 1 else x for x in d['pinsRemoved'] if (name, g, x) in wired]+            moved = sorted(x for x in set(pa) & set(pb) if pa[x] != pb[x])+            if moved:+                d['pinsChanged'] = moved+            if _art_of(a) != _art_of(b):+                d['drawingChanged'] = True+            if d:+                diff.setdefault('symbols', {})[g] = d+        dev = part.get('device') or ''+        tech = part.get('technology') or ''+        def attrs(dset):+            dv = next((x for x in dset.findall('devices/device') if (x.get('name') or '') == dev), None)+            if dv is None:+                return None+            tn = next((x for x in dv.findall('technologies/technology') if (x.get('name') or '') == tech), None)+            return {a.get('name'): a.get('value') for a in (tn.findall('attribute') if tn is not None else [])}+        ah, al = attrs(held), attrs(ds)+        if al is None:+            diff['device'] = 'the library has no device %r (technology %r) of %s' % (dev, tech, dsn)+        elif ah is not None and ah != al:+            diff['attributes'] = {k: {'held': ah.get(k), 'library': al.get(k)} for k in sorted(set(ah) | set(al)) if ah.get(k) != al.get(k)}+        row = {'designator': name, 'symbol': dsn, 'library': path, 'status': 'differs' if diff else 'up_to_date'}+        if diff:+            differs += 1+            row['diff'] = diff+        if dropped:+            row['droppedWiredPins'] = dropped+        rows.append(row)+    return {'plan': True, 'differs': differs, 'parts': rows}++ EDITS = {'doc_params': doc_params, 'sheet_params': sheet_params, 'sch_edit': sch_edit, 'annotate': annotate, 'sch_text': sch_text, 'sch_junction': sch_junction, 'sch_delete_wire': sch_delete_wire, 'sch_connect_pins': sch_connect_pins,          'sch_split_wire': sch_split_wire, 'sch_bus': sch_bus, 'sch_edit_label': sch_edit_label, 'sch_move_region': sch_move_region,          'sch_fix_connectivity': sch_fix_connectivity, 'sch_bus_entry': sch_bus_entry, 'sch_connect_to_bus': sch_connect_to_bus, 'sch_reset_fields': sch_reset_fields, 'sch_create': sch_create, 'sch_connect': sch_connect, 'sch_place_part': sch_place_part, 'sch_swap_part': sch_swap_part, 'variants': variants}@@ -2275,12 +2366,19 @@ def handle(app, args, action):         if action == 'cross_check':             return cross_check(app, args)         root, meta, _ = sch_snapshot(app, args)-        n, sheet = pick_sheet(root, args) if action not in ('sch_hierarchy', 'schematic_data', 'part_parameters') else (None, None)+        n, sheet = pick_sheet(root, args) if action not in ('sch_hierarchy', 'schematic_data', 'part_parameters', 'sch_update_from_library') else (None, None)         base = {'success': True, 'mutated': False, **meta, 'sheet': n, 'sheetCount': len(sheets_of(root))}         if action == 'schematic_data':             return {**base, **schematic_data(root, args), '_hint': 'Pins carry the pad numbers their device joins them to (empty for a pin with no pad, such as a supply symbol). Positions are each gate on its sheet, in mm.'}         if action == 'part_parameters':             return {**base, **part_parameters(root, args)}+        if action == 'sch_update_from_library':+            plan = sch_update_from_library(root, args)+            if args.get('apply'):+                return {**base, **plan, 'success': False, 'errorCode': 'not_supported',+                        'error': "apply is not supported in Fusion: its UPDATE asks about each part in a dialog, which the bridge never raises. The plan is below.",+                        '_hint': 'Update the parts in Fusion (Library Manager > Update, or right-click a part > Update); then fusion_run_erc.'}+            return {**base, **plan, '_hint': 'A plan: each placed part against the same-named deviceset in the libraries given (gates, pins, symbol drawing, attributes). Fusion updates parts from their library itself (it asks about each one), so apply is refused. droppedWiredPins names pins a new symbol would drop that are wired on the sheet.'}         if action == 'sch_items':             return {**base, **items(root, sheet, args.get('kinds')),                     '_hint': 'Ids are built from each item\'s kind and position (Fusion stores none): pass them to the sch_* edit verbs; an id stops matching once its item moves. Parts are in schematic_data or sch_pins.'}
rust/bridge/src/unsupported.rs−2
@@ -25,8 +25,6 @@ pub const TABLE: &[(&str, &str)] = &[     ("connect_workspace", "Altium's workspace picker; Fusion works in the signed-in Autodesk account's hub (fusion_account)"),     ("selftest_script_error", "a test of Altium's DelphiScript agent; Fusion runs none"),     ("symbol_mode", "an EAGLE symbol has one look: there are no display modes to show"),-    ("cleanup_libs", "a new Fusion library starts empty: there is no factory-default part to remove"),-    ("plate_vias", "EAGLE pads are always plated: an unplated hole is a separate hole object"),     ("panel", "Altium's docked panel tabs; Fusion's editors show their own panels, and the bridge opens a surface (library, symbol, footprint, board, schematic, 3D) with fusion_show_* in the background"),     ("selection", "Fusion does not show the user's board selection to the bridge: a ULP's ingroup() reads no part even right after a GROUP (checked live on Fusion 2706, 2026-10-30). Name the parts instead; fusion_electron_select shows one to the user"),     ("uninstall_library", "a library Fusion offers is a file in the Autodesk hub, so taking it out means deleting that file for everyone in the hub, which the standard's uninstall never does (it leaves files alone); a hub file the AI made itself is deleted with fusion_cleanup_cloud_files"),
rust/lbr/src/authoring.rs+105
@@ -837,6 +837,83 @@ pub fn set_params(xml: &str, symbol: Option<&str>, params: &Value, comment: Opti     }     Ok((root.to_xml_document(), json!({"component": n, "paramsSet": set_n, "footprint": linked}))) }+/// The standard's plate_vias: in EAGLE a pad is always plated, and an unplated drill is a <hole>.+/// Each hole in a footprint (or one footprint) becomes a plated pad. A hole inside an SMD land (a+/// heatsink via in an exposed pad) is named after that land, N@k, and joined to its connects in every+/// device, so it is on the land's net; any other hole becomes H<k>, unconnected.+pub fn plate_vias(xml: &str, footprint: Option<&str>) -> Result<(String, Value), String> {+    let mut root = Element::parse(xml).map_err(|e| e.to_string())?;+    let lib = lib_mut(&mut root)?;+    let fp = match footprint { Some(w) => Some(find_ci(&names(section(lib, "packages"), "package"), w).cloned().ok_or_else(|| format!("no footprint {w}"))?), None => None };+    let num = |e: &Element, k: &str| e.attr(k).and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0);+    let (mut holes, mut fixed, mut joins) = (0usize, Vec::new(), Vec::<(String, String, String)>::new());+    for pk in section(lib, "packages").children.iter_mut().filter(|p| p.tag == "package" && fp.as_deref().map(|n| p.attr("name") == Some(n)).unwrap_or(true)) {+        let pname = pk.attr("name").unwrap_or("").to_string();+        let mut taken: std::collections::HashSet<String> = pk.children.iter().filter(|c| c.tag == "pad" || c.tag == "smd").filter_map(|c| c.attr("name").map(str::to_string)).collect();+        // each SMD land as its half extents round its centre (a turned land by its bounding box)+        let lands: Vec<(String, f64, f64, f64, f64)> = pk.children.iter().filter(|c| c.tag == "smd").map(|s| {+            let deg = s.attr("rot").map(|r| r.trim_start_matches(|c: char| c.is_ascii_alphabetic()).parse::<f64>().unwrap_or(0.0)).unwrap_or(0.0).to_radians();+            let (hw, hh) = (num(s, "dx") / 2.0, num(s, "dy") / 2.0);+            (s.attr("name").unwrap_or("").to_string(), num(s, "x"), num(s, "y"), hw * deg.cos().abs() + hh * deg.sin().abs(), hw * deg.sin().abs() + hh * deg.cos().abs())+        }).collect();+        for c in pk.children.iter_mut().filter(|c| c.tag == "hole") {+            holes += 1;+            let (x, y) = (num(c, "x"), num(c, "y"));+            let land = lands.iter().find(|(_, lx, ly, hw, hh)| (x - lx).abs() <= *hw && (y - ly).abs() <= *hh).map(|l| l.0.clone());+            let stem = land.clone().map(|l| format!("{}@", l.split('@').next().unwrap_or(&l))).unwrap_or_else(|| "H".into());+            let name = (if land.is_some() { 2 } else { 1 }..).map(|k| format!("{stem}{k}")).find(|n| !taken.contains(n)).unwrap();+            taken.insert(name.clone());+            let drill = c.attr("drill").unwrap_or("0").to_string();+            // no diameter: the board's restring rules size the copper ring, as for any EAGLE via+            *c = el("pad", &[("name", name.clone()), ("x", f(x)), ("y", f(y)), ("drill", drill)]);+            fixed.push(format!("{pname}:{name}"));+            if let Some(l) = land { joins.push((pname.clone(), l, name)); }+        }+    }+    // a via named after its land joins the connect that land is on, in every device of that footprint+    for d in section(lib, "devicesets").children.iter_mut().filter(|d| d.tag == "deviceset") {+        for dev in d.find_child_mut("devices").map(|x| x.children.iter_mut().collect::<Vec<_>>()).unwrap_or_default() {+            let pk = dev.attr("package").unwrap_or("").to_string();+            for cn in dev.find_child_mut("connects").map(|x| x.children.iter_mut().collect::<Vec<_>>()).unwrap_or_default() {+                let mut pads: Vec<String> = cn.attr("pad").unwrap_or("").split_whitespace().map(str::to_string).collect();+                let add: Vec<String> = joins.iter().filter(|(p, l, _)| *p == pk && pads.contains(l)).map(|j| j.2.clone()).collect();+                if add.is_empty() { continue; }+                pads.extend(add);+                set(cn, "pad", &pads.join(" "));+            }+        }+    }+    let plated = fixed.len();+    Ok((root.to_xml_document(), json!({"holes": holes, "plated": plated, "fixed": fixed.join(";"), "joinedToLands": joins.len(),+        "note": if holes == 0 { "no unplated holes: every drilled pad in an EAGLE footprint is already plated" } else { "each hole is now a plated pad; one inside an SMD land is on that land's net" }})))+}++/// The standard's cleanup_libs: the empty leftovers a library collects removed: components with no+/// device, and footprints and symbols with nothing drawn that nothing uses. Anything with content stays.+pub fn cleanup_libs(xml: &str) -> Result<(String, Value), String> {+    let mut root = Element::parse(xml).map_err(|e| e.to_string())?;+    let lib = lib_mut(&mut root)?;+    let mut removed: Vec<String> = Vec::new();+    let ds = section(lib, "devicesets");+    ds.children.retain(|d| {+        let empty = d.tag == "deviceset" && d.find_child("devices").map(|x| x.children.iter().all(|c| c.tag != "device")).unwrap_or(true);+        if empty { removed.push(format!("component {}", d.attr("name").unwrap_or(""))); }+        !empty+    });+    let used_sym: std::collections::HashSet<String> = ds.children.iter().flat_map(|d| d.find_child("gates").map(|g| g.children.iter().filter_map(|x| x.attr("symbol").map(str::to_string)).collect::<Vec<_>>()).unwrap_or_default()).collect();+    let used_pk: std::collections::HashSet<String> = ds.children.iter().flat_map(|d| d.find_child("devices").map(|g| g.children.iter().filter_map(|x| x.attr("package").map(str::to_string)).collect::<Vec<_>>()).unwrap_or_default()).collect();+    let drawn = |e: &Element| e.children.iter().any(|c| c.tag != "description");+    for (sec, tag, used, what) in [("symbols", "symbol", &used_sym, "symbol"), ("packages", "package", &used_pk, "footprint")] {+        section(lib, sec).children.retain(|e| {+            let gone = e.tag == tag && !drawn(e) && !e.attr("name").map(|n| used.contains(n)).unwrap_or(false);+            if gone { removed.push(format!("{what} {}", e.attr("name").unwrap_or(""))); }+            !gone+        });+    }+    let n = removed.len();+    Ok((root.to_xml_document(), json!({"removed": n, "items": removed})))+}+  /// The preflight a library under construction must pass: every check of fusion_preflight_lbr /// except the ones only a finished library meets (has symbols, devicesets, packages, connects):@@ -971,3 +1048,31 @@ mod layout_tests {         assert!(create_from_layout(None, "X", "", &json!({"pads": [], "lines": []})).is_err());     } }++#[cfg(test)]+mod plate_cleanup_tests {+    use super::*;+    const LIB: &str = r#"<?xml version="1.0"?><eagle><drawing><library><packages>+<package name="QFN"><smd name="EP" x="0" y="0" dx="3" dy="3" layer="1"/><smd name="1" x="2" y="0" dx="0.3" dy="0.8" layer="1"/><hole x="0.5" y="0.5" drill="0.3"/><hole x="-0.5" y="-0.5" drill="0.3"/><hole x="5" y="5" drill="1"/></package>+<package name="EMPTY"></package></packages><symbols><symbol name="S"><pin name="GND" x="0" y="0"/><pin name="A" x="0" y="2"/></symbol><symbol name="BLANK"></symbol></symbols>+<devicesets><deviceset name="CHIP"><gates><gate name="G$1" symbol="S" x="0" y="0"/></gates><devices><device name="" package="QFN"><connects><connect gate="G$1" pin="GND" pad="EP"/><connect gate="G$1" pin="A" pad="1"/></connects></device></devices></deviceset>+<deviceset name="Component_1"><gates/><devices/></deviceset></devicesets></library></drawing></eagle>"#;++    #[test]+    fn holes_in_a_land_become_its_plated_vias_on_its_net() {+        let (x, s) = plate_vias(LIB, None).unwrap();+        assert_eq!((s["holes"].as_u64(), s["plated"].as_u64(), s["joinedToLands"].as_u64()), (Some(3), Some(3), Some(2)));+        assert_eq!(s["fixed"], "QFN:EP@2;QFN:EP@3;QFN:H1");+        assert!(x.contains(r#"pad="EP EP@2 EP@3""#) && !x.contains("<hole"));+        assert!(x.contains(r#"<pad name="H1" x="5" y="5" drill="1" />"#));+        assert_eq!(plate_vias(&x, Some("qfn")).unwrap().1["holes"], 0);+    }++    #[test]+    fn cleanup_takes_only_empty_unused_items() {+        let (x, s) = cleanup_libs(LIB).unwrap();+        assert_eq!(s["removed"], 3);+        assert!(x.contains(r#"name="CHIP""#) && !x.contains("Component_1") && !x.contains("BLANK") && !x.contains(r#"name="EMPTY""#));+        assert_eq!(cleanup_libs(&x).unwrap().1["removed"], 0);+    }+}
tests/run_verb_tests.py+3
@@ -460,6 +460,9 @@ t("fusion_panelize", "daily", dict(B, rows=2, cols=2, separation="vscore", dryRu t("fusion_replicate_layout", "daily", dict(B, source=["C11", "C12"], targets=[["C13", "C14"]], dryRun=True), {"ok": True, "fields": ["moves", "footprintMismatches"]}, "a channel's placement planned onto another (dry run on BQ25792)") t("fusion_symbol_prims", "daily", {"libraryPath": "C:/Users/olive/Documents/fusion-lane-tests/import/lane-import.lbr", "symbolName": "LDO-LANE"}, {"ok": True, "fields": ["pins", "bodyRectMil", "models"]}, "the imported LDO's drawing counted") t("fusion_set_visibility", "daily", {"libraryPath": "C:/Users/olive/Documents/fusion-lane-tests/import/lane-import.lbr", "symbolName": "LDO-LANE", "parameter": "MPN", "visible": True, "dryRun": True}, {"ok": True, "fields": ["data"]}, "show MPN on the sheet (dry run)")+t("fusion_sch_update_from_library", "daily", {"library": "C:/Users/olive/Documents/fusion-lane-tests/import/lane-import.lbr"}, {"ok": True, "fields": ["parts", "differs"]}, "read-only plan on the open design's schematic")+t("fusion_plate_vias", "daily", {"libraryPath": "C:/Users/olive/Documents/fusion-lane-tests/import/lane-import.lbr", "dryRun": True}, {"ok": True, "fields": ["data"]}, "offline .lbr: holes become plated pads (dryRun writes nothing)")+t("fusion_cleanup_libs", "daily", {"libraryPath": "C:/Users/olive/Documents/fusion-lane-tests/import/lane-import.lbr", "dryRun": True}, {"ok": True, "fields": ["data"]}, "offline .lbr: empty unused items listed (dryRun writes nothing)") t("fusion_variants", "daily", dict(B), {"ok": True, "fields": ["variants"]}, "the design's assembly variants (BQ25792 has none)") t("fusion_variants#edit", "negative", dict(B, name="FL-TEST", notFitted=["C11"]), {"pred": "must_refuse"}, "Fusion's VARIANT text command changes nothing, so an edit is refused not_supported") t("fusion_silkscreen_prep", "daily", dict(B, dryRun=True), {"ok": True, "fields": ["before", "after", "silkOffCopper"]}, "designators on pads counted and a move planned (dry run)")