app
fusion - the Fusion 360 bridge (macOS)
Public Made by Adomby adom
This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Desktop loads the bridge runtime itself from the release zip.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
"""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 local 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