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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
"""Detailed topology and sketch readback using resolvable guarded references."""
import json
import adsk.core
import adsk.fusion
from .core import MechanicalError, body_data, digest, identity, items, point_mm, ref, require, same
def concrete_geometry(geometry):
"""Read subclass properties even when a getter returns a base API wrapper."""
kind = getattr(adsk.core, geometry.objectType.split("::")[-1], None)
if kind is not None and hasattr(kind, "cast"):
geometry = kind.cast(geometry)
require(geometry is not None, "unavailable_geometry", "Cannot cast native geometry to its advertised type.")
return geometry
def geometry_data(geometry):
geometry = concrete_geometry(geometry)
result = {"type": geometry.objectType}
for name in ("startPoint", "endPoint", "center", "origin"):
if hasattr(geometry, name):
result[name] = point_mm(getattr(geometry, name))
for name in ("normal", "direction", "majorAxis", "minorAxis"):
if hasattr(geometry, name):
p = getattr(geometry, name)
result[name] = [p.x, p.y, p.z]
for name in ("radius", "majorRadius", "minorRadius"):
if hasattr(geometry, name):
result[name] = getattr(geometry, name) * 10
if geometry.objectType.endswith("::NurbsCurve3D"):
ok, controls, degree, knots, rational, weights, periodic = geometry.getData()
require(ok, "unavailable_geometry", "Cannot read complete spline geometry for the semantic witness.")
result.update(controlPoints=[point_mm(p) for p in controls], knots=list(knots), weights=list(weights),
degree=degree, rational=rational, periodic=periodic)
return result
def edge_geometry_data(edge):
geometry = edge.geometry
if geometry is not None:
return geometry_data(geometry)
require(edge.isDegenerate, "unavailable_geometry", "Nondegenerate BRep edge has no readable curve geometry.")
vertices = [point_mm(vertex.geometry) for vertex in (edge.startVertex, edge.endVertex) if vertex is not None]
require(vertices, "unavailable_geometry", "Degenerate BRep edge has no readable vertex position.")
return {"type": "degenerate", "isDegenerate": True, "vertices": vertices}
def profile_signature(profile):
props = profile.areaProperties()
return {"area": props.area * 100, "centroid": point_mm(props.centroid), "loops": profile.profileLoops.count}
def profile_ref(sketch, profile, index, value):
return {**value, "objectType": "adsk::fusion::Profile", "sketchToken": sketch.entityToken,
"profileIndex": index, "fingerprint": digest(profile_signature(profile))}
def curve_data(curve, value=None):
result = {"type": curve.objectType, "construction": curve.isConstruction,
"fixed": curve.isFixed, "geometry": geometry_data(curve.geometry)}
if value:
result["ref"] = ref(curve, value)
for name in ("startSketchPoint", "endSketchPoint", "centerSketchPoint"):
if hasattr(curve, name):
point = getattr(curve, name)
result[name] = {"position": point_mm(point.geometry)}
if value:
result[name]["ref"] = ref(point, value)
if hasattr(curve, "fitPoints"):
result["fitPoints"] = [point_mm(p.geometry) for p in items(curve.fitPoints)]
return result
def frame_data(matrix):
"""A Matrix3D as a frame: origin in mm and three unit directions, in component space."""
origin, x, y, z = matrix.getAsCoordinateSystem()
return {"origin": point_mm(origin), "xDirection": [x.x, x.y, x.z], "yDirection": [y.x, y.y, y.z],
"normal": [z.x, z.y, z.z], "space": "component", "lengthUnit": "mm"}
def to_component(sketch, point):
"""A sketch-space point in its component's space, without changing the original."""
moved = point.copy()
moved.transformBy(sketch.transform)
return point_mm(moved)
def plane_data(plane, value=None):
"""A construction plane (the three origin planes included): its frame in component space."""
from .core import origin_plane_key
geometry = plane.geometry
result = {"name": plane.name, "originPlane": origin_plane_key(plane), "space": "component", "lengthUnit": "mm",
"origin": point_mm(geometry.origin), "normal": [geometry.normal.x, geometry.normal.y, geometry.normal.z]}
for name in ("uDirection", "vDirection"):
if hasattr(geometry, name):
v = getattr(geometry, name)
result[name] = [v.x, v.y, v.z]
if value:
result["ref"] = ref(plane, value)
return result
def sketch_data(sketch, value=None, strict=True):
"""A sketch's curves, points and constraint graph. strict (the default) refuses a relationship the
graph reader cannot read completely (unsupported_constraint_graph); otherwise that relationship, or a
curve whose geometry cannot be read, is reported as its type with the reason, and constraintGraph says
the graph is incomplete. A fully readable sketch reads the same either way."""
curves, points = items(sketch.sketchCurves), items(sketch.sketchPoints)
constraints, dimensions = items(sketch.geometricConstraints), items(sketch.sketchDimensions)
graph = SketchGraph(curves, points, constraints)
unreadable = []
def read(reader, fallback):
if strict:
return reader()
try:
return reader()
except (MechanicalError, AttributeError, RuntimeError) as error:
if isinstance(error, MechanicalError) and error.code not in ("unsupported_constraint_graph", "unavailable_geometry"):
raise
row = fallback()
row["unreadable"] = str(error)[:300]
unreadable.append({"type": row.get("type"), "reason": row["unreadable"]})
return row
result = {"name": sketch.name, "space": "sketch", "lengthUnit": "mm",
"fullyConstrained": sketch.isFullyConstrained,
"curves": [read(lambda: curve_data(curve, value), lambda: {"type": curve.objectType}) for curve in curves],
"points": [{"position": point_mm(p.geometry), "fixed": p.isFixed, **({"ref": ref(p, value)} if value else {})}
for p in points],
"curvePointLinks": [read(lambda: graph.curve_points(curve), lambda: {"type": curve.objectType}) for curve in curves],
"constraints": [read(lambda: constraint_data(constraint, graph), lambda: {"type": constraint.objectType})
for constraint in constraints],
"dimensions": [read(lambda: dimension_data(dimension, graph), lambda: dimension_summary(dimension))
for dimension in dimensions]}
if unreadable:
result["constraintGraph"] = {"complete": False, "unreadable": unreadable}
if value:
# where the sketch sits: its x, y and normal in component space, so a sketch-space point
# (every curve, point and centroid above) maps to the model without guessing the plane
result["frame"] = frame_data(sketch.transform)
result["ref"] = ref(sketch, value)
result["originRef"] = ref(sketch.originPoint, value)
result["profiles"] = [{**profile_signature(profile),
"centroidComponent": to_component(sketch, profile.areaProperties().centroid),
"ref": profile_ref(sketch, profile, index, value)}
for index, profile in enumerate(items(sketch.profiles))]
return result
CONSTRAINT_LINKS = {
"CoincidentConstraint": ("point", "entity"),
"HorizontalConstraint": ("line",), "VerticalConstraint": ("line",),
"HorizontalPointsConstraint": ("pointOne", "pointTwo"),
"VerticalPointsConstraint": ("pointOne", "pointTwo"),
"ParallelConstraint": ("lineOne", "lineTwo"), "CollinearConstraint": ("lineOne", "lineTwo"),
"PerpendicularConstraint": ("lineOne", "lineTwo"),
"EqualConstraint": ("curveOne", "curveTwo"), "TangentConstraint": ("curveOne", "curveTwo"),
"SmoothConstraint": ("curveOne", "curveTwo"), "ConcentricConstraint": ("entityOne", "entityTwo"),
"MidPointConstraint": ("point", "midPointCurve"),
"SymmetryConstraint": ("entityOne", "entityTwo", "symmetryLine"),
"OffsetConstraint": (),
}
DIMENSION_LINKS = {
"SketchLinearDimension": ("entityOne", "entityTwo"),
"SketchAngularDimension": ("lineOne", "lineTwo"),
"SketchRadialDimension": ("entity",), "SketchDiameterDimension": ("entity",),
"SketchOffsetDimension": ("line", "entityTwo"),
"SketchOffsetCurvesDimension": ("offsetConstraint",),
"SketchConcentricCircleDimension": ("circleOne", "circleTwo"),
"SketchEllipseMajorRadiusDimension": ("ellipse",), "SketchEllipseMinorRadiusDimension": ("ellipse",),
"SketchLinearDiameterDimension": ("line", "entityTwo"),
"SketchTangentDistanceDimension": ("entityOne", "circleOrArc"),
}
class SketchGraph:
"""Links use local inventory nodes resolved by API equality, never tokens.
The associated geometry inventories are part of the same witness. Native
reordering may conservatively refuse recovery; retargeting distinct nodes
cannot disappear merely because their current positions coincide.
"""
def __init__(self, curves, points, constraints):
self.nodes = {"curves": curves, "points": points, "constraints": constraints}
self.comparisons = 0
self.direct = {}
for name, entities in self.nodes.items():
for index, entity in enumerate(entities):
self.direct.setdefault(id(entity), []).append({"collection": name, "index": index})
def node(self, entity):
matches = self.direct.get(id(entity))
if matches is None:
self.comparisons += sum(len(entities) for entities in self.nodes.values())
require(self.comparisons <= 1000000, "inspection_budget", "Native sketch graph equality lookup exceeds inspection budget.")
matches = [{"collection": name, "index": index}
for name, entities in self.nodes.items() for index, item in enumerate(entities)
if entity is not None and same(entity, item)]
require(len(matches) == 1, "unsupported_constraint_graph",
"Sketch graph link is null, ambiguous or outside the readable native sketch inventory.")
return matches[0]
def links(self, item, fields):
try:
return {name: self.node(getattr(item, name)) for name in fields}
except AttributeError as exc:
require(False, "unsupported_constraint_graph", "Native constraint relationship is unavailable.",
objectType=item.objectType, detail=str(exc))
def curve_points(self, curve):
result = self.links(curve, [name for name in ("startSketchPoint", "endSketchPoint", "centerSketchPoint") if hasattr(curve, name)])
if hasattr(curve, "fitPoints"):
result["fitPoints"] = [self.node(p) for p in items(curve.fitPoints)]
return result
def graph_item(item, supported):
kind = item.objectType.split("::")[-1]
require(kind in supported, "unsupported_constraint_graph",
"No complete recovery graph reader for this native constraint/dimension type.", objectType=item.objectType)
subtype = getattr(adsk.fusion, kind, None)
if subtype is not None:
item = subtype.cast(item)
require(item is not None, "unsupported_constraint_graph", "Cannot cast native sketch relationship.")
require(item.isValid, "unsupported_constraint_graph", "Native sketch relationship is no longer valid.")
return item, kind
def constraint_data(constraint, graph):
constraint, kind = graph_item(constraint, CONSTRAINT_LINKS)
result = {"type": constraint.objectType}
fields = CONSTRAINT_LINKS[kind]
if kind == "PerpendicularConstraint" and hasattr(constraint, "curve"):
# September 2026 added line/curve perpendicularity. Earlier runtimes
# expose only the two-line relation; do not invent the missing link.
fields = ("line", "curve")
result["links"] = graph.links(constraint, fields)
if kind == "OffsetConstraint":
# Offset links and sign belong to the recovery witness, beyond the
# number of constraints and generated curve geometry.
result.update(distance=constraint.distance * 10, topologyMatched=constraint.isTopologyMatched,
parents=sorted([graph.node(curve) for curve in constraint.parentCurves], key=lambda row: row["index"]),
children=sorted([graph.node(curve) for curve in constraint.childCurves], key=lambda row: row["index"]))
return result
def dimension_summary(dimension):
"""What any sketch dimension says about itself, for one whose links cannot be read."""
row = {"type": dimension.objectType}
try:
parameter = dimension.parameter
row.update(name=parameter.name if parameter else None, expression=parameter.expression if parameter else None,
driving=dimension.isDriving)
except (AttributeError, RuntimeError):
pass
return row
def dimension_data(dimension, graph):
dimension, kind = graph_item(dimension, DIMENSION_LINKS)
parameter = dimension.parameter
result = {"type": dimension.objectType, "name": parameter.name if parameter else None,
"expression": parameter.expression if parameter else None, "driving": dimension.isDriving,
"value": dimension.value * (180 / 3.141592653589793 if kind == "SketchAngularDimension" else 10),
"unit": "deg" if kind == "SketchAngularDimension" else "mm",
"links": graph.links(dimension, DIMENSION_LINKS[kind])}
if kind == "SketchLinearDimension": result["orientation"] = int(dimension.orientation)
return result
def construction_state(component):
from .geometry import axis_data
return [axis_data(axis) for axis in items(component.constructionAxes)] if hasattr(component, "constructionAxes") else []
def topology_data(body, value):
result = body_data(body)
result["ref"] = ref(body, value)
result["faces"] = [{"ref": ref(face, value), "area": face.area * 100,
"point": point_mm(face.pointOnFace), "surface": geometry_data(face.geometry),
"edges": [ref(edge, value) for edge in items(face.edges)]}
for face in items(body.faces)]
result["edges"] = [{"ref": ref(edge, value), "length": edge.length * 10,
"curve": edge_geometry_data(edge)} for edge in items(body.edges)]
return result
# topology witnesses by body: a body's revisionId changes whenever its geometry does, and the rest of the
# witness (its name, and its mass through its material) is in the key, so a write reads face by face only
# the bodies that changed since the last witness instead of every body in the design
_WITNESSES = {}
def topology_witness(body):
material = body.material
key = (identity(body), str(body.revisionId), body.name, material.id if material is not None else None)
cached = _WITNESSES.get(key)
if cached is not None:
return json.loads(cached)
result = _topology_witness(body)
if len(_WITNESSES) > 50000:
_WITNESSES.clear()
_WITNESSES[key] = json.dumps(result)
return result
def _topology_witness(body):
result = body_data(body)
# Compare geometry rather than revision or transient topology IDs after undo.
result["faceGeometry"] = sorted([
{"area": f.area * 100, "point": point_mm(f.pointOnFace), "surface": geometry_data(f.geometry)}
for f in items(body.faces)], key=lambda row: json.dumps(row, sort_keys=True))
result["edgeGeometry"] = sorted([
{"length": e.length * 10, "curve": edge_geometry_data(e)} for e in items(body.edges)],
key=lambda row: json.dumps(row, sort_keys=True))
return result
# features whose timeline position Fusion will not give (imported or grouped features raise on the
# index getter), keyed by identity and their component's revision: each refused read cost about 12 ms,
# three quarters of a 653-feature digest's index reads, for an index the digest never had
_UNINDEXED = set()
def feature_data(feature, value=None, order=None):
result = {"name": feature.name, "type": feature.objectType}
if order is not None:
result["componentFeatureOrder"] = order
if value:
result["ref"] = ref(feature, value)
timeline = getattr(feature, "timelineObject", None)
if timeline:
result["suppressed"] = timeline.isSuppressed
owner = getattr(feature, "parentComponent", None)
known = (identity(feature), str(owner.revisionId) if owner is not None else None)
if known in _UNINDEXED:
result.update(index=None, indexUnavailable="native_timeline_position_unavailable")
return result
try:
result["index"] = timeline.index
except RuntimeError as exc:
# Imported/native grouped features can have a valid timeline object
# whose index getter rejects its documented non-indexed position.
# Only this known getter failure is optional; retain name, type,
# suppression and component collection order in semantic witnesses.
if "InternalValidationError : res >= 0" not in str(exc):
raise
require(timeline.isValid, "invalid_timeline", "Feature timeline object is no longer valid.")
_UNINDEXED.add(known)
result.update(index=None, indexUnavailable="native_timeline_position_unavailable")
return result