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
Merge PR #22: Add fusion_assembly_bom and fusion_physical_properties verbs
5 files changed
+219−1
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.7.9+1.7.10
addin/AdomBridge/commands/__init__.py+4@@ -50,6 +50,8 @@ from .manufacturing import ( ) from .silkscreen_capture import handle_take_silkscreen_screenshot from .modeling import handle_run_modeling_script+from .assembly_bom import handle_assembly_bom+from .physical_properties import handle_physical_properties COMMAND_HANDLERS = { # App state@@ -71,6 +73,8 @@ COMMAND_HANDLERS = { "export_sat": handle_export_sat, "export_skp": handle_export_skp, "get_design_info": handle_get_design_info,+ "assembly_bom": handle_assembly_bom,+ "physical_properties": handle_physical_properties, "get_parameters": handle_get_parameters, "set_parameter": handle_set_parameter, "take_screenshot": handle_take_screenshot,
addin/AdomBridge/commands/assembly_bom.pyadded+140@@ -0,0 +1,140 @@+"""Structured, kit-aware mechanical assembly BOM (mirrors Fusion's Manage -> BOM).++The electronics `export_bom` only works on an open PCB board. This is its mechanical+counterpart: it walks the active Design assembly and returns a purchasing BOM that+respects the component hierarchy. It recurses through organizational subassemblies but+counts a physical part or a purchased kit/unit ONCE, so hardware modeled INSIDE a kit+(for example a "... with fasteners" bracket, or a bearing/pulley unit) is not+double-counted the way a flat allOccurrences walk would count it.+"""++import csv++import adsk.core+import adsk.fusion+++def _material_name(comp):+ try:+ if comp.material:+ return comp.material.name+ except Exception:+ pass+ try:+ if comp.bRepBodies.count > 0 and comp.bRepBodies.item(0).material:+ return comp.bRepBodies.item(0).material.name+ except Exception:+ pass+ return ""+++def _attr(comp, name):+ try:+ v = getattr(comp, name, "") or ""+ return v.strip()+ except Exception:+ return ""+++def handle_assembly_bom(app: adsk.core.Application, args: dict) -> dict:+ """Return a structured, kit-aware parts list for the active mechanical design.++ args:+ treatAsUnit optional list of name substrings whose subassemblies are counted once+ and NOT exploded (default: ["with fasteners"]).+ exclude optional list of top-level component-name prefixes to skip entirely.+ includePhysicalProperties optional bool -- add volume_cm3 / mass_kg per line.+ outputPath optional str -- also write a CSV to this Windows path.+ """+ design = adsk.fusion.Design.cast(app.activeProduct)+ if not design:+ return {"success": False, "error": "No active Fusion Design.",+ "_hint": "Open a mechanical design in the Design workspace, then retry."}++ args = args or {}+ kit = [k.lower() for k in (args.get("treatAsUnit") or ["with fasteners", "with fastener"])]+ exclude = tuple(args.get("exclude") or [])+ include_pp = bool(args.get("includePhysicalProperties", False))++ def is_leaf(comp):+ n = comp.name.lower()+ if any(k in n for k in kit):+ return True+ return comp.bRepBodies.count > 0 # a physical part is a leaf++ parts = {}++ def walk(occs):+ for i in range(occs.count):+ occ = occs.item(i)+ comp = occ.component+ name = comp.name+ if any(name.startswith(e) for e in exclude):+ continue+ children = occ.childOccurrences+ has_children = children is not None and children.count > 0+ if is_leaf(comp):+ row = parts.get(name)+ if row is None:+ row = {+ "componentName": name,+ "partNumber": _attr(comp, "partNumber"),+ "description": _attr(comp, "description"),+ "material": _material_name(comp),+ "quantity": 1,+ "bodies": comp.bRepBodies.count,+ }+ if include_pp:+ try:+ pp = comp.getPhysicalProperties(+ adsk.fusion.CalculationAccuracy.LowCalculationAccuracy)+ row["volume_cm3"] = round(pp.volume, 4)+ row["mass_kg"] = round(pp.mass, 6)+ except Exception:+ row["volume_cm3"] = None+ row["mass_kg"] = None+ parts[name] = row+ else:+ row["quantity"] += 1+ elif has_children:+ walk(children)++ walk(design.rootComponent.occurrences)+ rows = list(parts.values())++ doc_name = design.rootComponent.name+ try:+ if design.parentDocument:+ doc_name = design.parentDocument.name+ except Exception:+ pass++ out = {+ "success": True,+ "design": doc_name,+ "partCount": len(rows),+ "totalInstances": sum(r["quantity"] for r in rows),+ "treatAsUnit": kit,+ "parts": rows,+ }++ outpath = args.get("outputPath")+ if outpath:+ try:+ cols = ["Part Number", "Part Name", "Description", "Material", "Quantity"]+ if include_pp:+ cols += ["Volume cm3", "Mass kg"]+ with open(outpath, "w", newline="", encoding="utf-8") as f:+ w = csv.writer(f)+ w.writerow(cols)+ for r in rows:+ line = [r["partNumber"], r["componentName"], r["description"],+ r["material"], r["quantity"]]+ if include_pp:+ line += [r.get("volume_cm3"), r.get("mass_kg")]+ w.writerow(line)+ out["outputPath"] = outpath+ except Exception as e:+ out["csvError"] = str(e)++ return out
addin/AdomBridge/commands/physical_properties.pyadded+64@@ -0,0 +1,64 @@+"""Per-component physical properties (volume, mass, density, area, center of mass).++Fusion exposes physical properties only through the modeling API, so before this verb a+caller had to run getPhysicalProperties inside fusion_run_modeling_script. This verb reads+them directly, per component, so BOM/costing tools can price by volume or mass without a+custom script.+"""++import adsk.core+import adsk.fusion++_ACC = {+ "low": adsk.fusion.CalculationAccuracy.LowCalculationAccuracy,+ "medium": adsk.fusion.CalculationAccuracy.MediumCalculationAccuracy,+ "high": adsk.fusion.CalculationAccuracy.HighCalculationAccuracy,+ "veryhigh": adsk.fusion.CalculationAccuracy.VeryHighCalculationAccuracy,+}+++def handle_physical_properties(app: adsk.core.Application, args: dict) -> dict:+ """Return physical properties per unique component in the active design.++ args:+ names optional list of component names to measure (default: all).+ accuracy optional "low" | "medium" | "high" | "veryhigh" (default: low).+ """+ design = adsk.fusion.Design.cast(app.activeProduct)+ if not design:+ return {"success": False, "error": "No active Fusion Design.",+ "_hint": "Open a design in the Design workspace, then retry."}++ args = args or {}+ names = set(args.get("names") or [])+ acc = _ACC.get((args.get("accuracy") or "low").lower(),+ adsk.fusion.CalculationAccuracy.LowCalculationAccuracy)++ out = {}+ seen = set()+ for occ in design.rootComponent.allOccurrences:+ comp = occ.component+ if comp.name in seen:+ continue+ if names and comp.name not in names:+ continue+ seen.add(comp.name)+ try:+ pp = comp.getPhysicalProperties(acc)+ entry = {+ "volume_cm3": round(pp.volume, 4),+ "mass_kg": round(pp.mass, 6),+ "density": round(pp.density, 6),+ "area_cm2": round(pp.area, 4),+ }+ try:+ com = pp.centerOfMass+ entry["center_of_mass"] = [round(com.x, 4), round(com.y, 4), round(com.z, 4)]+ except Exception:+ pass+ out[comp.name] = entry+ except Exception as e:+ out[comp.name] = {"error": str(e)[:80]}++ return {"success": True, "count": len(out), "accuracy": (args.get("accuracy") or "low"),+ "properties": out}
describe.py+10@@ -135,6 +135,16 @@ _T = [ {"script": "required str (Python using adsk; globals: adsk, app, ui, design)"}, {"result": "obj", "documentName": "str"}, 180, None, True, {"script": "import adsk.fusion\n# ...build geometry..."}), + ("fusion_assembly_bom", "Structured, kit-aware MECHANICAL bill of materials for the active Design assembly (the counterpart to the electronics-only fusion_export_bom). Recurses organizational subassemblies but counts a physical part or a purchased kit/unit ONCE, so hardware modeled inside a kit (a '... with fasteners' bracket, a bearing/pulley) is NOT double-counted the way a flat allOccurrences walk counts it. Matches Fusion's Manage -> BOM quantities. Optionally writes a CSV.",+ {"treatAsUnit": "optional [str] name substrings counted as one unit (default ['with fasteners'])", "exclude": "optional [str] top-level name prefixes to skip", "includePhysicalProperties": "optional bool (adds volume_cm3/mass_kg per line)", "outputPath": "optional str (Windows CSV path)"},+ {"success": "bool", "design": "str", "partCount": "int", "totalInstances": "int", "parts": "[{componentName, partNumber, description, material, quantity, bodies}]"},+ 120, None, False, {"treatAsUnit": ["with fasteners"], "includePhysicalProperties": True}),++ ("fusion_physical_properties", "Per-component physical properties (volume cm3, mass kg, density, area, center of mass) for the active design. Reads them directly so BOM/costing tools no longer have to run getPhysicalProperties inside a modeling script. Pass names to scope to specific components.",+ {"names": "optional [str] component names to measure (default all)", "accuracy": "optional low|medium|high|veryhigh (default low)"},+ {"success": "bool", "count": "int", "properties": "{name: {volume_cm3, mass_kg, density, area_cm2, center_of_mass}}"},+ 120, None, False, {"accuracy": "low"}),+ ("fusion_set_preference", "Set Fusion appearance/navigation preferences in the LIVE session - applies immediately, NO restart. Keys: theme (light/darkblue/darkgray/classic/device/dark; some builds only ship LightGray/DarkBlue/Device, failures reported per-key), invertScrollZoom (bool - mouse scroll/zoom direction), orbitScheme (fusion360/alias/inventor/solidworks/tinkercad/powermill), modelingOrientation (yup/zup), gestureNav (bool), cameraPivot (bool), lengthUnit (mm/cm/m/in/ft - active design). Per-key outcome in `applied`, resulting state in `current`.", {"theme": "optional str", "invertScrollZoom": "optional bool", "orbitScheme": "optional str", "modelingOrientation": "optional str", "gestureNav": "optional bool", "cameraPivot": "optional bool", "lengthUnit": "optional str"}, {"applied": "obj", "current": "obj"}, 30, None, True, {"theme": "dark"}),