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 72 file(s)
11 files changed
+78−26
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.228+1.9.229
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.228",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.228/adom-bridge-fusion-v1.9.228.zip",- "sha256": "bf1ab587b0cf1ebcf176ffa60e8464cb9fac0c8f4dbf74782b7226d3cb090393",- "size": 524308,+ "version": "1.9.229",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.229/adom-bridge-fusion-v1.9.229.zip",+ "sha256": "dafb503099f9b14c3f9e4dda99afd40acea8ba68008c52e58780d5c7ed0fab58",+ "size": 525216, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.228",+ "version": "1.9.229", "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.9.228"+version = "1.9.229" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.228"+version = "1.9.229" edition = "2021" [dependencies]
describe.py+3−3@@ -15,7 +15,7 @@ _STATUS = "fusion_get_app_state" _T = [ # ── Lifecycle ────────────────────────────────────────────────────────── ("fusion_preflight_lbr", "PRE-ANALYZE a .lbr: will Fusion open it? Offline and instant - no Fusion needed, nothing launched. Runs the named checks the bridge learned from real Fusion rejections: file-readable, size-sane (truncated-transfer corpse detection), xml-parses, root-is-eagle, packages-nonempty (Fusion's silent 'No Components'), symbols-present, devicesets-present (missing ones make EDIT raise Create-new prompts), connects-resolve (dead wiring renders fine and connects nothing). fusion_open_lbr and fusion_create_library run the same gate automatically; call this directly to gate a batch BEFORE staging.", {"libraryPath": "str (Windows .lbr path)"}, {"success": "bool", "preflight": "{ok, checks[], errors[]}"}, 10, None, True, {"libraryPath": "C:/Temp/PART.lbr"}),- ("fusion_create_library", "ONE-CALL library creation for web-control (one call = one consent). TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates; top-level `verdict` proven/not_proven is the score - `stage:done` only means the chain finished). On the FINAL fetch pass {inlineFrames:true} to get each evidence frame as a base64 thumbnailDataUrl a browser can render (plain polls return box paths only). The chain closes its library document on success AND failure - batches need no between-part sweeping. stepPath is required for a PROVEN library - no real 3D body without it.", {"libraryPath": "optional str (shape A, or output path for B)", "kicadSymbolPath": "optional str", "kicadFootprintPath": "optional str", "stepPath": "optional str (required for proven)", "pinMap": "optional {pinName: padName}", "name": "optional str"}, {"started": "bool", "statusVerb": "str"}, 30, "fusion_prove_result", True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}),+ ("fusion_create_library", "ONE-CALL library creation for web-control (one call = one consent). EVERY LIBRARY IS MULTI-PART - a single-part library is simply parts:[one entry], and there is no separate single-part verb to choose. Pass parts:[{package, lbrPath, modelPath}] for any number of parts; the older {libraryPath, stepPath} shape still works and is normalised into a one-element parts list, so both run the identical code path and the ETA scales with the part count either way. TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates; top-level `verdict` proven/not_proven is the score - `stage:done` only means the chain finished). On the FINAL fetch pass {inlineFrames:true} to get each evidence frame as a base64 thumbnailDataUrl a browser can render (plain polls return box paths only). The chain closes its library document on success AND failure - batches need no between-part sweeping. stepPath is required for a PROVEN library - no real 3D body without it.", {"parts": "optional [{package, lbrPath, modelPath}] - THE canonical shape, any count", "libraryPath": "optional str (shape A, or output path for B)", "kicadSymbolPath": "optional str", "kicadFootprintPath": "optional str", "stepPath": "optional str (required for proven)", "pinMap": "optional {pinName: padName}", "name": "optional str"}, {"started": "bool", "statusVerb": "str"}, 30, "fusion_prove_result", True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}), ("fusion_start", "Launch Fusion 360 (if needed) and wait for the AdomBridge add-in to be ready.", {}, {"success": "bool", "addinReady": "bool", "status": "str"}, 150, None, True, {}), ("fusion_stop", "Gracefully STOP Fusion 360: close docs cleanly + WM_CLOSE, then wait. NO force-kill - the clean opposite of fusion_start. If a window will not close it returns failure; use fusion_kill then.",@@ -134,7 +134,7 @@ _T = [ ("fusion_open_lbr", "Import an EAGLE .lbr library into Fusion's Electronics LIBRARY editor (Content Manager / Electronics Library workspace), not a PCB design. PITFALL: filePath MUST be a WINDOWS-local path (C:/...). The chip-fetcher/adom-lbr pipeline runs in the cloud container, and there is NO container-to-Windows push verb, so STAGE the .lbr onto Windows first (serve it on a proxied port + Invoke-WebRequest via shell_execute), then pass the C:/ path. PITFALL: an adom-lbr .lbr is 2D ONLY - symbol + footprint + a PLACEHOLDER 3D package; the real 3D chip is NOT attached (that needs a Fusion cloud URN). To attach the 3D, see the 'fusion-libraries' skill. Pass verify:true to round-trip-confirm Fusion parsed the deviceset, then check fusion_get_app_state (activeWorkspace 'Electronics Library').", {"filePath": "required str (.lbr WINDOWS path C:/...; stage from container first)", "symbolName": "optional str", "verify": "optional bool"}, {"success": "bool", "postOpenScreenshot": "{savedTo}"}, 50, None, False, {"filePath": "C:/Users/<user>/adom-lib/nRF54L15.lbr"}),- ("fusion_attach_3d_package", "Attach a real 3D chip model to a library package, end to end: opens the .lbr (library active), runs Electron.Create3DPackage to enter the Package3DEnvironment showing the footprint, imports the STEP + auto-orients it flat on the footprint, then executes Package3DStop (FINISH). This BINDS the 3D onto the deviceset (Content Manager shows the 3D nested under the package; the Packages 'Package' column flips Placeholder->part-name). ONE desktop-side step remains: Fusion pops a modal Save dialog (an OWNED popup) - the response _hint gives the exact desktop_ui_click to confirm it. AFTER: re-grab desktop_screenshot_window and check ownedPopupCount for errors; the 3D preview LAGS a beat before showing the real chip. Both paths (filePath/modelPath) are WINDOWS paths - stage from the container first.",+ ("fusion_attach_3d_package", "LOW-LEVEL primitive: attaches ONE model to ONE package. Prefer fusion_create_library with parts[]. Attach a real 3D chip model to a library package, end to end: opens the .lbr (library active), runs Electron.Create3DPackage to enter the Package3DEnvironment showing the footprint, imports the STEP + auto-orients it flat on the footprint, then executes Package3DStop (FINISH). This BINDS the 3D onto the deviceset (Content Manager shows the 3D nested under the package; the Packages 'Package' column flips Placeholder->part-name). ONE desktop-side step remains: Fusion pops a modal Save dialog (an OWNED popup) - the response _hint gives the exact desktop_ui_click to confirm it. AFTER: re-grab desktop_screenshot_window and check ownedPopupCount for errors; the 3D preview LAGS a beat before showing the real chip. Both paths (filePath/modelPath) are WINDOWS paths - stage from the container first.", {"filePath": "required str (.lbr WINDOWS path C:/...)", "modelPath": "required str (.step WINDOWS path C:/...)", "packageName": "optional str"}, {"success": "bool", "savePending": "bool", "data": "{steps}", "_hint": "the Save-dialog click + verify steps"}, 240, _STATUS, True, {"filePath": "C:/Users/<user>/adom-lib/ADS8588SIPM.fusion.lbr", "modelPath": "C:/Users/<user>/adom-lib/ADS8588SIPM.step", "packageName": "ADS8588SIPM"}), ("fusion_install_fusion", "Install Fusion 360 FOR the user - downloads Autodesk's official Client Downloader and streams the free trial silently, with NO shell_execute and NO AD approval gate (runs inside the trusted bridge). Elevation-aware: --globalinstall when admin, PER-USER otherwise (a non-elevated globalinstall dies silently on UAC - learned live). Returns promptly; poll fusion_readiness (reports installing:true) until installed:true, then fusion_start. Declared as detect.installVerb so AD's bridge_readiness recommends it.",@@ -207,7 +207,7 @@ _T = [ ("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_bind_library_3d", "The CORRECT library in ONE call (the Library button's real bar: the 3D chip renders in the Content Manager preview): mints the part's 3D as a cloud f3d (make_3d_package), injects the wip_urn binding into the .lbr ON DISK (packages3d + package3dinstances), and reopens the library. Needs a signed-in Fusion session - the one cloud-dependent step in the wiki-to-EE circle. Autodesk's thumbnail preview lags ~2-3 min behind a fresh mint. NOTE: distinct from the older fusion_build_library_3d (multi-part batch pipeline).", {"libraryPath": "required str (WINDOWS .lbr path)", "stepPath": "required str (WINDOWS .step path)", "deviceset": "optional str (defaults to the library's first)"}, {"success": "bool", "wip_urn": "str", "bound": "bool", "reopened": "bool"}, 480, None, True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}),+ ("fusion_bind_library_3d", "LOW-LEVEL primitive: binds ONE STEP onto one deviceset. Prefer fusion_create_library with parts[] (which handles one part or many through a single path); reach for this only when you are driving a single bind deliberately. The CORRECT library in ONE call (the Library button's real bar: the 3D chip renders in the Content Manager preview): mints the part's 3D as a cloud f3d (make_3d_package), injects the wip_urn binding into the .lbr ON DISK (packages3d + package3dinstances), and reopens the library. Needs a signed-in Fusion session - the one cloud-dependent step in the wiki-to-EE circle. Autodesk's thumbnail preview lags ~2-3 min behind a fresh mint. NOTE: distinct from the older fusion_build_library_3d (multi-part batch pipeline).", {"libraryPath": "required str (WINDOWS .lbr path)", "stepPath": "required str (WINDOWS .step path)", "deviceset": "optional str (defaults to the library's first)"}, {"success": "bool", "wip_urn": "str", "bound": "bool", "reopened": "bool"}, 480, None, True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}), ("fusion_show_library", "Eight-surfaces contract: the LIBRARY surface. libraryPath (WINDOWS .lbr) opens it; name alone handles the already-open case (symmetric with kicad_show_library). Background; returns a screenshot path.", {"capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).", "libraryPath": "optional str (WINDOWS .lbr)", "name": "optional str (already-open library name)"}, {"success": "bool", "view": "str", "screenshot": "str"}, 240, None, True, {"libraryPath": "C:/Temp/PART.lbr"}), ("fusion_show_project", "Eight-surfaces contract: bring the PROJECT/design to its top level. No args = activate this session's generated test project (fusion_make_test_project). query = cloud design name (already-open match wins, else fire-and-poll open). urn = open_by_urn passthrough. Background; returns a screenshot path.", {"capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).", "query": "optional str (cloud design name)", "urn": "optional str", "fileName": "alias of query"}, {"success": "bool", "view": "str", "document": "str", "screenshot": "str"}, 240, "fusion_get_app_state", True, {"query": "BQ25792"}), ("fusion_set_window_sizing", "Read/toggle auto-sizing Fusion's WINDOW after launch (STICKY, default ON). Fusion opens at whatever geometry it last had, which after an automated launch is often a small default; the bridge grows it to ~92% of the monitor work area, centered, using SetWindowPos with NOACTIVATE (never ShowWindow/maximize) so focus is never stolen. A window the user maximized, or already sized to >=60% of the work area, is left alone. No args = read.", {"enabled": "optional bool (omit to read)"}, {"success": "bool", "sizeFusionWindow": "bool"}, 15, "fusion_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.9.228"+DASH_VERSION="1.9.229" 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.9.228",+ "version": "1.9.229", "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+11−8@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.228",+ "version": "1.9.229", "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": {@@ -11,6 +11,10 @@ "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -75,7 +79,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "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.",@@ -104,9 +111,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }-}+ ]+}
server.py+31−5@@ -11735,6 +11735,16 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: sym_p = (args.get("kicadSymbolPath") or "").replace("\\", "/") mod_p = (args.get("kicadFootprintPath") or "").replace("\\", "/") step_p = (args.get("stepPath") or "").replace("\\", "/")+ # ⛔ EVERY LIBRARY IS MULTI-PART. ONE PART IS N=1 (John, 2026-08-28: "any library+ # probably should just be considered multi-part and if it then just so happens to have+ # one part, so be it").+ # This used to be a whole SINGULAR family (create_library{stepPath},+ # bind_library_3d{stepPath}, attach_3d_package{modelPath}) beside a PLURAL one+ # (build_library_3d{parts[]}), which forced a caller to know their part count before+ # they could even pick a verb. Part count is DATA, not a different operation. `parts`+ # is now the canonical input and `stepPath` is sugar that normalises into it, so both+ # shapes run the identical code path and a 1-part library is not a special case.+ parts_in = args.get("parts") or [] if not lbr_p and not (sym_p or mod_p): return {"success": False, "errorCode": "no_input", "error": "Give me EITHER libraryPath (a ready .lbr) OR kicadSymbolPath / "@@ -11833,12 +11843,12 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: # A progress bar that lies is worse than none: the user concludes it has hung. # Per-part seconds are MEASURED per machine under their own key, so the bar is # paced by what THIS box actually does, and only the seed is a literal.- _n_parts = max(1, len((args.get("parts") or [])) or 1)+ _n_parts = max(1, len(args.get("parts") or []) or 1) _per_part, _pp_conf = _tmc.estimate("stage:bind_per_part", 60.0) _bind_est = _per_part * _n_parts _STAGES = [("launch", 60.0, _needs_launch), ("convert", 5.0, bool(sym_p or mod_p)),- ("bind", _bind_est, bool(step_p)),+ ("bind", _bind_est, bool(step_p or parts_in)), ("prove", 300.0, True)] state["plan"] = [{"stage": n, "estimatedSec": round(_tmc.estimate("stage:" + n, f)[0] or f, 1), "confidence": _tmc.estimate("stage:" + n, f)[1]}@@ -11889,13 +11899,29 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict: _leave("convert") target_lbr = conv.get("libraryPath") state["libraryPath"] = target_lbr- if step_p:+ # Normalise to the plural shape HERE, once the target .lbr is known, because a+ # single stepPath needs the library's own package name to become a part entry.+ _parts = list(parts_in)+ if not _parts and step_p:+ try:+ _pkgs = _library_artifact_names(target_lbr).get("pac") or []+ except Exception:+ _pkgs = []+ _parts = [{"package": _pkgs[0] if _pkgs else "",+ "lbrPath": target_lbr, "modelPath": step_p}]+ state["partsTotal"] = len(_parts)+ if _parts: _enter("binding 3D (STEP -> f3d -> package3d)")- bind = dispatch_command("bind_library_3d",- {"libraryPath": target_lbr, "stepPath": step_p})+ # ONE binder for one part or seven. Run synchronously: this chain is+ # already detached, so a second background thread would only hide the+ # per-part progress the caller is polling for.+ bind = dispatch_command("build_library_3d",+ {"lbrPath": target_lbr, "parts": _parts,+ "outLbrPath": target_lbr, "async": False}) state["bind"] = {k: bind.get(k) for k in ("success", "error", "wip_urn", "reopened", "reopenError")}+ state["bind"]["parts"] = bind.get("results") or bind.get("parts") if not bind.get("success"): # NEVER surface a failure with error:null (#49, John 2026-08-23). If the # bind handler somehow returns no reason, synthesize one from whatever it
skills/fusion-web-control/SKILL.md+23@@ -134,3 +134,26 @@ variant model that lets an automotive-grade part and a baseline one share one sy footprint and one 3D body, are covered in **`fusion-part-metadata`**. Check `fusion_library_parts {libraryPath}` before calling any library complete - `ok: true` from preflight only means the file OPENS.++## Every library is multi-part. One part is N=1.++There is no single-part import verb to choose, deliberately. **Part count is data, not a+different operation**, and a caller should never have to know how many parts a library holds+before they can pick a verb.++```+fusion_create_library {parts: [{package, lbrPath, modelPath}, ...]} <- any count+fusion_create_library {libraryPath, stepPath} <- sugar for N=1+```++The second form is normalised into a one-element `parts` list, so both run the **identical**+code path, through the same binder, with the same per-part progress and an ETA that scales+with the count. A one-part library is not a special case with its own bugs.++`fusion_bind_library_3d` and `fusion_attach_3d_package` still exist as **low-level+primitives** for a single deliberate bind. They are not the import lane, and reaching for+them because "this library only has one part" is the mistake this unification removes.++**History, so nobody re-introduces it:** this used to be a singular family+(`create_library{stepPath}`, `bind_library_3d{stepPath}`, `attach_3d_package{modelPath}`)+beside a plural one (`build_library_3d{parts[]}`). Two families, split on the wrong axis.