Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,22 @@ def __init__(self, design: adsk.fusion.Design) -> None:
# populate all joints
self.dynamicJoints: dict[str, adsk.fusion.Joint] = dict()

# Maps an occurrence's entityToken to every joint touching it, along with that joint's two
# resolved occurrences. Populated once in __getAllJoints() and consulted from _populateNode()
# Avoids using `occ.joints` accessor, which is a documented Fusion API crash
# (native access violation) on certain occurrences (e.g. large/derived components).
self.occurrenceJoints: dict[
str, list[tuple[adsk.fusion.Joint, adsk.fusion.Occurrence, adsk.fusion.Occurrence]]
] = dict()

self.simulationNodesRef: dict[str, SimulationNode] = dict()

# TODO: need to look through every single joint and find the starting point that is connected to ground
# Next add that occurrence to the graph and then traverse down that path etc
self.__getAllJoints()

# dynamic joint node for grounded components and static components
logger.log(10, f"Populating node tree for grounded occurrence '{self.grounded.name}'")
populate_node_result = self._populateNode(self.grounded, None, None, is_ground=True)
if populate_node_result.is_err(): # We need the value to proceed
message = populate_node_result.unwrap_err()[0]
Expand All @@ -256,9 +265,11 @@ def __init__(self, design: adsk.fusion.Design) -> None:
self.simulationNodesRef["GROUND"] = self.groundSimNode

# combine all ground prior to this possibly
logger.log(10, f"Looking for grounded joints ({len(self.groundedConnections)} grounded connection(s))")
_ = self._lookForGroundedJoints()

# creates the axis elements - adds all elements to axisNodes
logger.log(10, f"Populating {len(self.dynamicJoints)} axis/axes")
for key, value in self.dynamicJoints.items():
populate_axis_result = self._populateAxis(key, value)
if populate_axis_result.is_err():
Expand All @@ -267,63 +278,109 @@ def __init__(self, design: adsk.fusion.Design) -> None:
___: Err[None] = Err(message, ErrorSeverity.Fatal)
raise RuntimeError()

logger.log(10, "Linking all axis/axes")
__ = self._linkAllAxis()

logger.log(10, "JointParser initialization complete")
# self.groundSimNode.printLink()

def _resolveOccurrenceFromGeometry(self, entity: Any, jointName: str, label: str) -> adsk.fusion.Occurrence | None:
try:
resolved = entity.assemblyContext if entity is not None else None
except Exception as e:
resolved = None
_: Err[None] = Err(f"Exception resolving {label} for joint '{jointName}': {e}", ErrorSeverity.Warning)

if resolved is None:
__: Err[None] = Err(
f"Could not resolve {label} for joint '{jointName}' "
"(geometry/origin reference is broken or orphaned, e.g. from a copy-paste)",
ErrorSeverity.Warning,
)

return resolved

def __getAllJoints(self) -> Result[None]:
logger.log(10, "Getting Joints")
for joint in list(self.design.rootComponent.allJoints) + list(self.design.rootComponent.allAsBuiltJoints):
allJoints = list(self.design.rootComponent.allJoints) + list(self.design.rootComponent.allAsBuiltJoints)
logger.log(10, f"Found {len(allJoints)} total joints/as-built-joints in design")

for index, joint in enumerate(allJoints):
jointName = joint.name if joint else "None"
logger.log(10, f"[{index + 1}/{len(allJoints)}] Resolving occurrences for joint '{jointName}'")

occurrenceOne: adsk.fusion.Occurrence | None = None
occurrenceTwo: adsk.fusion.Occurrence | None = None

if joint and joint.occurrenceOne and joint.occurrenceTwo:
occurrenceOne = joint.occurrenceOne
occurrenceTwo = joint.occurrenceTwo
else:
# Non-fatal since it's recovered in the next two statements
_: Err[None] = Err("Found joint without two occurrences", ErrorSeverity.Warning)
_ = Err(f"Joint '{jointName}' found without two occurrences", ErrorSeverity.Warning)

