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
Design efficiency (PR #25 by Drew Owens); add-in 1.0.25
11 files changed
+714−31
diff truncated
CHANGELOG.md+18addin/AdomBridge/AdomBridge.manifest+1−1addin/AdomBridge/commands/mechanical/__init__.py+3−2addin/AdomBridge/commands/mechanical/assemblies.py+57−2addin/AdomBridge/commands/mechanical/core.py+24addin/AdomBridge/commands/mechanical/efficiency.py+231addin/AdomBridge/commands/mechanical/features.py+100−17addin/AdomBridge/commands/mechanical/schema.json+136−4docs/MECHANICAL.md+7−1rust/core/tests/golden/mech_capabilities.json+137−4rust/core/tests/golden/mech_validation.jsonCHANGELOG.md+18@@ -3,6 +3,24 @@ 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.18 (unreleased)++From Drew Owens (PR #25). Design efficiency: measure what makes a design slow, and stop the bridge from making it slow.++- `efficiency_report` reads what makes a design slow (timeline make-up and health, Position snapshots, Move features,+ size, repeated definitions made by Paste New, instance-weighted faces, imported history, wires in a parametric design,+ big sketches, pattern compute options, modeled threads, meshes, empty components) and returns findings with the fix.+ It never changes the model and is journaled like `bom_export`; a 4,579-occurrence cell took 76 s.+- `occurrence_update` keeps a move by editing the occurrence's initial position (Fusion's Edit Initial Position) when+ Fusion allows it, so nothing joins the timeline; an earlier occurrence is edited with the marker rolled back to it and+ restored. `method` chooses, and the reply's `placement` says which was used. Nine moves: 0 snapshots, against 9.+- Rectangular and circular patterns take features, faces or components (occurrences, patterned in their parent) as well+ as bodies, with `computeOption` (identical by default, optimized, adjust). The revision guard accepts a component+ pattern's copies. On a 100-hole plate a feature pattern recomputed in 0.24 s, the sketch of circles in 0.87 s, adjust+ in 2.42 s.+- The `fusion-design-efficiency` skill: the playbook, with those benchmarks and its sources.+- The add-in is 1.0.25 (new op `efficiency_report`: 62 typed operations); the exe embeds the new `schema.json`.+ ## 1.13.17 (2026-10-29) From Drew Owens (PR #24). The mechanical BOM reads a large real assembly right, and the revision guard accepts assemblies built from referenced designs.
addin/AdomBridge/AdomBridge.manifest+1−1@@ -6,7 +6,7 @@ "description": { "": "Adom Desktop Conduit bridge add-in. Exposes a localhost HTTP API for external automation of Fusion 360." },- "version": "1.0.24",+ "version": "1.0.25", "runOnStartup": true, "supportedOS": "windows" }
addin/AdomBridge/commands/mechanical/__init__.py+3−2@@ -14,7 +14,7 @@ import adsk.core from .core import SESSION_ID, MechanicalError, document_identity, require from .operations import OPERATIONS, WRITES from .journal import Journal, JournalError-from . import handoff, transactions, sketches, features, assemblies, purchased, bill_of_materials, geometry, sketch_edits, regular_joints, motion_links, configurations, render_scene, exploded_view, exploded_preview+from . import handoff, transactions, sketches, features, assemblies, purchased, bill_of_materials, geometry, sketch_edits, regular_joints, motion_links, configurations, render_scene, exploded_view, exploded_preview, efficiency for _name, _handler in {**sketches.OPERATIONS, **features.OPERATIONS, **assemblies.OPERATIONS, **geometry.OPERATIONS, **sketch_edits.OPERATIONS, **regular_joints.OPERATIONS, **motion_links.OPERATIONS}.items():@@ -32,7 +32,8 @@ for _name, _handler in {**exploded_preview.WRITES, **exploded_preview.READS}.ite WRITES.update(exploded_preview.WRITES) OPERATIONS.update(purchased.OPERATIONS) OPERATIONS.update(bill_of_materials.OPERATIONS)-WRITES.update(purchased.WRITES | bill_of_materials.WRITES)+OPERATIONS.update(efficiency.OPERATIONS)+WRITES.update(purchased.WRITES | bill_of_materials.WRITES | efficiency.WRITES) OPERATIONS["transaction_probe"] = transactions.probe WRITES.add("transaction_probe")
addin/AdomBridge/commands/mechanical/assemblies.py+57−2@@ -157,6 +157,46 @@ def occurrence_insert(design, value, args): return lambda fresh: {**created_occurrence_result(design, parent, occ, fresh, expected), "positionCaptured": captured} +def initial_position_editable(occ):+ """Can Fusion edit this occurrence's initial position? (The API spells the property isVaild...)"""+ for name in ("isVaildForEditInitialPosition", "isValidForEditInitialPosition"):+ try:+ value = getattr(occ, name, None)+ except RuntimeError:+ return False+ if value is not None:+ return bool(value)+ return False+++def edit_initial_position(design, occ, matrix):+ """Set the occurrence's initial position. Fusion edits only the last occurrence in the timeline in+ place ("Cannot be edited before rolling back"), so roll the marker to just after this occurrence's+ creation, edit it there and put the marker back, as the Edit Initial Position command does."""+ try:+ occ.initialTransform = matrix+ return True+ except RuntimeError:+ pass+ try:+ item = occ.timelineObject+ except RuntimeError:+ return False+ if item is None:+ return False+ timeline = design.timeline+ marker = timeline.markerPosition+ try:+ if not item.rollTo(False):+ return False+ occ.initialTransform = matrix+ return True+ except RuntimeError:+ return False+ finally:+ timeline.markerPosition = marker++ def occurrence_update(design, value, args): require(any(k in args for k in ("transform", "grounded", "groundedToParent")), "invalid_input", "Occurrence update requires a placement or grounding change.") occ = resolve(design, value, args["occurrence"], ("Occurrence",))@@ -164,14 +204,29 @@ def occurrence_update(design, value, args): occ.isGrounded = args["grounded"] if "groundedToParent" in args: occ.isGroundToParent = args["groundedToParent"]+ placed = None if "transform" in args: require(not occ.isGrounded and not occ.isGroundToParent, "grounded_occurrence", "Unground the occurrence explicitly before moving it.") expected = transform(args["transform"])- occ.transform2 = expected+ method = args.get("method", "auto")+ # Edit Initial Position: the placement the occurrence was created with, so no Position snapshot+ # joins the timeline. Fusion allows it for a root-level occurrence without joints or patterns+ # (the property's name is misspelled in the API).+ if method != "capture" and initial_position_editable(occ):+ if edit_initial_position(design, occ, expected):+ placed = "initial_position"+ else:+ require(method == "auto", "initial_position_refused", "Fusion refused to edit this occurrence's initial position.")+ require(placed or method != "initial_position", "initial_position_unavailable",+ "Fusion cannot edit this occurrence's initial position (nested, patterned or jointed): use method capture.")+ if placed is None:+ occ.transform2 = expected+ placed = "capture" actual = occ.transform2.asArray() require(all(abs(a-b) < 1e-8 for a,b in zip(actual, expected.asArray())), "placement_refused", "Constraints prevented the requested occurrence transform.") if design.snapshots.hasPendingSnapshot: require(design.snapshots.add() is not None, "placement_failed", "Could not capture occurrence position in the timeline.")+ placed = "capture" require("grounded" not in args or occ.isGrounded == args["grounded"], "grounding_refused", "Grounding did not match the request.") require("groundedToParent" not in args or occ.isGroundToParent == args["groundedToParent"], "grounding_refused", "Parent grounding did not match the request.") def result(fresh):@@ -181,7 +236,7 @@ def occurrence_update(design, value, args): "placement_refused", "Recompute did not retain the requested occurrence transform.") require("grounded" not in args or occ.isGrounded == args["grounded"], "grounding_refused", "Recompute did not retain grounding.") require("groundedToParent" not in args or occ.isGroundToParent == args["groundedToParent"], "grounding_refused", "Recompute did not retain parent grounding.")- return {"occurrence": occurrence_data(occ, fresh)}+ return {"occurrence": occurrence_data(occ, fresh), **({"placement": placed} if placed else {})} return result
addin/AdomBridge/commands/mechanical/core.py+24@@ -261,10 +261,34 @@ def occurrence_suppression(occurrence): except RuntimeError: if inside_reference(occurrence): return None+ element = pattern_element(occurrence)+ if element is not None:+ return bool(element.isSuppressed) raise return bool(timeline.isSuppressed) if timeline is not None else None +def pattern_element(occurrence):+ """The pattern element that made an occurrence, or None. A component pattern's copies have no+ timeline object of their own (Fusion raises InternalValidationError); the element carries their+ suppression. Looked up only on that error, in the owning component's patterns."""+ try:+ context = getattr(occurrence, "assemblyContext", None)+ owner = context.component if context is not None else occurrence.component.parentDesign.rootComponent+ path = occurrence.fullPathName+ groups = (owner.features.rectangularPatternFeatures, owner.features.circularPatternFeatures)+ except (AttributeError, RuntimeError):+ return None+ for group in groups:+ for i in range(group.count):+ elements = group.item(i).patternElements+ for j in range(elements.count):+ element = elements.item(j)+ if any(o.fullPathName == path for o in element.occurrences):+ return element+ return None++ def target(app, args, write=False, model_write=True): doc, design, key = active(app) wanted = args.get("target")
addin/AdomBridge/commands/mechanical/efficiency.pyadded+231@@ -0,0 +1,231 @@+"""Design efficiency: what makes a design slow to recompute, open and edit, read from the model and+held against the playbook (skills/fusion-design-efficiency). It never changes the model and needs no+revision digest; a time budget stops the costly passes early and says so.+findings() is pure, so every rule is tested offline."""+import re+import time++import adsk.core+import adsk.fusion++from .core import active, require+from .bom_views import GAUGE, WIRING_HARDWARE++# limits people cite (Autodesk's large-assembly article, AU classes, forum benchmarks); see the playbook+LIMITS = {"occurrences": 1000, "components": 1000, "timeline": 500, "sketchCurves": 200, "baseFeatures": 50,+ "facesPerDefinition": 20000, "wires": 20}+COPY_NAME = re.compile(r" \(\d+\)$")+++def size(things):+ """Count a Fusion collection (.count) or vector (len): allJoints and friends return vectors."""+ count = getattr(things, "count", None)+ return count if isinstance(count, int) else len(things)+++def _kind(entity):+ return entity.objectType.split("::")[-1] if entity is not None else "None"+++def measure(app, design, budget_seconds=45.0):+ """The numbers the playbook's rules read, cheapest first; passes past the budget are skipped."""+ start = time.perf_counter()+ skipped = []++ def spare():+ return time.perf_counter() - start < budget_seconds++ root = design.rootComponent+ parametric = design.designType == adsk.fusion.DesignTypes.ParametricDesignType+ intents = {}+ if hasattr(adsk.fusion, "DesignIntentTypes"):+ for name in ("PartDesignIntentType", "AssemblyDesignIntentType", "HybridDesignIntentType"):+ if hasattr(adsk.fusion.DesignIntentTypes, name):+ intents[getattr(adsk.fusion.DesignIntentTypes, name)] = name[:-len("DesignIntentType")].lower()+ m = {"designType": "parametric" if parametric else "direct",+ "designIntent": intents.get(getattr(design, "designIntent", None))}++ timeline = {"count": 0, "groups": 0, "byType": {}, "warnings": 0, "errors": 0, "suppressed": 0}+ if parametric:+ tl = design.timeline+ timeline["count"] = tl.count+ for i in range(tl.count):+ item = tl.item(i)+ if item.isGroup:+ timeline["groups"] += 1+ continue+ try:+ kind = _kind(item.entity)+ except Exception:+ kind = "unreadable"+ timeline["byType"][kind] = timeline["byType"].get(kind, 0) + 1+ state = getattr(item, "healthState", 0)+ timeline["warnings"] += state == adsk.fusion.FeatureHealthStates.WarningFeatureHealthState+ timeline["errors"] += state == adsk.fusion.FeatureHealthStates.ErrorFeatureHealthState+ timeline["suppressed"] += bool(getattr(item, "isSuppressed", False))+ m["timeline"] = timeline+ snapshots = getattr(design, "snapshots", None)+ m["snapshots"] = snapshots.count if snapshots is not None else 0+ m["pendingSnapshot"] = bool(snapshots.hasPendingSnapshot) if snapshots is not None else False++ every = [design.allComponents.item(i) for i in range(design.allComponents.count)]+ own = [c for c in every if c.parentDesign == design]+ m["components"] = {"all": len(every), "own": len(own), "referenced": len(every) - len(own)}+ m["occurrences"] = root.allOccurrences.count+ m["joints"] = {"joints": size(root.allJoints), "asBuilt": size(root.allAsBuiltJoints), "rigidGroups": size(root.allRigidGroups)}++ counts = {"bodies": 0, "faces": 0, "meshBodies": 0, "baseFeatures": 0, "moveFeatures": 0, "sketches": 0,+ "sketchCurves": 0, "modeledThreads": 0, "emptyComponents": 0, "wires": 0, "copyNames": 0}+ big_sketches, patterns, per_definition = [], [], []+ for comp in own:+ bodies = comp.bRepBodies+ faces = sum(bodies.item(i).faces.count for i in range(bodies.count))+ counts["bodies"] += bodies.count+ counts["faces"] += faces+ counts["meshBodies"] += comp.meshBodies.count+ features = comp.features+ counts["baseFeatures"] += features.baseFeatures.count+ counts["moveFeatures"] += features.moveFeatures.count+ for sketch in (comp.sketches.item(i) for i in range(comp.sketches.count)):+ counts["sketches"] += 1+ curves = sketch.sketchCurves.count+ counts["sketchCurves"] += curves+ if curves > LIMITS["sketchCurves"]:+ big_sketches.append({"component": comp.name, "sketch": sketch.name, "curves": curves})+ threads = features.threadFeatures+ counts["modeledThreads"] += sum(1 for i in range(threads.count) if threads.item(i).isModeled)+ for group in (features.rectangularPatternFeatures, features.circularPatternFeatures):+ for i in range(group.count):+ pattern = group.item(i)+ option = getattr(pattern, "patternComputeOption", None)+ names = {adsk.fusion.PatternComputeOptions.AdjustPatternCompute: "adjust",+ adsk.fusion.PatternComputeOptions.IdenticalPatternCompute: "identical",+ adsk.fusion.PatternComputeOptions.OptimizedPatternCompute: "optimized"}+ # a component pattern has no compute option to choose (Fusion reports adjust), so it is not a finding+ kinds = getattr(adsk.fusion, "PatternEntityTypes", None)+ try:+ of_occurrences = kinds is not None and pattern.patternEntityType == kinds.OccurrencesPatternType+ except (AttributeError, RuntimeError):+ of_occurrences = False+ patterns.append({"component": comp.name, "name": pattern.name,+ "computeOption": None if of_occurrences else names.get(option), "ofComponents": of_occurrences})+ if not bodies.count and not comp.occurrences.count and not comp.sketches.count and not comp.meshBodies.count:+ counts["emptyComponents"] += 1+ if GAUGE.search(comp.name) and not WIRING_HARDWARE.search(comp.name):+ counts["wires"] += 1+ counts["copyNames"] += bool(COPY_NAME.search(comp.name))+ per_definition.append((comp, faces))+ m.update(counts)+ m["bigSketches"] = big_sketches[:20]+ m["patterns"] = patterns[:50]++ # costly passes: instance-weighted faces and repeated definitions+ heavy, signatures = [], {}+ # instances per definition in one walk (allOccurrencesByComponent rescans every occurrence per call)+ instance_counts = {}+ every_occurrence = root.allOccurrences+ for i in range(every_occurrence.count):+ if not spare():+ break+ token = every_occurrence.item(i).component.entityToken+ instance_counts[token] = instance_counts.get(token, 0) + 1+ for comp, faces in per_definition:+ if not spare():+ skipped.append("heavy geometry and duplicate definitions")+ break+ instances = instance_counts.get(comp.entityToken, 0)+ if faces * max(instances, 1) > LIMITS["facesPerDefinition"]:+ heavy.append({"component": comp.name, "faces": faces, "instances": instances, "weightedFaces": faces * max(instances, 1)})+ bodies = comp.bRepBodies+ if 0 < bodies.count <= 5:+ key = []+ for i in range(bodies.count):+ body = bodies.item(i)+ box = body.boundingBox+ key.append((body.faces.count, round((box.maxPoint.x - box.minPoint.x) * 1000), round((box.maxPoint.y - box.minPoint.y) * 1000),+ round((box.maxPoint.z - box.minPoint.z) * 1000)))+ signatures.setdefault(tuple(sorted(key)), []).append(comp.name)+ heavy.sort(key=lambda h: -h["weightedFaces"])+ m["heavyDefinitions"] = heavy[:15]+ groups = sorted((names for names in signatures.values() if len(names) > 1), key=len, reverse=True)+ m["duplicateDefinitions"] = {"groups": len(groups), "extraDefinitions": sum(len(g) - 1 for g in groups),+ "largest": [{"count": len(g), "names": g[:5]} for g in groups[:10]]}+ m["skipped"] = skipped+ m["seconds"] = round(time.perf_counter() - start, 2)+ return m+++def findings(m):+ """Each rule the design breaks, with what was measured, the limit and the fix, worst first."""+ out = []++ def add(rule, severity, measured, limit, fix):+ out.append({"rule": rule, "severity": severity, "measured": measured, "limit": limit, "fix": fix})++ if m.get("snapshots"):+ add("placement-snapshots", "high" if m["snapshots"] > 10 else "medium", m["snapshots"], 0,+ "Place each occurrence where it belongs when inserting it (occurrence_insert transform), and move it with "+ "occurrence_update, which edits its initial position when Fusion allows: neither writes a Position snapshot.")+ if m.get("pendingSnapshot"):+ add("pending-snapshot", "medium", True, False, "A moved occurrence is waiting to be captured: capture or revert it before the next recompute.")+ if m.get("moveFeatures"):+ add("move-features", "medium", m["moveFeatures"], 0, "Model bodies in place, or move their occurrence instead of adding Move features.")+ dups = (m.get("duplicateDefinitions") or {}).get("extraDefinitions", 0)+ if dups:+ add("duplicate-definitions", "high" if dups > 20 else "medium", dups, 0,+ "Copies made as new components (Paste New): insert instances of one component so it is computed once and counted right.")+ size = max(m.get("occurrences", 0), (m.get("components") or {}).get("all", 0))+ if size > LIMITS["occurrences"]:+ add("design-size", "high", size, LIMITS["occurrences"],+ "Split the design into sub-designs inserted by reference: each computes on its own and opens fast.")+ if (m.get("timeline") or {}).get("count", 0) > LIMITS["timeline"]:+ add("timeline-length", "medium", m["timeline"]["count"], LIMITS["timeline"],+ "Shorten the timeline: placement at creation, instances instead of copies, sub-designs by reference.")+ if m.get("designType") == "parametric" and m.get("baseFeatures", 0) > LIMITS["baseFeatures"]:+ add("imported-history", "medium", m["baseFeatures"], LIMITS["baseFeatures"],+ "Keep imported and generated geometry in direct-mode designs inserted by reference, out of this timeline.")+ if m.get("designType") == "parametric" and m.get("wires", 0) > LIMITS["wires"]:+ add("wires-in-assembly", "high", m["wires"], LIMITS["wires"],+ "Build the wiring in its own direct-mode design and insert it by reference.")+ heavy = m.get("heavyDefinitions") or []+ if heavy:+ add("heavy-geometry", "medium", [h["component"] for h in heavy[:5]], LIMITS["facesPerDefinition"],+ "Defeature these models (delete small faces, replace detail with primitives) or keep a simplified copy.")+ if m.get("bigSketches"):+ add("sketch-size", "medium", len(m["bigSketches"]), LIMITS["sketchCurves"],+ "Split big sketches; pattern the 3D feature instead of repeating sketch entities.")+ adjust = [p for p in m.get("patterns") or [] if p.get("computeOption") == "adjust"]+ if adjust:+ add("pattern-compute", "medium", len(adjust), 0, "Set these patterns' compute option to identical or optimized.")+ timeline = m.get("timeline") or {}+ if timeline.get("errors") or timeline.get("warnings"):+ add("timeline-health", "high" if timeline.get("errors") else "low", {"errors": timeline.get("errors", 0), "warnings": timeline.get("warnings", 0)}, 0,+ "Fix the timeline's errors and warnings: each one costs time on every recompute.")+ if m.get("modeledThreads"):+ add("modeled-threads", "medium", m["modeledThreads"], 0, "Use cosmetic threads and unthreaded vendor models.")+ if m.get("meshBodies"):+ add("mesh-bodies", "low", m["meshBodies"], 0, "Convert or replace mesh bodies; they are heavy to display and cannot be edited.")+ if m.get("emptyComponents"):+ add("empty-components", "low", m["emptyComponents"], 0, "Delete empty components; each still takes part in the compute.")+ order = {"high": 0, "medium": 1, "low": 2}+ return sorted(out, key=lambda f: order[f["severity"]])+++def efficiency_report(app, args):+ doc, design, key = active(app)+ wanted = args.get("target") or {}+ require(wanted.get("documentId") == key, "wrong_document", "Target is not the active document.", activeDocumentId=key)+ metrics = measure(app, design, float(args.get("budgetSeconds", 120)))+ result = {"target": {"documentId": key}, "complete": not metrics["skipped"], "metrics": metrics, "findings": findings(metrics),+ "playbook": "skills/fusion-design-efficiency/SKILL.md"}+ if args.get("computeTiming", False):+ # the one number that sums everything up, and the slowest to get: opt in+ start = time.perf_counter()+ design.computeAll()+ result["computeAllSeconds"] = round(time.perf_counter() - start, 2)+ return result+++# journaled like bom_export: model-read-only, but a big design's report outlives the relay's 60 s+OPERATIONS = {"efficiency_report": efficiency_report}+WRITES = {"efficiency_report"}
addin/AdomBridge/commands/mechanical/features.py+100−17@@ -40,6 +40,98 @@ def factory(features, name): return result +PATTERN_INPUTS = ("bodies", "features", "faces", "occurrences")+# Fusion's pattern compute options: identical repeats the first result (fast), optimized is fastest but+# can drop instances that touch other geometry, adjust recomputes every instance (slowest)+COMPUTE_OPTIONS = {"identical": "IdenticalPatternCompute", "optimized": "OptimizedPatternCompute", "adjust": "AdjustPatternCompute"}+++def occurrence_parent(design, occurrence):+ """(the component that owns an occurrence, the occurrence as that component sees it). A root-context+ proxy belongs to its context's component; otherwise it is the root's own child or Fusion names its source."""+ context = getattr(occurrence, "assemblyContext", None)+ if context is not None:+ return context.component, occurrence.nativeObject+ root = design.rootComponent+ if any(same(child, occurrence) for child in items(root.occurrences)):+ return root, occurrence+ source = getattr(occurrence, "sourceComponent", None)+ require(source is not None, "unsupported_capability", "Fusion did not name the component that owns this occurrence.")+ return source, occurrence+++def pattern_inputs(design, value, args, comp):+ """What a pattern repeats, (kind, entities, component to build it in): exactly one of bodies, features,+ faces or occurrences. Occurrences are patterned in their shared parent (Fusion's pattern components),+ so the part stays one definition; the rest pattern inside the target component as before."""+ given = [key for key in PATTERN_INPUTS if key in args]+ require(len(given) <= 1, "mixed_pattern_inputs", "A pattern repeats one kind of entity; pass only one of " + ", ".join(given) + ".")+ require(given, "invalid_selection", "A pattern needs bodies, features, faces or occurrences.")+ key = given[0]+ if key == "occurrences":+ raw = args["occurrences"]+ require(isinstance(raw, list) and raw, "invalid_selection", "occurrences requires at least one reference.")+ owned = [occurrence_parent(design, resolve(design, value, r, ("Occurrence",))) for r in raw]+ parent = owned[0][0]+ require(all(same(owner_comp, parent) for owner_comp, _ in owned), "mixed_parents",+ "Patterned occurrences must share one parent component.")+ require(not args.get("component") or same(comp, parent), "wrong_component",+ "Occurrences are patterned in their parent component; omit component or pass that parent.")+ return key, [occurrence for _, occurrence in owned], parent+ entities = selected(design, value, args, key, {"bodies": ("BRepBody",), "faces": ("BRepFace",), "features": ()}[key], comp)+ if key == "features":+ require(all(entity.objectType.endswith("Feature") and getattr(entity, "timelineObject", None) is not None for entity in entities),+ "wrong_entity_type", "Patterned features must be timeline features (an extrude, a hole, a fillet).")+ return key, entities, comp+++def compute_option(inp, args, key):+ """Set the compute option on a body, face or feature pattern (identical unless asked); component+ patterns have none. Returns the option set, or None."""+ options = getattr(adsk.fusion, "PatternComputeOptions", None)+ if key == "occurrences" or options is None or not hasattr(inp, "patternComputeOption"):+ return None+ name = args.get("computeOption", "identical")+ inp.patternComputeOption = getattr(options, COMPUTE_OPTIONS[name])+ return name+++def compute_name(feature, requested):+ """The compute option Fusion reports on the created pattern, by name (the requested one when unreadable)."""+ options = getattr(adsk.fusion, "PatternComputeOptions", None)+ try:+ actual = getattr(feature, "patternComputeOption", None)+ except RuntimeError:+ actual = None+ if options is None or actual is None:+ return requested+ return next((name for name, attr in COMPUTE_OPTIONS.items() if getattr(options, attr, None) == actual), requested)+++def pattern_input(design, value, args, comp, kind, q):+ """(feature group, input, what is patterned, compute option) for a rectangular or circular pattern."""+ key, entities, comp = pattern_inputs(design, value, args, comp)+ features = comp.features+ if kind == "rectangular_pattern":+ if any(name in args for name in ("axis2", "count2", "spacing2")):+ require("count2" in args and "spacing2" in args, "invalid_input", "Second pattern direction requires count2 and spacing2; axis2 defaults to y.")+ group = factory(features, "rectangularPatternFeatures")+ inp = group.createInput(collection(entities), axis(design, value, comp, args["axis"]),+ q("count", ""), q("spacing"), adsk.fusion.PatternDistanceType.SpacingPatternDistanceType)+ inp.quantityTwo = quantity(design, 1, "", True)+ inp.isSymmetricInDirectionOne = False+ inp.isSymmetricInDirectionTwo = False+ if "count2" in args:+ require(inp.setDirectionTwo(axis(design, value, comp, args.get("axis2", "y")), q("count2", ""), q("spacing2")), "pattern_failed", "Second pattern direction was refused.")+ else:+ group = factory(features, "circularPatternFeatures")+ inp = group.createInput(collection(entities), axis(design, value, comp, args["axis"]))+ inp.quantity = q("count", "")+ inp.totalAngle = quantity(design, args.get("angle", 360), "deg", True)+ inp.isSymmetric = False+ return group, inp, key, compute_option(inp, args, key)++ def feature_create(design, value, args): comp = component(design, value, args) features, kind = comp.features, args["type"]@@ -141,23 +233,8 @@ def feature_create(design, value, args): group = factory(features, "mirrorFeatures") inp = group.createInput(collection(select("bodies", ("BRepBody",))), plane(design, value, comp, args["plane"])) inp.isCombine = False- elif kind == "rectangular_pattern":- if any(key in args for key in ("axis2", "count2", "spacing2")):- require("count2" in args and "spacing2" in args, "invalid_input", "Second pattern direction requires count2 and spacing2; axis2 defaults to y.")- group = factory(features, "rectangularPatternFeatures")- inp = group.createInput(collection(select("bodies", ("BRepBody",))), axis(design, value, comp, args["axis"]),- q("count", ""), q("spacing"), adsk.fusion.PatternDistanceType.SpacingPatternDistanceType)- inp.quantityTwo = quantity(design, 1, "", True)- inp.isSymmetricInDirectionOne = False- inp.isSymmetricInDirectionTwo = False- if "count2" in args:- require(inp.setDirectionTwo(axis(design, value, comp, args.get("axis2", "y")), q("count2", ""), q("spacing2")), "pattern_failed", "Second pattern direction was refused.")- elif kind == "circular_pattern":- group = factory(features, "circularPatternFeatures")- inp = group.createInput(collection(select("bodies", ("BRepBody",))), axis(design, value, comp, args["axis"]))- inp.quantity = q("count", "")- inp.totalAngle = quantity(design, args.get("angle", 360), "deg", True)- inp.isSymmetric = False+ elif kind in ("rectangular_pattern", "circular_pattern"):+ group, inp, patterned, requested = pattern_input(design, value, args, comp, kind, q) elif kind == "draft": group = factory(features, "draftFeatures") inp = group.createInput(select("faces", ("BRepFace",)), plane(design, value, comp, args["plane"]), args.get("tangentChain", False))@@ -174,6 +251,12 @@ def feature_create(design, value, args): else: require(False, "unsupported_feature", "This feature type is not supported.") feature = named(group.add(inp), args)+ if kind in ("rectangular_pattern", "circular_pattern"):+ elements = getattr(feature, "patternElements", None)+ pattern = {"patterned": patterned, "computeOption": compute_name(feature, requested),+ "instanceCount": elements.count if elements is not None else None}+ return lambda fresh: {"feature": feature_data(feature, fresh), "pattern": pattern,+ "bodies": [topology_data(body, fresh) for body in items(feature.bodies)]} return lambda fresh: {"feature": feature_data(feature, fresh), "bodies": [topology_data(body, fresh) for body in items(feature.bodies)]}
addin/AdomBridge/commands/mechanical/schema.json+136−4@@ -1114,7 +1114,10 @@ "threadType": {}, "designation": {}, "threadClass": {},- "modeled": {}+ "modeled": {},+ "features": {},+ "occurrences": {},+ "computeOption": {} }, "required": [ "op",@@ -1123,7 +1126,7 @@ "target", "type" ],- "description": "Create a typed native solid feature. All length quantities are mm (or expressions), angles deg. Cut/intersect require explicit participant bodies. Join needs a single existing component body; combine selects multiple bodies explicitly. Feature availability is runtime-checked; missing APIs fail before commit.",+ "description": "Create a typed native solid feature. All length quantities are mm (or expressions), angles deg. Cut/intersect require explicit participant bodies. Join needs a single existing component body; combine selects multiple bodies explicitly. Feature availability is runtime-checked; missing APIs fail before commit. Rectangular and circular patterns repeat exactly one of bodies, features, faces or occurrences (a component pattern, built in the occurrences' shared parent so the part stays one definition); computeOption is identical (default), optimized or adjust and does not apply to component patterns.", "oneOf": [ { "type": "object",@@ -2380,11 +2383,60 @@ "type": "string" } ]+ },+ "features": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Reference returned by inspect/inspect_entity in this document revision; native component-definition references for authoring."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "faces": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Reference returned by inspect/inspect_entity in this document revision; native component-definition references for authoring."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "occurrences": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Occurrence reference returned by inspect in this document revision; all must share one parent component, where the component pattern is built."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "computeOption": {+ "enum": [+ "identical",+ "optimized",+ "adjust"+ ],+ "description": "identical (default) repeats the first result; optimized is fastest but can drop instances that touch other geometry; adjust recomputes every instance (slowest). Ignored for occurrences." } }, "required": [ "type",- "bodies", "axis", "count", "spacing"@@ -2484,11 +2536,60 @@ "type": "string" } ]+ },+ "features": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Reference returned by inspect/inspect_entity in this document revision; native component-definition references for authoring."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "faces": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Reference returned by inspect/inspect_entity in this document revision; native component-definition references for authoring."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "occurrences": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Occurrence reference returned by inspect in this document revision; all must share one parent component, where the component pattern is built."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "computeOption": {+ "enum": [+ "identical",+ "optimized",+ "adjust"+ ],+ "description": "identical (default) repeats the first result; optimized is fastest but can drop instances that touch other geometry; adjust recomputes every instance (slowest). Ignored for occurrences." } }, "required": [ "type",- "bodies", "axis", "count" ],@@ -3070,6 +3171,15 @@ }, "groundedToParent": { "type": "boolean"+ },+ "method": {+ "type": "string",+ "enum": [+ "auto",+ "initial_position",+ "capture"+ ],+ "description": "How a new transform is kept. auto (default): edit the occurrence's initial position when Fusion allows it (a root-level occurrence without joints or patterns), which adds nothing to the timeline, else capture a Position snapshot. initial_position: refuse rather than capture. capture: always capture a snapshot. The reply's placement says which was used." } }, "required": [@@ -7163,5 +7273,27 @@ } }, "description": "Main-thread live preview ownership/geometry/history observation. Cached operation receipts do not establish current graphics liveness. Old-session history remains separately readable through mech_operation_status; no native reference adoption or lifecycle repair is provided here."+ },+ "efficiency_report": {+ "type": "object",+ "description": "Read what makes the design slow to recompute, open and edit, held against the efficiency playbook (skills/fusion-design-efficiency): timeline make-up and health, Position snapshots, Move features, size, repeated definitions made by Paste New, instance-weighted faces, imported history, wires in a parametric design, big sketches, pattern compute options, modeled threads, meshes and empty components. Returns metrics and findings (rule, severity, measured, limit, fix). Read-only and needs no revision digest. It is journaled like bom_export, so a long report on a big design is followed with fusion_mech_operation_status after the relay's 60 s; passes past budgetSeconds are skipped and listed, with complete:false.",+ "properties": {+ "budgetSeconds": {+ "type": "number",+ "minimum": 5,+ "maximum": 600,+ "description": "Stop the costly passes after this many seconds (default 120)."+ },+ "computeTiming": {+ "type": "boolean",+ "description": "Also time a full computeAll (slow on big designs; default false)."+ }+ },+ "required": [+ "op",+ "sessionId",+ "operationId",+ "target"+ ] } }
docs/MECHANICAL.md+7−1@@ -170,6 +170,8 @@ Create a sketch with `sketch_create` on `xy`, `xz`, `yz`, a construction plane o Pass current profile references to `feature_create`. The schema enumerates extrusion, revolve, fillet, chamfer, shell, hole, loft, sweep, combine, split, move, mirror, rectangular/circular pattern, draft and thread inputs. Creation targets a native component definition. Assembly-context proxy geometry is for inspection; obtain native references when authoring a definition. Cut/intersect operations require explicit participant bodies. Join requires an unambiguous single body; use combine for explicit multiple-body selection. +A rectangular or circular pattern repeats exactly one kind of entity: `bodies`, `features`, `faces` or `occurrences`. Pattern a feature or its faces rather than drawing every instance in a sketch: one hole cut once and patterned is far cheaper to recompute than a sketch full of circles. `occurrences` makes a component pattern. It is built in the occurrences' shared parent component, so the part stays one definition and the BOM counts every instance; mixed inputs (`mixed_pattern_inputs`) and occurrences with different parents (`mixed_parents`) are refused. `computeOption` sets Fusion's pattern compute option for bodies, features and faces: `identical` (the default) repeats the first result, `optimized` is fastest but can drop instances that touch other geometry, and `adjust` recomputes every instance and is the slowest. Component patterns have no compute option. The reply's `pattern` says what was patterned, the option Fusion reports and the number of instances.+ Use `feature_edit` to rename or suppress a feature or change the expressions of parameters owned by that feature. `entity_delete` handles supported native deletable entities. `parameter_batch` groups user-parameter creation/deletion and user/model-parameter expression edits in one measured model transaction. Generic mixed operations with file/cloud rollback are outside this contract. ## Select, measure and edit everyday geometry@@ -257,12 +259,16 @@ See [build handoff](build-2026-10-02.md) for the verified checkpoint and live ho `component_create` creates an internal definition and its first occurrence. These operations require a Hybrid design; they never silently change the intent of an existing Part or Assembly design. `occurrence_insert` reuses a definition, or uses `independentCopy:true` to create a separate definition. `parent` is a component-definition reference, so adding children to it affects every instance of that definition. -Create/insert transforms use parent-component coordinates. Creation, insertion and CAD import apply the requested pose to every currently placed parent instance, capture pending positions, and verify the resulting root-context matrices after recompute; `affectedOccurrences` lists these instances and `placementScope` is `all_current_parent_instances`. The definition-local base transform can differ from the computed position after a capture. Occurrence updates use the selected instance's assembly context. The transform input uses `position:[x,y,z]` in mm and `axis:[x,y,z]`, `angle` in degrees; it specifies an absolute pose. `occurrence_update` can ground/unground explicitly and captures changed positions in the timeline. Constrained or grounded placements refuse changes that cannot be applied and measured.+Create/insert transforms use parent-component coordinates. Creation, insertion and CAD import apply the requested pose to every currently placed parent instance, capture pending positions, and verify the resulting root-context matrices after recompute; `affectedOccurrences` lists these instances and `placementScope` is `all_current_parent_instances`. The definition-local base transform can differ from the computed position after a capture. Occurrence updates use the selected instance's assembly context. The transform input uses `position:[x,y,z]` in mm and `axis:[x,y,z]`, `angle` in degrees; it specifies an absolute pose. `occurrence_update` can ground/unground explicitly. A new transform edits the occurrence's initial position when Fusion allows it (a root-level occurrence without joints or patterns; earlier occurrences are edited with the timeline marker rolled back and restored), so nothing joins the timeline; otherwise the position is captured as a snapshot. `method` (`auto`, `initial_position`, `capture`) chooses, and the reply's `placement` says which was used. Constrained or grounded placements refuse changes that cannot be applied and measured. `joint_create` creates an **as-built** rigid, revolute, slider, cylindrical, pin-slot, planar or ball joint. First place the occurrences, then supply exact occurrence references and joint geometry for non-rigid motion. Joint axes are in joint coordinates. Read the returned unit metadata: joint-geometry transform matrices retain explicitly labeled native cm translations, while motion values use mm/degrees. `joint_edit` reads/sets enabled limits, rest positions and motion values in mm/degrees, and rejects out-of-limit driving. Rigid groups have separate create/edit/delete operations. Joint and group suppression changes capture pending native state before recompute; responses report `positionCaptured`. Full motion-study authoring remains a comparison gap. `interference` analyzes selected occurrence instances or root-context body proxies and returns overlap volumes without adding interference bodies to the model. `assembly_properties` reads whole-assembly, component, occurrence or body mass, volume, area and center of mass with explicit units and coordinate scope. Material values reflect Fusion's assigned/default materials; they are not a supplier certification. +## Design efficiency++`efficiency_report` reads what makes a design slow to recompute, open and edit: timeline make-up and health, Position snapshots, Move features, size, repeated definitions made by Paste New, instance-weighted faces, imported history and wires in a parametric design, big sketches, pattern compute options, modeled threads, meshes and empty components. It returns `metrics` and `findings` (rule, severity, measured, limit, fix), worst first. It never changes the model and needs no revision digest; it is journaled like `bom_export`, so a long report on a big design is followed with `fusion_mech_operation_status`. Passes past `budgetSeconds` (default 120) are skipped and listed with `complete:false`; `computeTiming:true` also times a full `computeAll`. The rules and the benchmarks behind them are in `skills/fusion-design-efficiency/SKILL.md`.+ ## McMaster and purchased CAD `mcmaster_discover` reports availability of Fusion's native Insert McMaster-Carr command. `mcmaster_open` launches its interactive supplier picker in the targeted Hybrid design. Its recorded result is a launch request, not proof that a part was inserted. Inspect again after insertion or cancellation. This operation does not place orders.
rust/core/tests/golden/mech_capabilities.json+137−4@@ -1129,7 +1129,10 @@ "threadType": {}, "designation": {}, "threadClass": {},- "modeled": {}+ "modeled": {},+ "features": {},+ "occurrences": {},+ "computeOption": {} }, "required": [ "op",@@ -1138,7 +1141,7 @@ "target", "type" ],- "description": "Create a typed native solid feature. All length quantities are mm (or expressions), angles deg. Cut/intersect require explicit participant bodies. Join needs a single existing component body; combine selects multiple bodies explicitly. Feature availability is runtime-checked; missing APIs fail before commit.",+ "description": "Create a typed native solid feature. All length quantities are mm (or expressions), angles deg. Cut/intersect require explicit participant bodies. Join needs a single existing component body; combine selects multiple bodies explicitly. Feature availability is runtime-checked; missing APIs fail before commit. Rectangular and circular patterns repeat exactly one of bodies, features, faces or occurrences (a component pattern, built in the occurrences' shared parent so the part stays one definition); computeOption is identical (default), optimized or adjust and does not apply to component patterns.", "oneOf": [ { "type": "object",@@ -2395,11 +2398,60 @@ "type": "string" } ]+ },+ "features": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Reference returned by inspect/inspect_entity in this document revision; native component-definition references for authoring."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "faces": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Reference returned by inspect/inspect_entity in this document revision; native component-definition references for authoring."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "occurrences": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Occurrence reference returned by inspect in this document revision; all must share one parent component, where the component pattern is built."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "computeOption": {+ "enum": [+ "identical",+ "optimized",+ "adjust"+ ],+ "description": "identical (default) repeats the first result; optimized is fastest but can drop instances that touch other geometry; adjust recomputes every instance (slowest). Ignored for occurrences." } }, "required": [ "type",- "bodies", "axis", "count", "spacing"@@ -2499,11 +2551,60 @@ "type": "string" } ]+ },+ "features": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Reference returned by inspect/inspect_entity in this document revision; native component-definition references for authoring."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "faces": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Reference returned by inspect/inspect_entity in this document revision; native component-definition references for authoring."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "occurrences": {+ "type": "array",+ "items": {+ "type": "object",+ "required": [+ "documentId",+ "revision",+ "objectType"+ ],+ "description": "Occurrence reference returned by inspect in this document revision; all must share one parent component, where the component pattern is built."+ },+ "minItems": 1,+ "maxItems": 256+ },+ "computeOption": {+ "enum": [+ "identical",+ "optimized",+ "adjust"+ ],+ "description": "identical (default) repeats the first result; optimized is fastest but can drop instances that touch other geometry; adjust recomputes every instance (slowest). Ignored for occurrences." } }, "required": [ "type",- "bodies", "axis", "count" ],@@ -3085,6 +3186,15 @@ }, "groundedToParent": { "type": "boolean"+ },+ "method": {+ "type": "string",+ "enum": [+ "auto",+ "initial_position",+ "capture"+ ],+ "description": "How a new transform is kept. auto (default): edit the occurrence's initial position when Fusion allows it (a root-level occurrence without joints or patterns), which adds nothing to the timeline, else capture a Position snapshot. initial_position: refuse rather than capture. capture: always capture a snapshot. The reply's placement says which was used." } }, "required": [@@ -7178,6 +7288,28 @@ } }, "description": "Main-thread live preview ownership/geometry/history observation. Cached operation receipts do not establish current graphics liveness. Old-session history remains separately readable through mech_operation_status; no native reference adoption or lifecycle repair is provided here."+ },+ "efficiency_report": {+ "type": "object",+ "description": "Read what makes the design slow to recompute, open and edit, held against the efficiency playbook (skills/fusion-design-efficiency): timeline make-up and health, Position snapshots, Move features, size, repeated definitions made by Paste New, instance-weighted faces, imported history, wires in a parametric design, big sketches, pattern compute options, modeled threads, meshes and empty components. Returns metrics and findings (rule, severity, measured, limit, fix). Read-only and needs no revision digest. It is journaled like bom_export, so a long report on a big design is followed with fusion_mech_operation_status after the relay's 60 s; passes past budgetSeconds are skipped and listed, with complete:false.",+ "properties": {+ "budgetSeconds": {+ "type": "number",+ "minimum": 5,+ "maximum": 600,+ "description": "Stop the costly passes after this many seconds (default 120)."+ },+ "computeTiming": {+ "type": "boolean",+ "description": "Also time a full computeAll (slow on big designs; default false)."+ }+ },+ "required": [+ "op",+ "sessionId",+ "operationId",+ "target"+ ] } }, "transactions": {@@ -7208,6 +7340,7 @@ "component_create", "component_metadata", "construction_axis",+ "efficiency_report", "entity_delete", "exploded_preview_create", "exploded_preview_dispose",
rust/core/tests/golden/mech_validation.json@@ -1 +1 @@