app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
Update 18 file(s)
18 files changed
+191−47
BRIDGE_VERSION+1−1CHANGELOG.md+16bridge.json+1−1contracts/describe.json+3−2dashboard/Cargo.lock+1−1dashboard/Cargo.toml+1−1describe.py+2−2install.sh+1−1package.json+1−1page.json+2−2rust/Cargo.lock+5−5rust/Cargo.toml+1−1rust/bridge/src/lbr_use.py+65rust/bridge/src/routing.rs+1−1rust/bridge/src/routing_script.py+13−22rust/bridge/src/sch.rs+1−1rust/bridge/src/sch_script.py+9−5tests/test_lbr_use.py+67BRIDGE_VERSION+1−1@@ -1 +1 @@-1.13.46+1.13.47
CHANGELOG.md+16@@ -3,6 +3,22 @@ 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.47 (2026-10-31)++Issue #135, the actual cause, bisected live on Fusion 2706.1.14 with the AI Flow lane (adom-aiflow#114): Fusion+raises its PlacePartWorker `assetAdded` error on every ADD from a .lbr whose elements are not separated by line+breaks (`<packages><package ...>` on one line), whatever the parts are. The same library with a line break between+its elements adds cleanly, with or without 3D packages. One compact boundary inside `<devicesets>` was enough.+1.13.46's diagnosis (parts without a 3D package) was wrong: those parts add fine from a library broken into lines.+- `fusion_schematic_run`, `fusion_sch_place_part` and `fusion_sch_swap_part` rewrite a compact .lbr in place before+ USE: a line break between elements, nothing else (the element tree is compared before and after and nothing is+ written if it differs). In place, because a copy at another path under the same library name makes Fusion ask+ which one to take once both are in use. The reply says `libraryReformatted`. A file the bridge cannot write is+ refused (`library_not_writable`) with the reason.+- The `no_3d_package` refusal from 1.13.46 is removed.+- `fusion_schematic_run` still checks each ADD and stops at the first that did not land (`add_failed`), and an+ assertion during the run still fails it (`fusion_assert`).+ ## 1.13.46 (2026-10-31) Issue #135, the real cause (checked live on Fusion 2706.1.14): ADD raises Fusion's PlacePartWorker `assetAdded`
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.13.46",+ "version": "1.13.47", "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.",
contracts/describe.json+3−2@@ -1,6 +1,6 @@ { "contract": "fusion-bridge verb catalog",- "frozen": "2026-10-30",+ "frozen": "2026-10-31", "note": "The Python bridge and the Rust bridge must both serve this catalog byte-for-byte in meaning: same names, same args, same result shapes, same timeouts. Regenerate ONLY from describe.py and diff in CI.", "source": "describe.py", "verbs": [@@ -22204,13 +22204,14 @@ "output": { "added": "[name]", "commands": "int",+ "libraryReformatted": "bool", "nets": "{net:[part.pin]}", "parts": "[name]", "success": "bool", "verified": "bool" }, "statusVerb": "fusion_get_app_state",- "summary": "Build a schematic live: a list of complete, non-interactive EAGLE commands run one at a time on the active schematic (ADD 'dev@lib' 'NAME' R0 (x y), VALUE, NET 'name' (pts), LABEL, WIRE, TEXT, LAYER, CHANGE STYLE/SIZE/WIDTH/RATIO, WINDOW FIT), with an optional pause between them so a viewer sees it grow. Anything else is refused before the first command runs. ADD is proven against libraryPath on disk first (the deviceset must be there; the library is USEd by the bridge), so the component picker can never open. Readback: the parts and nets on the sheet from a fresh export.",+ "summary": "Build a schematic live: a list of complete, non-interactive EAGLE commands run one at a time on the active schematic (ADD 'dev@lib' 'NAME' R0 (x y), VALUE, NET 'name' (pts), LABEL, WIRE, TEXT, LAYER, CHANGE STYLE/SIZE/WIDTH/RATIO, WINDOW FIT), with an optional pause between them so a viewer sees it grow. Anything else is refused before the first command runs. ADD is proven against libraryPath on disk first (the deviceset must be there; the library is USEd by the bridge), so the component picker can never open. A .lbr with no line breaks between its elements is rewritten in place first, whitespace only (libraryReformatted): Fusion 2706 cannot ADD from one. Readback: the parts and nets on the sheet from a fresh export.", "timeoutSeconds": 120 }, {
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.13.46"+version = "1.13.47" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.46"+version = "1.13.47" edition = "2021" [dependencies]
describe.py+2−2@@ -662,9 +662,9 @@ _T = [ ("fusion_ripup_nets", "Take whole nets' tracks and vias back up (RIPUP per net, one undo step each) so they can be routed again, wider or another way. Their pours stay. Readback proves no track or via is left on those nets and no pour was lost.", {"expectDocument":"required exact board name","boardId":"required","expectedRevision":"required","nets":"required [net name]"}, {"success":"bool","verified":"bool","ripped":"[net]","revision":"str"},120,"fusion_routing_state",False,{}),- ("fusion_schematic_run", "Build a schematic live: a list of complete, non-interactive EAGLE commands run one at a time on the active schematic (ADD 'dev@lib' 'NAME' R0 (x y), VALUE, NET 'name' (pts), LABEL, WIRE, TEXT, LAYER, CHANGE STYLE/SIZE/WIDTH/RATIO, WINDOW FIT), with an optional pause between them so a viewer sees it grow. Anything else is refused before the first command runs. ADD is proven against libraryPath on disk first (the deviceset must be there; the library is USEd by the bridge), so the component picker can never open. Readback: the parts and nets on the sheet from a fresh export.",+ ("fusion_schematic_run", "Build a schematic live: a list of complete, non-interactive EAGLE commands run one at a time on the active schematic (ADD 'dev@lib' 'NAME' R0 (x y), VALUE, NET 'name' (pts), LABEL, WIRE, TEXT, LAYER, CHANGE STYLE/SIZE/WIDTH/RATIO, WINDOW FIT), with an optional pause between them so a viewer sees it grow. Anything else is refused before the first command runs. ADD is proven against libraryPath on disk first (the deviceset must be there; the library is USEd by the bridge), so the component picker can never open. A .lbr with no line breaks between its elements is rewritten in place first, whitespace only (libraryReformatted): Fusion 2706 cannot ADD from one. Readback: the parts and nets on the sheet from a fresh export.", {"expectDocument":"optional exact schematic name","libraryPath":"required for ADD: the .lbr on this machine (its file name is the library name in ADD)","commands":"required [command] (1..400)","pauseMs":"optional 0..3000 between commands"},- {"success":"bool","verified":"bool","parts":"[name]","added":"[name]","nets":"{net:[part.pin]}","commands":"int"},120,"fusion_get_app_state",False,{}),+ {"success":"bool","verified":"bool","parts":"[name]","added":"[name]","nets":"{net:[part.pin]}","commands":"int","libraryReformatted":"bool"},120,"fusion_get_app_state",False,{}), ("fusion_board_bom", "The bill of materials from the live board's own part attributes: one line per MPN with designators, quantity, value, package, manufacturer and the Mouser, DigiKey and LCSC numbers, plus the JLCPCB form (Comment, Designator, Footprint, LCSC). Board-only parts (test pads) are listed as such, and any part missing a distributor number is named. Read-only; with outputDir it also writes bom.csv and bom-jlcpcb.csv there.", {"expectDocument":"required exact board name","outputDir":"optional folder on this machine for bom.csv + bom-jlcpcb.csv"}, {"lines":"[{designators,quantity,value,package,mpn,manufacturer,mouser,digikey,lcsc}]","csv":"str","jlcpcbCsv":"str","files":"{name:path}","boardOnly":"[str]","missingPartNumbers":"{mouser,digikey,lcsc:[str]}"},60,"fusion_routing_state",False,{}),
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.46"+DASH_VERSION="1.13.47" 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.46",+ "version": "1.13.47", "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.46",+ "version": "1.13.47", "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" ]-}+}
rust/Cargo.lock+5−5@@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "adom-fusion-bridge"-version = "1.13.46"+version = "1.13.47" dependencies = [ "adom-eda-checks", "base64",@@ -299,7 +299,7 @@ dependencies = [ [[package]] name = "fusion-core"-version = "1.13.46"+version = "1.13.47" dependencies = [ "serde", "serde_json",@@ -310,7 +310,7 @@ dependencies = [ [[package]] name = "fusion-lbr"-version = "1.13.46"+version = "1.13.47" dependencies = [ "regex", "roxmltree",@@ -322,14 +322,14 @@ dependencies = [ [[package]] name = "fusion-os-mac"-version = "1.13.46"+version = "1.13.47" dependencies = [ "fusion-core", ] [[package]] name = "fusion-os-win"-version = "1.13.46"+version = "1.13.47" 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.46"+version = "1.13.47" edition = "2021" license = "MIT"
rust/bridge/src/lbr_use.pyadded+65@@ -0,0 +1,65 @@+++# ── loading a .lbr for ADD / REPLACE (issue #135) ───────────────────────────────────────────────+# Fusion 2706 raises "PlacePartWorker.cpp, Line 23. Condition: assetAdded" on every ADD from a+# library file whose elements are not separated by line breaks (<packages><package ...> on one+# line), whatever the parts are; the same library with a line break between elements adds cleanly,+# with or without 3D packages (bisected live on 2706.1.14, 2026-10-31: one compact boundary in+# <devicesets> was enough). So a compact .lbr is rewritten in place with a line break between+# elements before USE. In place, because a copy at another path with the same library name makes+# Fusion ask which one to take (its Select Library dialog) once both have been USEd.++def _lbr_shape(root):+ return [(e.tag if isinstance(e.tag, str) else 'comment', sorted(e.attrib.items()), (e.text or '').strip(), (e.tail or '').strip())+ for e in root.iter()]+++def lbr_needs_line_breaks(data):+ """True when two elements inside the file's <library> (bytes or text) touch with no line break+ between them. The header outside it may be compact: Fusion's own exports are, and add fine."""+ import re as _re+ if isinstance(data, str): data = data.encode('utf-8')+ i = data.find(b'<library')+ return i >= 0 and _re.search(rb'>[ \t]*<', data[i:]) is not None+++def normalize_lbr(path):+ """Rewrite a compact .lbr in place with a line break between elements. Returns True when it+ rewrote the file. Only whitespace between elements changes: the element tree (tags, attributes,+ text, comments) is compared before and after, and nothing is written when it differs."""+ import os as _os+ import re as _re+ import xml.etree.ElementTree as _ET+ from pathlib import Path as _P+ p = _P(path)+ raw = p.read_bytes()+ if not lbr_needs_line_breaks(raw):+ return False+ # parsed from bytes, so the file's own declared encoding is honoured; written back as UTF-8+ root = _ET.fromstring(raw, parser=_ET.XMLParser(target=_ET.TreeBuilder(insert_comments=True)))+ before = _lbr_shape(root)+ _ET.indent(root, space='')+ body = _ET.tostring(root, encoding='unicode')+ again = _ET.fromstring(body, parser=_ET.XMLParser(target=_ET.TreeBuilder(insert_comments=True)))+ if _lbr_shape(again) != before:+ raise RuntimeError('rewriting %s with line breaks would change more than whitespace' % p.name)+ m = _re.search(r'<!DOCTYPE[^>]*>', raw[:2000].decode('latin-1'))+ out = '<?xml version="1.0" encoding="utf-8"?>\n' + (m.group(0) + '\n' if m else '') + body + '\n'+ tmp = p.with_name(p.name + '.adom-tmp')+ tmp.write_text(out, encoding='utf-8')+ _os.replace(str(tmp), str(p))+ return True+++def use_library(app, path):+ """USE a .lbr on this machine so ADD/REPLACE can take its devices, rewriting it first when+ Fusion 2706 would refuse it (see above). Returns True when the file was rewritten."""+ from pathlib import Path as _P+ try:+ changed = normalize_lbr(path)+ except PermissionError:+ raise RuntimeError('library_not_writable: %s has no line breaks between its elements, which Fusion 2706 cannot ADD from '+ '(PlacePartWorker assetAdded), and this machine will not let the bridge rewrite it. Save it somewhere writable '+ 'and pass that path' % path)+ app.executeTextCommand("Electron.run USE '%s';" % _P(path).as_posix())+ return changed
rust/bridge/src/routing.rs+1−1@@ -1,7 +1,7 @@ //! Routing operations execute together on Fusion's main thread through the existing //! script transport. Embedding the implementation avoids replacing a loaded add-in. use serde_json::{json, Value};-const SOURCE: &str = concat!(include_str!("routing_script.py"), "\n", include_str!("routing_planner.py"), "\n", include_str!("board_model.py"),+const SOURCE: &str = concat!(include_str!("routing_script.py"), "\n", include_str!("routing_planner.py"), "\n", include_str!("board_model.py"), "\n", include_str!("lbr_use.py"), "\nFUSION_DEFAULT_RULES = r'''", include_str!("fusion_default_designrules.xml"), "'''\n"); pub fn owns(cmd: &str) -> bool { matches!(cmd, "routing_state" | "route_net" | "routing_drc" | "move_parts" | "place_vias" | "apply_pours" | "pour_state" | "board_source" | "ripup_nets" | "schematic_run" | "board_bom" | "change_pours" | "schematic_plan" | "board_seed" | "remove_pours" | "layer_stack" | "netlist" | "bodies" | "component_data" | "component_pins" | "list_nets" | "net_connections" | "list_designators" | "layers" | "copper_layers" | "layers_in_use" | "read_rules" | "rules_by_kind" | "silk_state" | "net_classes" | "diff_pairs" | "run_drc" | "repour" | "zoom_to" | "export_bom" | "save_document" | "save_to_cloud" | "apply_silkscreen" | "set_layer_visibility" | "select_net" | "set_track_width" | "remove_items" | "set_outline" | "board_shape" | "board_outline" | "run_erc" | "autoroute" | "update_board_from_eagle" | "board_dxf" | "edit_parts" | "align_parts" | "remove_graphics" | "remove_parts" | "check_inputs" | "export_odb" | "export_ipc356" | "export_gencad" | "export_ipc2581" | "import_svg" | "copy_routing" | "set_layer_count" | "board_info_compat" | "footprint_primitives" | "place_planned_vias" | "place_teardrops" | "board_features" | "add_rule" | "read_board") } pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {
rust/bridge/src/routing_script.py+13−22@@ -664,10 +664,8 @@ def sch_run(app, args): if m.group(2) != libname: fail('library_mismatch', 'ADD names library %s but libraryPath is %s' % (m.group(2), libname)) if m.group(1).upper() not in devices: fail('unknown_device', 'No deviceset %s in %s' % (m.group(1), lbr)) done = []- # ADD resolves from a library the design HOLDS (fusion_board_seed embeds the whole project library).- # USE of the .lbr on disk is only the fallback when the design does not hold it: on Fusion 2706 that- # USE is accepted but ADD then finds nothing (issue #135). (Fusion cannot take a library out of use- # from a command, so two in-use libraries with the same name make ADD ask which one.)+ # ADD takes the device from the .lbr loaded with USE. (Fusion cannot take a library out of use from a+ # command, so two in-use libraries with the same name at different paths make ADD ask which one.) prod = app.activeProduct def part_names(): try: return {prod.parts.item(i).name for i in range(prod.parts.count)}@@ -677,21 +675,14 @@ def sch_run(app, args): for i in range(prod.libraries.count): held.add(prod.libraries.item(i).name) except Exception: pass used = 'use' if lbr else None+ reformatted = False if lbr:- # Fusion 2706 asserts on ADD (PlacePartWorker assetAdded) when a part's footprint has no 3D- # package it can add to the design (#135, checked live 2026-10-31: the same ADD lands when the- # package carries a cloud 3D package). Refuse those before any ADD, naming them.- pk3 = {pi.get('name') for q in lroot.iter('package3d') if (q.get('urn') or q.get('wip_urn')) for pi in q.iter('packageinstance')}- no3d = []- for c in cmds:- m = SCH_FORMS[0][1].match(c.strip()) if isinstance(c, str) else None- if not m: continue- ds = next((d for d in lroot.iter('deviceset') if d.get('name').upper() == m.group(1).upper()), None)- pkgs = {x.get('package') for x in ds.iter('device') if x.get('package')} if ds is not None else set()- if pkgs and not (pkgs & pk3): no3d.append({'part': m.group(3), 'deviceset': m.group(1), 'footprints': sorted(pkgs)})- if no3d:- fail('no_3d_package', 'Fusion cannot ADD these parts: their footprints carry no 3D package (Fusion 2706 raises its PlacePartWorker assetAdded error on such an ADD): %s. Nothing was added.' % ', '.join('%s (%s)' % (x['part'], x['deviceset']) for x in no3d))- app.executeTextCommand("Electron.run USE '%s';" % Path(lbr).as_posix())+ # a compact .lbr (no line break between elements) is rewritten first: Fusion 2706 asserts+ # (PlacePartWorker assetAdded) on every ADD from one (#135; lbr_use.py)+ try: reformatted = use_library(app, lbr)+ except RuntimeError as ex:+ code = 'library_not_writable' if str(ex).startswith('library_not_writable') else 'library_unreadable'+ fail(code, str(ex).split(': ', 1)[-1] if code == 'library_not_writable' else str(ex)) pause = min(max(float(args.get('pauseMs', 0)), 0), 3000) / 1000.0 for c in cmds: c = c.strip()@@ -711,9 +702,9 @@ def sch_run(app, args): names = part_names() if names is not None and want not in names: return {'success': False, 'mutated': len(done) > 1, 'errorCode': 'add_failed', 'failedAt': len(done) - 1, 'failedCommand': c, 'part': want,- 'libraryFrom': used, 'librariesHeld': sorted(held), 'commands': len(done), 'documentName': name,+ 'libraryFrom': used, 'libraryReformatted': reformatted, 'librariesHeld': sorted(held), 'commands': len(done), 'documentName': name, 'error': 'ADD of %s did not land: Fusion found no such device in %s (%s). The commands before it ran.' % (want, libname,- 'the design holds that library, so the device is missing from it' if used == 'design' else 'the design does not hold that library and USE of the .lbr on disk does not make it addable on this Fusion'),+ 'the design holds that library, so the device is missing from it' if used == 'design' else 'Fusion did not take it from the .lbr loaded with USE'), '_hint': 'Seed the design with fusion_board_seed (it embeds the whole library), or add the part to the design\'s library; then run the rest of the commands.'} if pause: app.executeTextCommand('Electron.ImmediateRepaint /full')@@ -726,7 +717,7 @@ def sch_run(app, args): ok = not missing return {'success': ok, 'mutated': True, 'verified': ok, 'errorCode': None if ok else 'readback_mismatch', 'error': None if ok else 'parts not on the sheet after ADD: %s' % missing, 'documentName': name,- 'parts': after, 'added': [p for p in after if p not in before], 'nets': nets, 'commands': len(done), 'libraryFrom': used}+ 'parts': after, 'added': [p for p in after if p not in before], 'nets': nets, 'commands': len(done), 'libraryFrom': used, 'libraryReformatted': reformatted} def board_bom(board, args):@@ -862,7 +853,7 @@ def board_seed(args): fixed = args.get('fixed') or [] keep = {str(f.get('deviceset')).upper() for f in fixed} # the fixed parts' devices only (wholeLibrary:true embeds every one); schematic_run ADDs the rest- # through USE of the .lbr, which works when each part carries an addable 3D package (#135)+ # through USE of the .lbr if args.get('wholeLibrary') is not True: for d in list(lib.find('devicesets')): if d.get('name').upper() not in keep: lib.find('devicesets').remove(d)
rust/bridge/src/sch.rs+1−1@@ -1,7 +1,7 @@ //! The schematic verbs of the EDA Bridge Standard (sch_*): sch_script.py runs on Fusion's main //! thread through the script transport, as the routing verbs do. use serde_json::{json, Value};-const SOURCE: &str = include_str!("sch_script.py");+const SOURCE: &str = concat!(include_str!("sch_script.py"), "\n", include_str!("lbr_use.py")); pub fn owns(cmd: &str) -> bool { matches!(cmd, "sch_batch" | "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); }
rust/bridge/src/sch_script.py+9−5@@ -956,8 +956,8 @@ def sch_connect_pins(app, args): def _library_for(e, lib): """A library to ADD from: the name of one the design already holds (its schematic's copy is the- truth), or a .lbr path on this machine. Fusion 2706 loads a local .lbr through USE without an- error but ADD then finds nothing (live 2026-10-29), so a path is tried and refused honestly."""+ truth), or a .lbr path on this machine (loaded with use_library, which rewrites a compact file+ Fusion 2706 cannot ADD from; #135). If ADD still finds nothing, that is refused honestly.""" from pathlib import Path as _P held = {l_.get('name'): l_ for l_ in e.root.iter('library')} if lib in held:@@ -969,7 +969,7 @@ def _library_for(e, lib): def _not_loaded(path):- return ('Fusion did not load %s: a local .lbr is taken by USE without an error but ADD then finds nothing. '+ return ('Fusion did not take the part from %s: USE loaded it without an error but ADD found nothing. ' 'Add the library to the design first (Fusion\'s Library Manager, or fusion_install_library_bundle for a hub library), then pass its name as library' % path) @@ -1089,7 +1089,9 @@ def sch_place_part(app, args): 'footprints': [d.get('package') for d in devices if d.get('package')]} if dry(args): return plan_reply(e, **out)- if lpath: app.executeTextCommand("Electron.run USE '%s';" % _P(lpath).as_posix())+ if lpath:+ try: use_library(app, lpath)+ except RuntimeError as ex: sfail('library_not_writable', str(ex).split(': ', 1)[-1]) e.run() placed = {p_.get('name') for p_ in e.after.iter('part')} if lpath and placed == parts:@@ -1137,7 +1139,9 @@ def sch_swap_part(app, args): e.cmds.append('SET CONFIRM YES; VALUE %s %s; SET CONFIRM OFF;' % (quote(ref), quote(old_value[:40]))) if dry(args): return plan_reply(e, was={'deviceset': part.get('deviceset'), 'device': part.get('device'), 'library': part.get('library')})- if lpath: app.executeTextCommand("Electron.run USE '%s';" % _P(lpath).as_posix())+ if lpath:+ try: use_library(app, lpath)+ except RuntimeError as ex: sfail('library_not_writable', str(ex).split(': ', 1)[-1]) e.run() now_part = next((p_ for p_ in e.after.iter('part') if p_.get('name') == ref), None) if lpath and now_part is not None and now_part.get('library') == part.get('library') and now_part.get('deviceset') == part.get('deviceset') and now_part.get('device') == part.get('device'):
tests/test_lbr_use.pyadded+67@@ -0,0 +1,67 @@+"""lbr_use.py: a compact .lbr is rewritten with line breaks (Fusion 2706 cannot ADD from one, #135),+only whitespace changes, and a library already broken into lines is left alone."""+import os+import tempfile+import unittest+import xml.etree.ElementTree as ET++SRC = os.path.join(os.path.dirname(__file__), '..', 'rust', 'bridge', 'src', 'lbr_use.py')+NS = {}+exec(open(SRC, encoding='utf-8').read(), NS)++COMPACT = ('<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE eagle SYSTEM "eagle.dtd">\n<eagle version="9.6.2"><drawing><layers><layer number="1" name="Top" color="4" fill="1" visible="yes" active="yes"/></layers>'+ '<library><description>made by a <b>tool</b></description><packages><package name="R0603"><smd name="1" x="-0.762" y="0" dx="0.6" dy="0.7" layer="1"/>'+ '<text x="0" y="1" size="1" layer="25">>NAME</text></package></packages><symbols><symbol name="R"><pin name="A" x="0" y="0"/></symbol></symbols>'+ '<devicesets><deviceset name="RES"><gates><gate name="G$1" symbol="R" x="0" y="0"/></gates><devices><device name="" package="R0603"><connects>'+ '<connect gate="G$1" pin="A" pad="1"/></connects><technologies><technology name=""/></technologies></device></devices></deviceset></devicesets></library></drawing></eagle>')+BROKEN = COMPACT.replace('><', '>\n<')+HEADER_ONLY = COMPACT[:COMPACT.index('<library>')] + BROKEN[BROKEN.index('<library>'):]+++def shape(path):+ return NS['_lbr_shape'](ET.parse(path).getroot())+++class LbrUse(unittest.TestCase):+ def write(self, text):+ d = tempfile.mkdtemp()+ p = os.path.join(d, 'lib.lbr')+ with open(p, 'w', encoding='utf-8') as f: f.write(text)+ return p++ def test_compact_is_rewritten_with_whitespace_only(self):+ p = self.write(COMPACT)+ before = shape(p)+ self.assertTrue(NS['normalize_lbr'](p))+ self.assertEqual(shape(p), before)+ out = open(p, encoding='utf-8').read()+ self.assertIn('<!DOCTYPE eagle SYSTEM "eagle.dtd">', out)+ self.assertIn('>NAME', out)+ self.assertIn('<devicesets>\n<deviceset name="RES">', out)+ self.assertFalse(NS['lbr_needs_line_breaks'](open(p, 'rb').read()))+ self.assertFalse(os.path.exists(p + '.adom-tmp'))++ def test_second_pass_and_broken_files_are_left_alone(self):+ p = self.write(COMPACT)+ NS['normalize_lbr'](p)+ once = open(p, 'rb').read()+ self.assertFalse(NS['normalize_lbr'](p))+ self.assertEqual(open(p, 'rb').read(), once)+ for text in (BROKEN, HEADER_ONLY):+ q = self.write(text)+ self.assertFalse(NS['normalize_lbr'](q))+ self.assertEqual(open(q, encoding='utf-8').read(), text)++ def test_use_library_runs_use_after_the_rewrite(self):+ p = self.write(COMPACT)+ seen = []+ class App:+ def executeTextCommand(self, c): seen.append((c, open(p, 'rb').read()))+ self.assertTrue(NS['use_library'](App(), p))+ self.assertEqual(len(seen), 1)+ self.assertTrue(seen[0][0].startswith("Electron.run USE '"))+ self.assertFalse(NS['lbr_needs_line_breaks'](seen[0][1]))+++if __name__ == '__main__':+ unittest.main()