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.
master
Drew Owens
The mechanical layer on 1.12.8 with the restart fixes (1.13.0-dev.1, add-in 1.0.23)
2ef0220
15d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
"""Local supplier CAD ingestion and Fusion's native McMaster command."""
import hashlib
from pathlib import Path
from urllib.parse import urlparse
import adsk.core
from . import transactions
from .assemblies import apply_metadata, created_occurrence_result, hybrid, parent_component, place_created_occurrence, transform
from .assembly_state import metadata
from .core import MechanicalError, active, current_target, items, ref, require, resolve, target
MCM_COMMAND = "InsertMcMasterCarrComponentCommand"
def file_hash(path):
result = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
result.update(block)
return result.hexdigest()
def preflight_file(args):
path = Path(args["filename"])
require(path.is_absolute() and path.is_file() and not path.is_symlink(), "invalid_source", "Supplier CAD must be an absolute regular local file.")
require(path.suffix.lower() in (".step", ".stp", ".sat"), "unsupported_format", "Local supplier ingestion supports STEP and SAT.")
require(0 < path.stat().st_size <= 512 * 1024 * 1024, "invalid_source", "Supplier CAD must be nonempty and no larger than 512 MiB.")
provenance = args["provenance"]
url = urlparse(provenance["sourceUrl"])
require(url.scheme == "https" and url.hostname and not url.username and not url.password, "invalid_provenance", "sourceUrl must be an HTTPS supplier URL without credentials.")
if provenance["supplier"].lower() in ("mcmaster", "mcmaster-carr"):
require(url.hostname.lower() == "mcmaster.com" or url.hostname.lower().endswith(".mcmaster.com"), "invalid_provenance", "McMaster provenance must use the supplier's domain.")
sha = file_hash(path)
if "sha256" in args:
require(args["sha256"].lower() == sha, "source_hash_mismatch", "Downloaded CAD does not match the expected SHA-256.")
return path, sha
def cad_import(app, args):
# Source preflight runs before any transaction/model mutation. The importer
# only reads this file; no cloud upload, browser download or file rewrite.
target(app, args, True)
path, sha = preflight_file(args)
document_count = app.documents.count
manager = app.importManager
factory = manager.createSATImportOptions if path.suffix.lower() == ".sat" else manager.createSTEPImportOptions
options = factory(str(path))
require(options is not None, "import_unavailable", "Fusion refused import options.")
options.isViewFit = False
def mutate(design, value, request):
hybrid(design)
parent = parent_component(design, value, request)
expected = transform(request.get("transform"))
occurrence = parent.occurrences.addNewComponent(expected)
require(occurrence is not None, "import_failed", "Could not create import wrapper component.")
comp = occurrence.component
comp.name = request.get("name", path.stem)
imported = manager.importToTarget2(options, comp)
require(imported is not None and imported.count > 0, "import_failed", "Fusion returned no imported objects.")
require(app.documents.count == document_count, "import_document_side_effect", "Importer changed the document set; inspect documents before continuing.")
components = [comp] + [o.component for o in items(comp.allOccurrences)]
require(any(c.bRepBodies.count for c in components), "import_empty", "Imported CAD contains no BRep bodies.")
require(file_hash(path) == sha, "source_changed", "Supplier CAD changed while import was running.")
provenance = {**request["provenance"], "sourceFile": str(path), "sha256": sha,
"asPurchased": request.get("asPurchased", True), "partNumber": request["provenance"]["supplierPartNumber"]}
apply_metadata(comp, provenance)
captured = place_created_occurrence(design, parent, occurrence, expected)
return lambda fresh: {**created_occurrence_result(design, parent, occurrence, fresh, expected), "positionCaptured": captured,
"source": {"filename": str(path), "sha256": sha},
"importedObjectCount": imported.count, "externalEffects": "read-only source file; no cloud or document creation requested"}
try:
return transactions.execute(app, args, mutate)
except MechanicalError as exc:
exc.details["sourceFileModified"] = False
exc.details["documentSetUnchanged"] = app.documents.count == document_count
raise
def mcmaster_discover(app, args):
_, design, _ = active(app)
command = app.userInterface.commandDefinitions.itemById(MCM_COMMAND)
return {"target": current_target(app), "commandId": MCM_COMMAND, "available": command is not None,
"name": command.name if command else None, "designIntent": str(getattr(design, "designIntent", "unavailable")),
"requirements": ["Hybrid design", "online supplier access", "interactive selection and download"],
"downloadedCadAlternative": "cad_import with local STEP/SAT and supplier provenance"}
def mcmaster_open(app, args):
_, design, value = target(app, args, True)
hybrid(design)
command = app.userInterface.commandDefinitions.itemById(MCM_COMMAND)
require(command is not None, "mcmaster_unavailable", "Fusion's native McMaster command is not installed.")
require(command.execute(), "mcmaster_launch_failed", "Fusion refused the native McMaster command.")
return {"commandId": MCM_COMMAND, "launchRequested": True, "targetAtLaunch": value,
"completion": "interactive; inspect after inserting or cancelling", "refreshRequired": True,
"transactional": False, "recovery": "native command controls its own user interaction"}
def material_list(app, args):
_, design, value = target(app, args)
libraries = [{"id": library.id, "name": library.name, "count": library.materials.count} for library in items(app.materialLibraries)]
query = args.get("query", "").lower()
if args.get("libraryId"):
matches = [lib for lib in items(app.materialLibraries) if lib.id == args["libraryId"]]
require(len(matches) == 1, "material_library_missing", "Material library ID must identify one loaded library.")
sources = [(matches[0].id, matches[0].materials)]
else:
# the document's own materials first, then every loaded library: a new design holds only
# the materials it uses, so a search for "copper" there used to find nothing. Each row
# names the libraryId that material_assign needs (null: the document's own)
sources = [(None, design.materials)] + [(lib.id, lib.materials) for lib in items(app.materialLibraries)]
rows = [{"id": material.id, "name": material.name, "libraryId": library_id}
for library_id, source in sources for material in items(source) if query in material.name.lower()]
limit = args.get("limit", 100)
return {"target": value, "libraries": libraries, "materials": rows[:limit], "complete": len(rows) <= limit,
"matchingCount": len(rows), "libraryId": args.get("libraryId")}
def material_assign(app, args):
def mutate(design, value, request):
entity = resolve(design, value, request["entity"], ("Component", "BRepBody"))
require(getattr(entity, "assemblyContext", None) is None, "definition_reference_required", "Material assignment affects a native component or body definition.")
source = design.materials
if request.get("libraryId"):
matches = [lib for lib in items(app.materialLibraries) if lib.id == request["libraryId"]]
require(len(matches) == 1, "material_library_missing", "Material library ID must identify one loaded library.")
source = matches[0].materials
material = source.itemById(request["materialId"])
require(material is not None, "material_missing", "Material ID was not found in the selected source.")
entity.material = material
def result(fresh):
require(entity.material is not None and entity.material.id == material.id, "material_failed", "Assigned material ID did not read back; same display names are not material identity.")
return {"entity": ref(entity, fresh), "material": {"id": entity.material.id, "name": entity.material.name}}
return result
return transactions.execute(app, args, mutate)
OPERATIONS = {"cad_import": cad_import, "mcmaster_open": mcmaster_open, "material_assign": material_assign,
"mcmaster_discover": mcmaster_discover, "material_list": material_list}
WRITES = {"cad_import", "mcmaster_open", "material_assign"}