if occurrenceOne is None:
if joint.geometryOrOriginOne.entityOne.assemblyContext is None:
____: Err[None] = Err(
"occurrenceOne and entityOne's assembly context are None", ErrorSeverity.Fatal
try:
geometryOrOriginOne = joint.geometryOrOriginOne
entityOne = geometryOrOriginOne.entityOne if geometryOrOriginOne is not None else None
except Exception as e:
entityOne = None
_ = Err(
f"Exception accessing geometryOrOriginOne for joint '{jointName}': {e}", ErrorSeverity.Warning
)
occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext
occurrenceOne = self._resolveOccurrenceFromGeometry(entityOne, jointName, "occurrenceOne")

if occurrenceTwo is None:
if joint.geometryOrOriginTwo.entityTwo.assemblyContext is None:
__: Err[None] = Err("occurrenceOne and entityTwo's assembly context are None", ErrorSeverity.Fatal)
occurrenceTwo = joint.geometryOrOriginTwo.entityTwo.assemblyContext
try:
geometryOrOriginTwo = joint.geometryOrOriginTwo
entityTwo = geometryOrOriginTwo.entityTwo if geometryOrOriginTwo is not None else None
except Exception as e:
entityTwo = None
_ = Err(
f"Exception accessing geometryOrOriginTwo for joint '{jointName}': {e}", ErrorSeverity.Warning
)
occurrenceTwo = self._resolveOccurrenceFromGeometry(entityTwo, jointName, "occurrenceTwo")

oneEntityToken = ""
twoEntityToken = ""

# TODO: Fix change to if statement with Result returning
try:
oneEntityToken = occurrenceOne.entityToken
except:
oneEntityToken = occurrenceOne.name

try:
twoEntityToken = occurrenceTwo.entityToken
except:
twoEntityToken = occurrenceTwo.name
if occurrenceOne is not None:
try:
oneEntityToken = occurrenceOne.entityToken
except Exception:
oneEntityToken = occurrenceOne.name

if occurrenceTwo is not None:
try:
twoEntityToken = occurrenceTwo.entityToken
except Exception:
twoEntityToken = occurrenceTwo.name

if occurrenceOne is None or occurrenceTwo is None:
# Already logged above (either missing both occurrences, or an unresolvable geometry/origin
# reference) - skip this joint rather than aborting the whole export
continue

self.occurrenceJoints.setdefault(oneEntityToken, []).append((joint, occurrenceOne, occurrenceTwo))
if twoEntityToken != oneEntityToken:
self.occurrenceJoints.setdefault(twoEntityToken, []).append((joint, occurrenceOne, occurrenceTwo))

typeJoint = joint.jointMotion.jointType

if typeJoint != 0:
if oneEntityToken not in self.dynamicJoints.keys():
self.dynamicJoints[oneEntityToken] = joint

# TODO: Check if this is fatal or not
if occurrenceTwo is None and occurrenceOne is None:
___: Err[None] = Err(
f"Occurrences that connect joints could not be found\n\t1: {occurrenceOne}\n\t2: {occurrenceTwo}",
ErrorSeverity.Fatal,
)
else:
if oneEntityToken == self.grounded.entityToken:
self.groundedConnections.append(occurrenceTwo)
elif twoEntityToken == self.grounded.entityToken:
self.groundedConnections.append(occurrenceOne)
logger.log(
10,
f"Finished Getting Joints: {len(self.dynamicJoints)} dynamic joint(s), "
f"{len(self.groundedConnections)} grounded connection(s)",
)
return Ok(None)

def _linkAllAxis(self) -> Result[None]:
Expand Down Expand Up @@ -415,37 +472,32 @@ def _populateNode(
if populate_result.is_fatal():
return populate_result

# if not is_ground: # THIS IS A BUG - OCCURRENCE ACCESS VIOLATION
# this is the current reason for wrapping in try except pass
for joint in occ.joints:
if joint and joint.occurrenceOne and joint.occurrenceTwo:
occurrenceOne = joint.occurrenceOne
occurrenceTwo = joint.occurrenceTwo
connection = None
rigid = joint.jointMotion.jointType == 0

if rigid:
if joint.occurrenceOne == occ:
connection = joint.occurrenceTwo
if joint.occurrenceTwo == occ:
connection = joint.occurrenceOne
else:
if joint.occurrenceOne != occ:
connection = joint.occurrenceOne

if connection is not None:
if prev is None or connection.entityToken != prev.data.entityToken:
populate_result = self._populateNode(
connection,
node,
(OccurrenceRelationship.CONNECTION if rigid else OccurrenceRelationship.NEXT),
is_ground=is_ground,
)
if populate_result.is_fatal():
return populate_result
# Deliberately not using the `occ.joints` accessor here as it is a documented Fusion API
# crash. Instead look up joints touching occurrences from the map built once in __getAllJoints()
occJoints = self.occurrenceJoints.get(occ.entityToken, [])
for joint, occurrenceOne, occurrenceTwo in occJoints:
connection = None
rigid = joint.jointMotion.jointType == 0

if rigid:
if occurrenceOne.entityToken == occ.entityToken:
connection = occurrenceTwo
if occurrenceTwo.entityToken == occ.entityToken:
connection = occurrenceOne
else:
# Check if this joint occurance violation is really a fatal error or just something we should filter on
return Err("Joint without two occurrences", ErrorSeverity.Fatal)
if occurrenceOne.entityToken != occ.entityToken:
connection = occurrenceOne

if connection is not None:
if prev is None or connection.entityToken != prev.data.entityToken:
populate_result = self._populateNode(
connection,
node,
(OccurrenceRelationship.CONNECTION if rigid else OccurrenceRelationship.NEXT),
is_ground=is_ground,
)
if populate_result.is_fatal():
return populate_result

if prev is not None:
edge = DynamicEdge(relationship, node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,18 @@ def _addJointInstance(


def _addRigidGroup(joint: adsk.fusion.Joint, assembly: assembly_pb2.Assembly) -> None:
if joint.jointMotion.jointType != 0 or not (
joint.occurrenceOne.isLightBulbOn and joint.occurrenceTwo.isLightBulbOn
):
if joint.jointMotion.jointType != 0:
return

if joint.occurrenceOne is None or joint.occurrenceTwo is None:
# A rigid joint can end up with a broken/orphaned occurrence reference (e.g. after copy-pasting
# a component that carried a joint along with it) - skip it rather than crashing the export.
_: Err[None] = Err(
f"Rigid joint '{joint.name}' has a missing occurrence, skipping rigid group", ErrorSeverity.Warning
)
return

if not (joint.occurrenceOne.isLightBulbOn and joint.occurrenceTwo.isLightBulbOn):
return

mira_group = joint_pb2.RigidGroup()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def reload() -> None:
import importlib

importlib.reload(Components)
importlib.reload(JointHierarchy)
importlib.reload(Joints)
importlib.reload(Materials)
importlib.reload(PDMessage)
Expand Down
5 changes: 4 additions & 1 deletion exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import os
import re
import traceback
import webbrowser
from typing import Any

Expand Down Expand Up @@ -363,6 +364,7 @@ def export(exporterOptions: moduleExporterOptions.ExporterOptions, html_args: ad
try:
Parser.Parser(exporterOptions).export()
except RuntimeError as e:
logger.error(f"Export failed with RuntimeError:\n{traceback.format_exc()}")
html_args.returnData = json.dumps({"_err": str(e)})
return
exporterOptions.writeToDesign()
Expand Down Expand Up @@ -434,7 +436,8 @@ def notify(self, _: adsk.core.CommandEventArgs) -> None:

try:
Parser.Parser(exporterOptions).export()
except:
except Exception:
logger.error(f"Export failed:\n{traceback.format_exc()}")
jointConfigTab.reset()
gamepieceConfigTab.reset()

Expand Down
Loading