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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
"""When a write runs Fusion's full recompute (Design.computeAll), following Fusion's own lead.
Fusion recomputes what an edit touches as the edit is made ("Typically Fusion computes everything
automatically as you edit a parametric design", Autodesk's Compute All help) and keeps Compute All for
checkpoints: major milestones, significant changes, edits upstream in the timeline that later features
reference (deletes above all), and a check before the design leaves. Fusion computes on its main thread
in both cases, so nothing runs in the background; the only choice is when. So every write reads the
timeline's health after Fusion's own compute, and the full recompute runs at a checkpoint:
- the writes since the last full recompute add up to CHECKPOINT (WEIGHTS: deletes and assembly
relationships count most, edits of existing history next, appended features least, metadata nothing);
- the writes move on to another top-level occurrence, so the previous part's edits are done;
- before an export leaves Fusion;
- whenever a caller asks: health with recompute:true.
The counters and the last health a write verified are per document, for this add-in session.
A write is held to the faults the model carried before it (its baseline), not to a perfect model: a
production design can carry old timeline faults that nobody is about to fix. The write may not add a
fault, change one (its health state or message), or leave a fault on what it edits in place; faults it
clears are reported. With a healthy baseline this is the old rule: any fault refuses."""
from collections import Counter
import time
from . import core
from .core import MechanicalError, health, same
CHECKPOINT = 6.0
# a move to another part checkpoints edits worth one: an edit of existing history, a joint, a delete, two appends
FOCUS_MIN = 2.0
WEIGHTS = {
# appended at the end of the timeline: nothing later depends on it yet
"sketch_create": 0.5, "sketch_geometry": 0.5, "sketch_constraint": 0.5, "sketch_dimension": 0.5,
"sketch_project": 0.5, "sketch_offset": 0.5, "plane_create": 0.5, "construction_axis": 0.5,
"joint_origin_create": 0.5, "feature_create": 1.0, "component_create": 1.0, "occurrence_insert": 1.0,
"cad_import": 1.0, "occurrence_update": 1.0,
# edits of existing history, which later features may reference
"parameter_set": 1.5, "feature_edit": 2.0, "sketch_edit": 2.0, "sketch_curve_edit": 2.0, "joint_origin_edit": 2.0,
# assembly relationships
"joint_create": 2.0, "joint_edit": 2.0, "regular_joint_create": 2.0, "regular_joint_edit": 2.0,
"rigid_group_create": 2.0, "rigid_group_edit": 2.0, "motion_link_create": 2.0, "motion_link_edit": 2.0,
# deletes of something later features or joints may reference
"entity_delete": 3.0, "occurrence_delete": 3.0, "joint_delete": 3.0, "joint_origin_delete": 3.0,
"rigid_group_delete": 3.0, "motion_link_delete": 3.0,
# no geometry
"parameter_create": 0.0, "component_metadata": 0.0, "material_assign": 0.0,
}
BATCH = {"create": 0.0, "set": WEIGHTS["parameter_set"], "delete": 3.0}
_ledgers = {}
def ledger(document):
return _ledgers.setdefault(document, {"significance": 0.0, "writes": 0, "focus": None, "last": None, "verified": None})
def reset():
_ledgers.clear()
def weight(op, args):
if op == "parameter_batch":
return min(CHECKPOINT, sum(BATCH.get(change.get("action"), 1.0) for change in args.get("changes") or []))
return WEIGHTS.get(op, 1.0)
def focus_of(args):
"""The top-level occurrence a write works in: the first segment of the first occurrence path among
its references, in argument order. None for root-level work, which never counts as a move to
another part."""
pending = [value for key, value in reversed(list(args.items())) if key != "target"]
while pending:
item = pending.pop()
if isinstance(item, dict):
path = item.get("occurrencePath")
if isinstance(path, str) and path:
return path.split("+")[0]
pending.extend(reversed(list(item.values())))
elif isinstance(item, list):
pending.extend(reversed(item))
return None
def due(document, w, focus):
"""Why this write runs the full recompute, or None."""
led = ledger(document)
if led["significance"] + w >= CHECKPOINT:
return "significance"
if focus is not None and led["focus"] is not None and focus != led["focus"] and led["significance"] >= FOCUS_MIN:
return "focus_change"
return None
def status(document):
led = ledger(document)
return {"significance": round(led["significance"], 2), "checkpointAt": CHECKPOINT,
"writesSinceFullCompute": led["writes"], "focus": led["focus"], "lastFullCompute": led["last"]}
FIRST = 5 # faults a reply lists; the count covers the rest
def fault_rows(state):
"""A health read's faults, then the items whose health could not be read."""
return list(state.get("faults") or ()) + list(state.get("unknown") or ())
def fault_key(row):
"""A fault as writes compare it: the item's name, health state and message. Not its timeline index,
which moves when an earlier item is deleted."""
if "error" in row:
return ("unreadable", row.get("name"), row.get("error"))
return ("fault", row.get("name"), row.get("state"), row.get("message"))
def unmatched(rows, others):
"""The rows the other read does not account for, counting repeats: two faults with the same name and
message are two. Matching runs in timeline order, so the later of two lookalikes is the one left over."""
budget = Counter(fault_key(row) for row in others)
left = []
for row in rows:
key = fault_key(row)
if budget[key] > 0:
budget[key] -= 1
else:
left.append(row)
return left
def summary(baseline, state=None):
"""The faults the model carried before the write, for its reply: how many, the first few, and the ones
the write cleared (deleted, suppressed or repaired)."""
rows = fault_rows(baseline)
result = {"count": len(rows), "faults": rows[:FIRST]}
if len(rows) > FIRST:
result["more"] = len(rows) - FIRST
if state is not None:
cleared = unmatched(rows, fault_rows(state))
if cleared:
result["cleared"] = {"count": len(cleared), "faults": cleared[:FIRST]}
return result
def _native(entity):
try:
return getattr(entity, "nativeObject", None) or entity
except Exception:
return entity
# What a write edits in place, by argument. A fault the model already carried there that the write did not
# clear is the write's own result. Inputs it only reads (a profile, an axis, joint geometry) are not, and a
# new item it creates is held to "no new fault".
SUBJECTS = {"feature_edit": "feature", "occurrence_update": "occurrence", "joint_edit": "joint",
"regular_joint_edit": "joint", "rigid_group_edit": "group", "joint_origin_edit": "origin",
"motion_link_edit": "link", **{op: "sketch" for op in (
"sketch_geometry", "sketch_constraint", "sketch_dimension", "sketch_project", "sketch_edit",
"sketch_curve_edit", "sketch_offset")}}
def owner_of(design, name):
"""The feature (or sketch dimension, or joint) that owns a model parameter, as a list; a user parameter has none."""
try:
parameter = design.allParameters.itemByName(name)
owner = getattr(parameter, "createdBy", None) if parameter is not None else None
except (AttributeError, RuntimeError):
return []
return [owner] if owner is not None else []
def subjects(design, value, args):
"""The entities a write edits in place (SUBJECTS; a parameter batch edits the owners of the parameters it
sets), read before the edit. A reference that does not resolve is left for the operation to refuse."""
found = []
key = SUBJECTS.get(args.get("op"))
if key and isinstance(args.get(key), dict):
try:
found.append(core.resolve(design, value, args[key]))
except (MechanicalError, AttributeError, RuntimeError):
pass
if args.get("op") == "parameter_batch":
for change in args.get("changes") or ():
if isinstance(change, dict) and change.get("action") == "set":
found.extend(owner_of(design, change.get("name")))
return found
def edited(natives, edits):
"""The faults among natives ((row, timeline item) pairs) that sit on an entity the write edits in place:
a fault left there means the write's own result is not healthy."""
if not edits:
return []
mine = [_native(entity) for entity in edits if entity is not None]
found = []
for row, item in natives:
try:
entity = item.entity
except Exception:
continue
if entity is None:
continue
entity = _native(entity)
try:
if any(same(entity, other) for other in mine):
found.append(row)
except Exception:
continue
return found
def judge(state, baseline, natives=(), edits=(), **details):
"""Refuse (unhealthy_model) a health read a write may not leave behind: a fault the baseline lacks (a
new one, or an existing one whose state or message changed), a fault left on an entity the write edits
in place, or a full recompute that failed. (Design.computeAll returns true on a model that carries
faults: measured with 15 lost-reference warnings and with a failed extrude.)"""
added = unmatched(fault_rows(state), fault_rows(baseline))
own = [] if added else edited(natives, edits)
failed = state.get("computeSucceeded") is False
if not (added or own or failed):
return
if added:
message = "The write added a fault, or changed one the model already had."
elif own:
message = "The write left a fault on an entity it edited."
else:
message = "Recompute failed."
raise MechanicalError("unhealthy_model", message, health=state, newFaults=added, editedFaults=own,
preExistingFaults=summary(baseline), **details)
def health_before(design, document, revision):
"""The model's health before a write, the baseline the write is held to: what the last write verified
when nothing changed since (same revision), otherwise a walk of the timeline. Faults the model already
carries do not refuse the write; settle refuses one that adds a fault or changes one."""
cached = ledger(document)["verified"]
if cached is not None and cached["revision"] == revision:
return dict(cached["health"])
return health(design, False)
def settle(design, document, args, baseline, edits=()):
"""After a write's edit and before it commits. Fusion's own compute has run; the full recompute runs
first when a checkpoint is due. Refuses a result that judge refuses against the baseline; edits are
the entities the write edits in place (subjects)."""
w = weight(args.get("op"), args)
focus = focus_of(args)
reason = due(document, w, focus)
started = time.perf_counter()
natives = []
state = health(design, reason is not None, natives)
info = {"full": reason is not None, "reason": reason, "seconds": round(time.perf_counter() - started, 2), "weight": w, "focus": focus}
judge(state, baseline, natives, edits, compute=info)
return state, info
def midway(design):
"""Inside a write, before it settles (Fusion solves a joint as it is added): the model's health now,
held to the write's baseline."""
baseline = core.write_baseline()
state = health(design, False)
judge(state, baseline if baseline is not None else {})
return state
def restored(design, baseline):
"""After a failed write is undone, or the transaction probe: the full recompute, and no fault the
model did not carry before."""
state = health(design, True)
judge(state, baseline)
return state
def committed(document, revision, state, info):
"""Count a committed write, remember the health it verified, and report the ledger."""
led = ledger(document)
if info["full"]:
led.update(significance=0.0, writes=0, last={"reason": info["reason"], "seconds": info["seconds"], "at": round(time.time())})
else:
led["significance"] += info["weight"]
led["writes"] += 1
if info["focus"] is not None:
led["focus"] = info["focus"]
# stored as a plain walk reports it, so a later witness compares equal
led["verified"] = {"revision": revision, "health": dict(state, computeSucceeded=None)}
return dict(status(document), full=info["full"], reason=info["reason"], seconds=info["seconds"])
def checkpoint(design, document, reason):
"""Run the full recompute now (an export, or a caller asking) and start the counters again."""
started = time.perf_counter()
state = health(design, True)
led = ledger(document)
led.update(significance=0.0, writes=0, verified=None,
last={"reason": reason, "seconds": round(time.perf_counter() - started, 2), "at": round(time.time())})
return state
def pending(document):
led = ledger(document)
return led["writes"] > 0 or led["significance"] > 0