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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
"""Semantic attachment graph and frames for regular joints, origins and links.
Entity relationships use native API equality through session identities. Transient
JointGeometry wrappers and token strings never serve as graph identity.
"""
import math
import adsk.fusion
from .core import MAX_ENTITIES, concrete, identity, items, number, point_mm, ref, require, same
_source_entities = []
def source_identity(entity):
"""Re-resolve token locators before comparing API identity after recompute.
Fusion can invalidate an old BRep wrapper while preserving its model entity.
A token is only a lookup locator; fresh native equality is the identity proof.
"""
from .geometry import owner
require(entity is not None and entity.isValid, 'lost_joint_geometry', 'Attachment native source is invalid.')
component = owner(entity)
if component is None:
return identity(entity)
design = component.parentDesign
token = entity.entityToken
for saved in _source_entities:
if saved['type'] != entity.objectType or not saved['owner'].isValid or not same(saved['owner'], component):
continue
candidates = [concrete(candidate) for candidate in design.findEntityByToken(saved['token'])]
require(len(candidates) <= MAX_ENTITIES, 'inspection_budget', 'Attachment token lookup exceeds inspection budget.')
matches = [candidate for candidate in candidates if candidate.isValid and same(candidate, entity)]
if matches:
require(len(candidates) == 1 and candidates[0].objectType == entity.objectType,
'ambiguous_reference', 'Attachment identity token no longer resolves uniquely.', matchCount=len(candidates))
return saved['id']
require(len(_source_entities) < MAX_ENTITIES, 'identity_budget', 'Session attachment identity budget exhausted.')
result = identity(entity)
_source_entities.append({'owner': component, 'type': entity.objectType, 'token': token, 'id': result})
return result
def entity_link(entity, value=None):
entity = concrete(entity)
if entity is None:
return None
require(entity.isValid, "lost_joint_geometry", "Joint attachment is no longer valid.")
native = getattr(entity, "nativeObject", None) or entity
context = getattr(entity, "assemblyContext", None)
result = {"entityId": source_identity(native), "objectType": native.objectType,
"occurrenceId": identity(context) if context is not None else None}
if value is not None:
result["ref"] = ref(entity, value)
return result
def vector_data(vector):
require(vector is not None, "joint_frame_unavailable", "Joint direction is unavailable.")
return [number(getattr(vector, axis), "joint direction") for axis in "xyz"]
def matrix_data(matrix):
require(matrix is not None, "joint_frame_unavailable", "Joint frame is unavailable.")
result = [number(v, "joint transform") for v in matrix.asArray()]
require(len(result) == 16, "joint_frame_unavailable", "Joint frame must be a 4 by 4 matrix.")
for i in (3, 7, 11):
result[i] *= 10
return result
def parameter_state(parameter, unit):
require(parameter is not None, "unsupported_inferred_joint", "Joint parameter is unavailable (inferred joints are not editable).")
scale = 180 / math.pi if unit == "deg" else 10
return {"expression": parameter.expression, "value": number(parameter.value, "joint parameter") * scale, "unit": unit}
def geometry_state(geometry, value=None):
geometry = concrete(geometry)
require(geometry is not None and geometry.isValid, "lost_joint_geometry", "Joint geometry is unavailable.")
if geometry.objectType.endswith("::JointOrigin"):
return {"origin": entity_link(geometry, value)}
require(geometry.objectType.endswith("::JointGeometry"), "unsupported_joint_geometry", "Unsupported joint attachment wrapper.")
result = {"geometryType": int(geometry.geometryType), "keyPointType": int(geometry.keyPointType),
"frameSpace": "native_joint_geometry_api", "originUnit": "mm",
"entityOne": entity_link(geometry.entityOne, value), "entityTwo": entity_link(geometry.entityTwo, value),
"origin": point_mm(geometry.origin),
"primaryAxis": vector_data(geometry.primaryAxisVector),
"secondaryAxis": vector_data(geometry.secondaryAxisVector),
"thirdAxis": vector_data(geometry.thirdAxisVector)}
# These parameters distinguish analytical face and tangent attachment sites.
kinds = adsk.fusion.JointGeometryTypes
if geometry.geometryType in (kinds.JointTangentBRepFaceGeometry, kinds.JointTangentBRepFaceEdgeGeometry):
for name in ("tangentFaceType", "tangentFaceParamOne", "tangentFaceParamTwo"):
result[name] = number(getattr(geometry, name), name)
for name in ("planeOne", "planeTwo"):
plane = getattr(geometry, name)
result[name] = entity_link(plane, value)
return result
def regular_state(joint, value=None):
return {"geometryOne": geometry_state(joint.geometryOrOriginOne, value),
"geometryTwo": geometry_state(joint.geometryOrOriginTwo, value),
"geometryOneTransform": matrix_data(joint.geometryOneTransform),
"geometryTwoTransform": matrix_data(joint.geometryTwoTransform),
"angle": parameter_state(joint.angle, "deg"),
"offset": parameter_state(joint.offset, "mm"),
"offsetX": parameter_state(joint.offsetX, "mm"),
"offsetY": parameter_state(joint.offsetY, "mm"),
"flipped": bool(joint.isFlipped), "locked": bool(joint.isLocked),
"ownerId": identity(joint.parentComponent),
"frameSpace": "native_joint_api", "transformTranslationUnit": "mm"}
def origin_data(origin, value=None):
origin = concrete(origin)
result = {"name": origin.name, "geometry": geometry_state(origin.geometry, value),
"flipped": bool(origin.isFlipped), "transform": matrix_data(origin.transform),
"xAxisEntity": entity_link(origin.xAxisEntity, value),
"zAxisEntity": entity_link(origin.zAxisEntity, value),
"primaryAxis": vector_data(origin.primaryAxisVector),
"secondaryAxis": vector_data(origin.secondaryAxisVector),
"thirdAxis": vector_data(origin.thirdAxisVector),
"ownerId": identity(origin.parentComponent),
"space": "root" if getattr(origin, "assemblyContext", None) else "component",
"units": {"length": "mm", "angle": "deg", "transformTranslation": "mm"}}
for name in ("angle", "offsetX", "offsetY", "offsetZ"):
result[name] = parameter_state(getattr(origin, name), "deg" if name == "angle" else "mm")
if value is not None:
result["ref"] = ref(origin, value)
return result
# Public motion names remain independent of Autodesk enum integer values.
LINK_MOTIONS = {
"RevoluteJointMotion": {"rotation": "RevoluteJointRotateMotionType"},
"SliderJointMotion": {"slide": "SliderJointSlideMotionType"},
"CylindricalJointMotion": {"rotation": "CylindricalJointRotateMotionType", "slide": "CylindricalJointSlideMotionType"},
"PinSlotJointMotion": {"rotation": "PinSlotJointRotateMotionType", "slide": "PinSlotJointSlideMotionType"},
"PlanarJointMotion": {"rotation": "PlanarJointRotateMotionType", "primarySlide": "PlanarJointSlideOneMotionType", "secondarySlide": "PlanarJointSlideTwoMotionType"},
"BallJointMotion": {"pitch": "BallJointPitchMotionType", "roll": "BallJointRollMotionType", "yaw": "BallJointYawMotionType"}}
def link_motion_name(joint, enum):
import adsk.fusion
from .assembly_state import joint_motion
names = LINK_MOTIONS.get(joint_motion(joint).objectType.split("::")[-1], {})
matches = [name for name, member in names.items() if getattr(adsk.fusion.JointMotionTypes, member) == enum]
require(len(matches) == 1, "unsupported_link_motion", "Motion link has an unsupported joint degree of freedom.")
return matches[0]
def link_data(link, value=None):
link = concrete(link)
first, second = concrete(link.jointOne), concrete(link.jointTwo)
names = [link_motion_name(joint, enum) for joint, enum in
((first, link.motionOne), (second or first, link.motionTwo))]
result = {"name": link.name, "suppressed": bool(link.isSuppressed), "reversed": bool(link.isReversed),
"jointOne": entity_link(first, value), "jointTwo": entity_link(second, value),
"sameJoint": second is None or same(first, second),
"motionOne": names[0], "motionTwo": names[1], "healthState": int(link.healthState),
"message": link.errorOrWarningMessage}
for suffix, name in zip(("One", "Two"), names):
result["value" + suffix] = parameter_state(getattr(link, "value" + suffix), "mm" if "lide" in name else "deg")
if value is not None:
result["ref"] = ref(link, value)
return result
def component_joint_state(comp):
return {"jointOrigins": [origin_data(o) for o in items(comp.jointOrigins)] if hasattr(comp, "jointOrigins") else [],
"motionLinks": [link_data(o) for o in items(comp.motionLinks)] if hasattr(comp, "motionLinks") else []}
MOTION_AXES = {
"RigidJointMotion": (),
"RevoluteJointMotion": (("rotationAxis", "customRotationAxisEntity"),),
"CylindricalJointMotion": (("rotationAxis", "customRotationAxisEntity"),),
"SliderJointMotion": (("slideDirection", "customSlideDirectionEntity"),),
"PinSlotJointMotion": (("rotationAxis", "customRotationAxisEntity"), ("slideDirection", "customSlideDirectionEntity")),
"PlanarJointMotion": (("normalDirection", "customNormalDirectionEntity"), ("primarySlideDirection", "customPrimarySlideDirectionEntity")),
"BallJointMotion": (("pitchDirection", "customPitchDirectionEntity"), ("yawDirection", "customYawDirectionEntity"))}
def motion_axes_state(motion, value=None):
kind = motion.objectType.split("::")[-1]
require(kind in MOTION_AXES, "unsupported_joint_motion", "Unknown joint direction contract.")
result = {}
for direction, source in MOTION_AXES[kind]:
enum = getattr(motion, direction)
custom = getattr(motion, source) if enum == adsk.fusion.JointDirections.CustomJointDirection else None
require(enum != adsk.fusion.JointDirections.CustomJointDirection or custom is not None,
"lost_joint_geometry", "Custom joint axis has no source entity.")
result[direction] = int(enum)
result[source] = entity_link(custom, value)
return result