From 7be2246d707f0b9d6fdd6a73aef882fc374488aa Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 25 Jun 2025 16:46:31 -0700 Subject: [PATCH 01/68] feat: basic result error handling system --- .../SynthesisFusionAddin/src/ErrorHandling.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 exporter/SynthesisFusionAddin/src/ErrorHandling.py diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py new file mode 100644 index 0000000000..1988b21649 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -0,0 +1,43 @@ +from enum import Enum +from typing import Generic, TypeVar + +class ErrorSeverity(Enum): + Fatal = 1 + Warning = 2 +type ErrorMessage = str +type Error = tuple[ErrorSeverity, ErrorMessage] + +T = TypeVar('T') + +class Result(Generic[T]): + def is_ok(self) -> bool: + return isinstance(self, Ok) + + def is_err(self) -> bool: + return isinstance(self, Err) + + def unwrap(self) -> T: + if self.is_ok(): + return self.value # type: ignore + raise Exception(f"Called unwrap on Err: {self.error}") # type: ignore + + def unwrap_err(self) -> Error: + if self.is_err(): + return self.error # type: ignore + raise Exception("Called unwrap_err on Ok: {self.value}") + +class Ok(Result[T]): + value: T + def __init__(self, value: T): + self.value = value + + def __repr__(self): + return f"Ok({self.value})" + +class Err(Result[T]): + error: Error + def __init__(self, error: Error): + self.error = error + + def __repr__(self): + return f"Err({self.error})" From e5be04ccbf552dd1c8cfa8c58bb2c872666c8017 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 25 Jun 2025 18:04:01 -0700 Subject: [PATCH 02/68] feat: start applying value-based error handling system to materials file --- .../SynthesisFusionAddin/src/ErrorHandling.py | 12 +-- .../src/Parser/SynthesisParser/Materials.py | 31 ++++++-- .../src/Parser/SynthesisParser/Utilities.py | 79 +++---------------- 3 files changed, 46 insertions(+), 76 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 1988b21649..752e0b9354 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -1,11 +1,11 @@ from enum import Enum from typing import Generic, TypeVar +# TODO Figure out if we need an error severity system +# Warnings are kind of useless if they break control flow anyways, so why not just replace warnings with writing to a log file and have errors break control flow class ErrorSeverity(Enum): Fatal = 1 Warning = 2 -type ErrorMessage = str -type Error = tuple[ErrorSeverity, ErrorMessage] T = TypeVar('T') @@ -35,9 +35,11 @@ def __repr__(self): return f"Ok({self.value})" class Err(Result[T]): - error: Error - def __init__(self, error: Error): - self.error = error + message: str + severity: ErrorSeverity + def __init__(self, message: str, severity: ErrorSeverity): + self.message = message + self.severity = severity def __repr__(self): return f"Err({self.error})" diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 08c5e17bb3..133136e52e 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -5,6 +5,7 @@ from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import construct_info, fill_info from src.Proto import material_pb2 +from ErrorHandling import ErrorMessage, ErrorSeverity, Result, Ok, Err OPACITY_RAMPING_CONSTANT = 14.0 @@ -44,8 +45,10 @@ def _MapAllPhysicalMaterials( getPhysicalMaterialData(material, newmaterial, options) -def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions) -> None: - construct_info("default", physicalMaterial) +def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions) -> Result[None]: + construct_info_result = construct_info("default", physicalMaterial) + if construct_info_result.is_err(): + return construct_info_result physicalMaterial.description = "A default physical material" if options.frictionOverride: @@ -59,11 +62,12 @@ def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: physicalMaterial.deformable = False physicalMaterial.matType = 0 # type: ignore[assignment] + return Ok(None) @logFailure def getPhysicalMaterialData( fusionMaterial: adsk.core.Material, physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions -) -> None: +) -> Result[None]: """Gets the material data and adds it to protobuf Args: @@ -71,7 +75,9 @@ def getPhysicalMaterialData( proto_material (protomaterial): proto material mirabuf options (parseoptions): parse options """ - construct_info("", physicalMaterial, fus_object=fusionMaterial) + construct_info_result = construct_info("", physicalMaterial, fus_object=fusionMaterial) + if construct_info_result.is_err(): + return construct_info_result physicalMaterial.deformable = False physicalMaterial.matType = 0 # type: ignore[assignment] @@ -127,17 +133,28 @@ def getPhysicalMaterialData( mechanicalProperties.density = materialProperties.itemById("structural_Density").value mechanicalProperties.damping_coefficient = materialProperties.itemById("structural_Damping_coefficient").value + missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] #ignore: type + if missingProperties.__len__() > 0: + return Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) + """ Strength Properties """ strengthProperties.yield_strength = materialProperties.itemById("structural_Minimum_yield_stress").value strengthProperties.tensile_strength = materialProperties.itemById("structural_Minimum_tensile_strength").value + + missingProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] #ignore: type + if missingProperties.__len__() > 0: + return Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) + """ strengthProperties.thermal_treatment = materialProperties.itemById( "structural_Thermally_treated" ).value """ + return Ok(None) + def _MapAllAppearances( appearances: list[material_pb2.Appearance], @@ -154,6 +171,8 @@ def _MapAllAppearances( for appearance in appearances: progressDialog.addAppearance(appearance.name) + # NOTE I'm not sure if this should be integrated with the error handling system or not, since it's fully intentional and immediantly aborts, which is the desired behavior + # TODO Talk to Brandon about this if progressDialog.wasCancelled(): raise RuntimeError("User canceled export") @@ -169,7 +188,9 @@ def setDefaultAppearance(appearance: material_pb2.Appearance) -> None: """ # add info - construct_info("default", appearance) + construct_info_result = construct_info("default", appearance) + if construct_info_result.is_err(): + return construct_info_result appearance.roughness = 0.5 appearance.metallic = 0.5 diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index d8f38d921f..6b84d5e8ac 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -4,7 +4,8 @@ import adsk.core import adsk.fusion -from src.Proto import assembly_pb2 +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result +from src.Proto import assembly_pb2, types_pb2 def guid_component(comp: adsk.fusion.Component) -> str: @@ -19,8 +20,8 @@ def guid_none(_: None) -> str: return str(uuid.uuid4()) -def fill_info(proto_obj: assembly_pb2.Assembly, fus_object: adsk.core.Base, override_guid: str | None = None) -> None: - construct_info("", proto_obj, fus_object=fus_object, GUID=override_guid) +def fill_info(proto_obj: assembly_pb2.Assembly, fus_object: adsk.core.Base, override_guid: str | None = None) -> Result[None]: + return construct_info("", proto_obj, fus_object=fus_object, GUID=override_guid) def construct_info( @@ -29,7 +30,8 @@ def construct_info( version: int = 5, fus_object: adsk.core.Base | None = None, GUID: str | None = None, -) -> None: +) -> Result[None]: + # TODO Fix out of date documentation """Constructs a info object from either a name or a fus_object Args: @@ -49,8 +51,10 @@ def construct_info( if fus_object is not None: proto_obj.info.name = fus_object.name - else: + elif name != "": proto_obj.info.name = name + else: + return Err("Attempted to set proto_obj.info.name to None", ErrorSeverity.Warning) if GUID is not None: proto_obj.info.GUID = str(GUID) @@ -59,29 +63,13 @@ def construct_info( else: proto_obj.info.GUID = str(uuid.uuid4()) + return Ok(None) + -# Transition: AARD-1765 -# Will likely be removed later as this is no longer used. Avoiding adding typing for now. -# My previous function was alot more optimized however now I realize the bug was this doesn't work well with degrees -def euler_to_quaternion(r): # type: ignore - (yaw, pitch, roll) = (r[0], r[1], r[2]) - qx = math.sin(roll / 2) * math.cos(pitch / 2) * math.cos(yaw / 2) - math.cos(roll / 2) * math.sin( - pitch / 2 - ) * math.sin(yaw / 2) - qy = math.cos(roll / 2) * math.sin(pitch / 2) * math.cos(yaw / 2) + math.sin(roll / 2) * math.cos( - pitch / 2 - ) * math.sin(yaw / 2) - qz = math.cos(roll / 2) * math.cos(pitch / 2) * math.sin(yaw / 2) - math.sin(roll / 2) * math.sin( - pitch / 2 - ) * math.cos(yaw / 2) - qw = math.cos(roll / 2) * math.cos(pitch / 2) * math.cos(yaw / 2) + math.sin(roll / 2) * math.sin( - pitch / 2 - ) * math.sin(yaw / 2) - return [qx, qy, qz, qw] def rad_to_deg(rad): # type: ignore - """Very simple method to convert Radians to degrees + """Converts radians to degrees Args: rad (float): radians unit @@ -91,49 +79,8 @@ def rad_to_deg(rad): # type: ignore """ return (rad * 180) / math.pi - -def quaternion_to_euler(qx, qy, qz, qw): # type: ignore - """Takes in quat values and converts to degrees - - - roll is x axis - atan2(2(qwqy + qzqw), 1-2(qy^2 + qz^2)) - - pitch is y axis - asin(2(qxqz - qwqy)) - - yaw is z axis - atan2(2(qxqw + qyqz), 1-2(qz^2+qw^3)) - - Args: - qx (float): quat_x - qy (float): quat_y - qz (float): quat_z - qw (float): quat_w - - Returns: - roll: x value in degrees - pitch: y value in degrees - yaw: z value in degrees - """ - # roll - sr_cp = 2 * ((qw * qx) + (qy * qz)) - cr_cp = 1 - (2 * ((qx * qx) + (qy * qy))) - roll = math.atan2(sr_cp, cr_cp) - # pitch - sp = 2 * ((qw * qy) - (qz * qx)) - if abs(sp) >= 1: - pitch = math.copysign(math.pi / 2, sp) - else: - pitch = math.asin(sp) - # yaw - sy_cp = 2 * ((qw * qz) + (qx * qy)) - cy_cp = 1 - (2 * ((qy * qy) + (qz * qz))) - yaw = math.atan2(sy_cp, cy_cp) - # convert to degrees - roll = rad_to_deg(roll) - pitch = rad_to_deg(pitch) - yaw = rad_to_deg(yaw) - # round and return - return round(roll, 4), round(pitch, 4), round(yaw, 4) - - def throwZero(): # type: ignore - """Simple function to report incorrect quat values + """Errors on incorrect quat values Raises: RuntimeError: Error describing the issue From bfea5493311ce64e22e29241951567740e343c8d Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 10:44:37 -0700 Subject: [PATCH 03/68] feat: error handling in components and materials --- .../SynthesisFusionAddin/src/ErrorHandling.py | 23 ++-- .../src/Parser/SynthesisParser/Components.py | 110 ++++++++++++------ .../src/Parser/SynthesisParser/Materials.py | 51 +++++--- .../src/Parser/SynthesisParser/Parser.py | 29 +++-- .../src/Parser/SynthesisParser/Utilities.py | 6 +- 5 files changed, 150 insertions(+), 69 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 752e0b9354..dc08bdcbe7 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -1,8 +1,10 @@ +from .Logging import getLogger from enum import Enum from typing import Generic, TypeVar -# TODO Figure out if we need an error severity system -# Warnings are kind of useless if they break control flow anyways, so why not just replace warnings with writing to a log file and have errors break control flow +# NOTE +# Severity refers to to the error's affect on the parser as a whole, rather than on the function itself +# If an error is non-fatal to the function that generated it, it should be declared but not return, which prints it to the screen class ErrorSeverity(Enum): Fatal = 1 Warning = 2 @@ -19,12 +21,12 @@ def is_err(self) -> bool: def unwrap(self) -> T: if self.is_ok(): return self.value # type: ignore - raise Exception(f"Called unwrap on Err: {self.error}") # type: ignore + raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore - def unwrap_err(self) -> Error: + def unwrap_err(self) -> tuple[str, ErrorSeverity]: if self.is_err(): - return self.error # type: ignore - raise Exception("Called unwrap_err on Ok: {self.value}") + return tuple[self.message, self.severity] # type: ignore + raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore class Ok(Result[T]): value: T @@ -41,5 +43,12 @@ def __init__(self, message: str, severity: ErrorSeverity): self.message = message self.severity = severity + self.write_error() + def __repr__(self): - return f"Err({self.error})" + return f"Err({self.message})" + + def write_error(self) -> None: + logger = getLogger() + # Figure out how to integrate severity with the logger + logger.log(1, self.message) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 6a78ad7cdf..c6d206e7a4 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -1,7 +1,9 @@ # Contains all of the logic for mapping the Components / Occurrences +from requests.models import parse_header_links import adsk.core import adsk.fusion +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser import PhysicalProperties @@ -15,15 +17,13 @@ from src.Types import ExportMode # TODO: Impelement Material overrides - - -def _MapAllComponents( +def MapAllComponents( design: adsk.fusion.Design, options: ExporterOptions, progressDialog: PDMessage, partsData: assembly_pb2.Parts, materials: material_pb2.Materials, -) -> None: +) -> Result[None]: for component in design.allComponents: adsk.doEvents() if progressDialog.wasCancelled(): @@ -32,31 +32,42 @@ def _MapAllComponents( comp_ref = guid_component(component) - fill_info(partsData, None) + fill_info_result = fill_info(partsData, None) + if fill_info_result.is_err(): + return fill_info_result + partDefinition = partsData.part_definitions[comp_ref] - fill_info(partDefinition, component, comp_ref) + fill_info_result = fill_info(partDefinition, component, comp_ref) + if fill_info_result.is_err(): + return fill_info_result + PhysicalProperties.GetPhysicalProperties(component, partDefinition.physical_data) - if options.exportMode == ExportMode.FIELD: - partDefinition.dynamic = False - else: - partDefinition.dynamic = True + partDefinition.dynamic = options.exportMode != ExportMode.FIELD - def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> None: + def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[None]: if progressDialog.wasCancelled(): raise RuntimeError("User canceled export") if body.isLightBulbOn: part_body = partDefinition.bodies.add() - fill_info(part_body, body) part_body.part = comp_ref + fill_info_result = fill_info(part_body, body) + if fill_info_result.is_err(): + return fill_info_result + if isinstance(body, adsk.fusion.BRepBody): - _ParseBRep(body, options, part_body.triangle_mesh) + parse_result = _ParseBRep(body, options, part_body.triangle_mesh) + if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: + return parse_result else: - _ParseMesh(body, options, part_body.triangle_mesh) + parse_result = _ParseMesh(body, options, part_body.triangle_mesh) + if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: + return parse_result + appearance_key = "{}_{}".format(body.appearance.name, body.appearance.id) # this should be appearance @@ -66,13 +77,17 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> None: part_body.appearance_override = "default" for body in component.bRepBodies: - processBody(body) - + process_result = processBody(body) + if process_result.is_err() and process_result.unwrap_err()[0] == ErrorSeverity.Fatal: + return process_result for body in component.meshBodies: - processBody(body) + process_result = processBody(body) + if process_result.is_err() and process_result.unwrap_err()[0] == ErrorSeverity.Fatal: + return process_result + -def _ParseComponentRoot( +def ParseComponentRoot( component: adsk.fusion.Component, progressDialog: PDMessage, options: ExporterOptions, @@ -86,7 +101,9 @@ def _ParseComponentRoot( node.value = mapConstant - fill_info(part, component, mapConstant) + fill_info_result = fill_info(part, component, mapConstant) + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return fill_info_result def_map = partsData.part_definitions @@ -99,18 +116,22 @@ def _ParseComponentRoot( if occur.isLightBulbOn: child_node = types_pb2.Node() - __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + + parse_child_result = __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + if parse_child_result.is_err(): + return parse_child_result + node.children.append(child_node) -def __parseChildOccurrence( +def parseChildOccurrence( occurrence: adsk.fusion.Occurrence, progressDialog: PDMessage, options: ExporterOptions, partsData: assembly_pb2.Parts, material_map: dict[str, material_pb2.Appearance], node: types_pb2.Node, -) -> None: +) -> Result[None]: if occurrence.isLightBulbOn is False: return @@ -124,7 +145,9 @@ def __parseChildOccurrence( node.value = mapConstant - fill_info(part, occurrence, mapConstant) + fill_info_result = fill_info(part, occurrence, mapConstant) + if fill_info_result.is_err() and fill_info_result.unwrap_err() == ErrorSeverity.Fatal: + return fill_info_result collision_attr = occurrence.attributes.itemByName("synthesis", "collision_off") if collision_attr != None: @@ -134,11 +157,15 @@ def __parseChildOccurrence( try: part.appearance = "{}_{}".format(occurrence.appearance.name, occurrence.appearance.id) except: + _ = Err("Failed to format part appearance", ErrorSeverity.Warning); # ignore: type part.appearance = "default" # TODO: Add phyical_material parser + # TODO: I'm fairly sure that this should be a fatal error if occurrence.component.material: part.physical_material = occurrence.component.material.id + else: + _ = Err(f"Component Material is None", ErrorSeverity.Warning) def_map = partsData.part_definitions @@ -165,7 +192,11 @@ def __parseChildOccurrence( if occur.isLightBulbOn: child_node = types_pb2.Node() - __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + + parse_child_result = __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + if parse_child_result.is_err(): + return parse_child_result + node.children.append(child_node) @@ -180,12 +211,11 @@ def GetMatrixWorld(occurrence: adsk.fusion.Occurrence) -> adsk.core.Matrix3D: return matrix -@logFailure -def _ParseBRep( +def ParseBRep( body: adsk.fusion.BRepBody, options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, -) -> None: +) -> Result[None]: meshManager = body.meshManager calc = meshManager.createMeshCalculator() # Disabling for now. We need the user to be able to adjust this, otherwise it gets locked @@ -196,26 +226,36 @@ def _ParseBRep( # calc.surfaceTolerance = 0.5 mesh = calc.calculate() - fill_info(trimesh, body) + fill_info_result = fill_info(trimesh, body) + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return fill_info_result + trimesh.has_volume = True plainmesh_out = trimesh.mesh - plainmesh_out.verts.extend(mesh.nodeCoordinatesAsFloat) plainmesh_out.normals.extend(mesh.normalVectorsAsFloat) plainmesh_out.indices.extend(mesh.nodeIndices) plainmesh_out.uv.extend(mesh.textureCoordinatesAsFloat) + return Ok(None) + -@logFailure -def _ParseMesh( +def ParseMesh( meshBody: adsk.fusion.MeshBody, options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, -) -> None: +) -> Result[None]: mesh = meshBody.displayMesh + if mesh is None: + return Err("Component Mesh was None", ErrorSeverity.Fatal) + + + fill_info_result = fill_info(trimesh, meshBody) + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return fill_info_result + - fill_info(trimesh, meshBody) trimesh.has_volume = True plainmesh_out = trimesh.mesh @@ -225,8 +265,10 @@ def _ParseMesh( plainmesh_out.indices.extend(mesh.nodeIndices) plainmesh_out.uv.extend(mesh.textureCoordinatesAsFloat) + return Ok(None) + -def _MapRigidGroups(rootComponent: adsk.fusion.Component, joints: joint_pb2.Joints) -> None: +def MapRigidGroups(rootComponent: adsk.fusion.Component, joints: joint_pb2.Joints) -> None: groups = rootComponent.allRigidGroups for group in groups: mira_group = joint_pb2.RigidGroup() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 133136e52e..3456683c7b 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -1,11 +1,10 @@ import adsk.core -from src.Logging import logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import construct_info, fill_info from src.Proto import material_pb2 -from ErrorHandling import ErrorMessage, ErrorSeverity, Result, Ok, Err +from src.ErrorHandling import ErrorSeverity, Result, Ok, Err OPACITY_RAMPING_CONSTANT = 14.0 @@ -27,22 +26,32 @@ } -def _MapAllPhysicalMaterials( +def MapAllPhysicalMaterials( physicalMaterials: list[material_pb2.PhysicalMaterial], materials: material_pb2.Materials, options: ExporterOptions, progressDialog: PDMessage, -) -> None: - setDefaultMaterial(materials.physicalMaterials["default"], options) +) -> Result[None]: + set_result = setDefaultMaterial(materials.physicalMaterials["default"], options) + if set_result.is_err() and set_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return set_result for material in physicalMaterials: - progressDialog.addMaterial(material.name) + if material.name is None or material.id is None: + return Err("Material missing id or name", ErrorSeverity.Fatal) + progressDialog.addMaterial(material.name) if progressDialog.wasCancelled(): raise RuntimeError("User canceled export") newmaterial = materials.physicalMaterials[material.id] - getPhysicalMaterialData(material, newmaterial, options) + material_result = getPhysicalMaterialData(material, newmaterial, options) + if material_result.is_err() and material_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return material_result + + return Ok(None) + + def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions) -> Result[None]: @@ -64,7 +73,6 @@ def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: return Ok(None) -@logFailure def getPhysicalMaterialData( fusionMaterial: adsk.core.Material, physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions ) -> Result[None]: @@ -156,17 +164,19 @@ def getPhysicalMaterialData( return Ok(None) -def _MapAllAppearances( +def MapAllAppearances( appearances: list[material_pb2.Appearance], materials: material_pb2.Materials, options: ExporterOptions, progressDialog: PDMessage, -) -> None: +) -> Result[None]: # in case there are no appearances on a body # this is just a color tho setDefaultAppearance(materials.appearances["default"]) - fill_info(materials, None) + fill_info_result = fill_info(materials, None) + if fill_info_result.is_err(): + return fill_info_result for appearance in appearances: progressDialog.addAppearance(appearance.name) @@ -177,10 +187,14 @@ def _MapAllAppearances( raise RuntimeError("User canceled export") material = materials.appearances["{}_{}".format(appearance.name, appearance.id)] - getMaterialAppearance(appearance, options, material) + material_result = getMaterialAppearance(appearance, options, material) + if material_result.is_err() and material_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return material_result + + return Ok(None) -def setDefaultAppearance(appearance: material_pb2.Appearance) -> None: +def setDefaultAppearance(appearance: material_pb2.Appearance) -> Result[None]: """Get a default color for the appearance Returns: @@ -202,18 +216,21 @@ def setDefaultAppearance(appearance: material_pb2.Appearance) -> None: color.B = 127 color.A = 255 + return Ok(None) def getMaterialAppearance( fusionAppearance: adsk.core.Appearance, options: ExporterOptions, appearance: material_pb2.Appearance, -) -> None: +) -> Result[None]: """Takes in a Fusion Mesh and converts it to a usable unity mesh Args: fusionAppearance (adsk.core.Appearance): Fusion appearance material """ - construct_info("", appearance, fus_object=fusionAppearance) + construct_info_result = construct_info("", appearance, fus_object=fusionAppearance) + if construct_info_result.is_err(): + return construct_info_result appearance.roughness = 0.9 appearance.metallic = 0.3 @@ -227,12 +244,15 @@ def getMaterialAppearance( color.A = 127 properties = fusionAppearance.appearanceProperties + if properties is None: + return Err("Apperarance Properties were None", ErrorSeverity.Fatal) roughnessProp = properties.itemById("surface_roughness") if roughnessProp: appearance.roughness = roughnessProp.value # Thank Liam for this. + # TODO Test if this is should be an error that we're just ignoring, or if it's actually just something we can skip over modelItem = properties.itemById("interior_model") if modelItem: matModelType = modelItem.value @@ -281,3 +301,4 @@ def getMaterialAppearance( color.B = baseColor.blue color.A = baseColor.opacity break + return Ok(None) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index b085b48d32..5bd22c631d 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -1,12 +1,15 @@ import gzip import pathlib +from google.protobuf import message + import adsk.core import adsk.fusion from google.protobuf.json_format import MessageToJson from src import gm from src.APS.APS import getAuth, upload_mirabuf +from src.ErrorHandling import ErrorSeverity, Result from src.Logging import getLogger, logFailure, timed from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser import ( @@ -44,11 +47,11 @@ def export(self) -> None: return assembly_out = assembly_pb2.Assembly() - fill_info( + handle_err_top(fill_info( assembly_out, design.rootComponent, override_guid=design.parentDocument.name, - ) + )) # set int to 0 in dropdown selection for dynamic assembly_out.dynamic = self.exporterOptions.exportMode == ExportMode.ROBOT @@ -77,21 +80,21 @@ def export(self) -> None: progressDialog, ) - Materials._MapAllAppearances( + handle_err_top(Materials._MapAllAppearances( design.appearances, assembly_out.data.materials, self.exporterOptions, self.pdMessage, - ) - - Materials._MapAllPhysicalMaterials( + )) + + handle_err_top(Materials.MapAllPhysicalMaterials( design.materials, assembly_out.data.materials, self.exporterOptions, self.pdMessage, - ) + )) - Components._MapAllComponents( + Components.MapAllComponents( design, self.exporterOptions, self.pdMessage, @@ -101,7 +104,7 @@ def export(self) -> None: rootNode = types_pb2.Node() - Components._ParseComponentRoot( + Components.ParseComponentRoot( design.rootComponent, self.pdMessage, self.exporterOptions, @@ -110,7 +113,7 @@ def export(self) -> None: rootNode, ) - Components._MapRigidGroups(design.rootComponent, assembly_out.data.joints) + Components.MapRigidGroups(design.rootComponent, assembly_out.data.joints) assembly_out.design_hierarchy.nodes.append(rootNode) @@ -249,3 +252,9 @@ def export(self) -> None: ) logger.debug(debug_output.strip()) + +def handle_err_top[T](err: Result[T]): + if err.is_err(): + message, severity = err.unwrap_err() + if severity == ErrorSeverity.Fatal: + app.userInterface.messageBox(f"Fatal Error Encountered: {message}") diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index 6b84d5e8ac..2e45497372 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -5,7 +5,7 @@ import adsk.fusion from src.ErrorHandling import Err, ErrorSeverity, Ok, Result -from src.Proto import assembly_pb2, types_pb2 +from src.Proto import assembly_pb2, material_pb2, types_pb2 def guid_component(comp: adsk.fusion.Component) -> str: @@ -20,13 +20,13 @@ def guid_none(_: None) -> str: return str(uuid.uuid4()) -def fill_info(proto_obj: assembly_pb2.Assembly, fus_object: adsk.core.Base, override_guid: str | None = None) -> Result[None]: +def fill_info(proto_obj: assembly_pb2.Assembly | material_pb2.Materials, fus_object: adsk.core.Base, override_guid: str | None = None) -> Result[None]: return construct_info("", proto_obj, fus_object=fus_object, GUID=override_guid) def construct_info( name: str, - proto_obj: assembly_pb2.Assembly, + proto_obj: assembly_pb2.Assembly | material_pb2.Materials | material_pb2.PhysicalMaterial, version: int = 5, fus_object: adsk.core.Base | None = None, GUID: str | None = None, From 9b19be0e382c6819e9b6eedafe49cfa33adcb031 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 10:52:25 -0700 Subject: [PATCH 04/68] fix: typing and function name --- .../src/Parser/SynthesisParser/Components.py | 2 +- .../src/Parser/SynthesisParser/Parser.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index c6d206e7a4..85c5f994ef 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -94,7 +94,7 @@ def ParseComponentRoot( partsData: assembly_pb2.Parts, material_map: dict[str, material_pb2.Appearance], node: types_pb2.Node, -) -> None: +) -> Result[None]: mapConstant = guid_component(component) part = partsData.part_instances[mapConstant] diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 5bd22c631d..968b0d8738 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -80,7 +80,7 @@ def export(self) -> None: progressDialog, ) - handle_err_top(Materials._MapAllAppearances( + handle_err_top(Materials.MapAllAppearances( design.appearances, assembly_out.data.materials, self.exporterOptions, @@ -94,24 +94,24 @@ def export(self) -> None: self.pdMessage, )) - Components.MapAllComponents( + handle_err_top(Components.MapAllComponents( design, self.exporterOptions, self.pdMessage, assembly_out.data.parts, assembly_out.data.materials, - ) + )) rootNode = types_pb2.Node() - Components.ParseComponentRoot( + handle_err_top(Components.ParseComponentRoot( design.rootComponent, self.pdMessage, self.exporterOptions, assembly_out.data.parts, assembly_out.data.materials.appearances, rootNode, - ) + )) Components.MapRigidGroups(design.rootComponent, assembly_out.data.joints) From b87f72ac96c575844bd200fee8731279ee52adb0 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 13:06:49 -0700 Subject: [PATCH 05/68] fix: tuple literal --- exporter/SynthesisFusionAddin/src/ErrorHandling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index dc08bdcbe7..2d1594f879 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -25,7 +25,7 @@ def unwrap(self) -> T: def unwrap_err(self) -> tuple[str, ErrorSeverity]: if self.is_err(): - return tuple[self.message, self.severity] # type: ignore + return (self.message, self.severity) # type: ignore raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore class Ok(Result[T]): From db071036637541b1560c156a5b9d70e217cf0648 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 13:47:02 -0700 Subject: [PATCH 06/68] fix: updated old variable names (Components and Materials fully done :D) --- .../src/Parser/SynthesisParser/Components.py | 12 +++++++----- .../src/Parser/SynthesisParser/Materials.py | 5 ++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 85c5f994ef..fd4add0bb2 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -60,11 +60,11 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non return fill_info_result if isinstance(body, adsk.fusion.BRepBody): - parse_result = _ParseBRep(body, options, part_body.triangle_mesh) + parse_result = ParseBRep(body, options, part_body.triangle_mesh) if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: return parse_result else: - parse_result = _ParseMesh(body, options, part_body.triangle_mesh) + parse_result = ParseMesh(body, options, part_body.triangle_mesh) if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: return parse_result @@ -117,11 +117,12 @@ def ParseComponentRoot( if occur.isLightBulbOn: child_node = types_pb2.Node() - parse_child_result = __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + parse_child_result = parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) if parse_child_result.is_err(): return parse_child_result node.children.append(child_node) + return Ok(None) def parseChildOccurrence( @@ -133,7 +134,7 @@ def parseChildOccurrence( node: types_pb2.Node, ) -> Result[None]: if occurrence.isLightBulbOn is False: - return + return Ok(None) progressDialog.addOccurrence(occurrence.name) @@ -193,11 +194,12 @@ def parseChildOccurrence( if occur.isLightBulbOn: child_node = types_pb2.Node() - parse_child_result = __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + parse_child_result = parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) if parse_child_result.is_err(): return parse_child_result node.children.append(child_node) + return Ok(None) # saw online someone used this to get the correct context but oh boy does it look pricey diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 3456683c7b..8d10d79a75 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -172,7 +172,9 @@ def MapAllAppearances( ) -> Result[None]: # in case there are no appearances on a body # this is just a color tho - setDefaultAppearance(materials.appearances["default"]) + set_default_result = setDefaultAppearance(materials.appearances["default"]) + if set_default_result.is_err() and set_default_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return set_default_result fill_info_result = fill_info(materials, None) if fill_info_result.is_err(): @@ -202,6 +204,7 @@ def setDefaultAppearance(appearance: material_pb2.Appearance) -> Result[None]: """ # add info + # TODO: Check if appearance actually can be passed in here in place of an assembly or smth construct_info_result = construct_info("default", appearance) if construct_info_result.is_err(): return construct_info_result From 4ca02937d366dffbf6be2944debb9d70de9778b7 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 15:26:16 -0700 Subject: [PATCH 07/68] feat: value-based error handling in JointHierarchy.py where possible --- .../Parser/SynthesisParser/JointHierarchy.py | 226 ++++++++++-------- 1 file changed, 129 insertions(+), 97 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 0b5be182c2..b0ff7e5bbe 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -1,11 +1,15 @@ import enum +from logging import ERROR from typing import Any, Iterator, cast +from google.protobuf.message import Error + import adsk.core import adsk.fusion from src import gm from src.Logging import getLogger, logFailure +from src.ErrorHandling import Result, Err, Ok, ErrorSeverity from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import guid_component, guid_occurrence @@ -99,7 +103,7 @@ class DynamicOccurrenceNode(GraphNode): def __init__(self, occurrence: adsk.fusion.Occurrence, isGround: bool = False, previous: GraphNode | None = None): super().__init__(occurrence) self.isGround = isGround - self.name = occurrence.name + self.name = occurrence.name # type: ignore def print(self) -> None: print(f"\n\t-------{self.data.name}-------") @@ -188,26 +192,27 @@ class SimulationEdge(GraphEdge): ... class JointParser: grounded: adsk.fusion.Occurrence + # NOTE This function cannot under the value-based error handling system, since it's an __init__ function @logFailure def __init__(self, design: adsk.fusion.Design) -> None: - # Create hierarchy with just joint assembly - # - Assembly - # - Grounded - # - Axis 1 - # - Axis 2 - # - Axis 3 - - # 1. Find all Dynamic joint items to isolate [o] - # 2. Find the grounded component [x] (possible - not optimized) - # 3. Populate tree with all items from each set of joints [x] (done with grounding) - # - 3. a) Each Child element with no joints [x] - # - 3. b) Each Rigid Joint Connection [x] - # 4. Link Joint trees by discovery from root [x] - # 5. Record which trees have no children for creating end effectors [x] (next up) - this kinda already exists - - # Need to investigate creating an additional button for end effector possibly - # It might be possible to have multiple end effectors - # Total Number of final elements + """ Create hierarchy with just joint assembly + - Assembly + - Grounded + - Axis 1 + - Axis 2 + - Axis 3 + + 1. Find all Dynamic joint items to isolate [o] + 2. Find the grounded component [x] (possible - not optimized) + 3. Populate tree with all items from each set of joints [x] (done with grounding) + - 3. a) Each Child element with no joints [x] + - 3. b) Each Rigid Joint Connection [x] + 4. Link Joint trees by discovery from root [x] + 5. Record which trees have no children for creating end effectors [x] (next up) - this kinda already exists + + Need to investigate creating an additional button for end effector possibly + It might be possible to have multiple end effectors + Total Number of final elements""" self.current = None self.previousJoint = None @@ -237,7 +242,11 @@ def __init__(self, design: adsk.fusion.Design) -> None: self.__getAllJoints() # dynamic joint node for grounded components and static components - rootNode = self._populateNode(self.grounded, None, None, is_ground=True) + populate_node_result = self._populateNode(self.grounded, None, None, is_ground=True) + if populate_node_result.is_err(): # We need the value to proceed + raise RuntimeWarning(populate_node_result.unwrap_err()[0]) + + rootNode = populate_node_result.unwrap() self.groundSimNode = SimulationNode(rootNode, None, grounded=True) self.simulationNodesRef["GROUND"] = self.groundSimNode @@ -253,30 +262,29 @@ def __init__(self, design: adsk.fusion.Design) -> None: # self.groundSimNode.printLink() - @logFailure - def __getAllJoints(self) -> None: + def __getAllJoints(self) -> Result[None]: for joint in list(self.design.rootComponent.allJoints) + list(self.design.rootComponent.allAsBuiltJoints): if joint and joint.occurrenceOne and joint.occurrenceTwo: occurrenceOne = joint.occurrenceOne occurrenceTwo = joint.occurrenceTwo else: - return + # Non-fatal since it's recovered in the next two statements + _ = Err("Found joint without two occurences", ErrorSeverity.Warning) if occurrenceOne is None: - try: - occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext - except: - pass + if joint.geometryOrOriginOne.entityOne.assemblyContext is None + return Err("occurrenceOne and entityOne's assembly context are None", ErrorSeverity.Fatal) + occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext if occurrenceTwo is None: - try: - occurrenceTwo = joint.geometryOrOriginTwo.entityOne.assemblyContext - except: - pass + if joint.geometryOrOriginTwo.entityTwo.assemblyContext is None + return Err("occurrenceOne and entityTwo's assembly context are None", ErrorSeverity.Fatal) + occurrenceTwo = joint.geometryOrOriginTwo.entityTwo.assemblyContext oneEntityToken = "" twoEntityToken = "" + # TODO: Fix change to if statement with Result returning try: oneEntityToken = occurrenceOne.entityToken except: @@ -293,124 +301,138 @@ def __getAllJoints(self) -> None: 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: - logger.error( - f"Occurrences that connect joints could not be found\n\t1: {occurrenceOne}\n\t2: {occurrenceTwo}" - ) - return + return 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) + return Ok(None) - def _linkAllAxis(self) -> None: + def _linkAllAxis(self) -> Result[None]: # looks through each simulation nood starting with ground and orders them using edges # self.groundSimNode is ground - self._recurseLink(self.groundSimNode) + return self._recurseLink(self.groundSimNode) - def _recurseLink(self, simNode: SimulationNode) -> None: + def _recurseLink(self, simNode: SimulationNode) -> Result[None]: connectedAxisNodes = [ self.simulationNodesRef.get(componentKeys, None) for componentKeys in simNode.data.getConnectedAxisTokens() ] + if any([node is None for node in connectedAxisNodes]): + return Err(f"Found None Connected Access Node", ErrorSeverity.Fatal) + for connectedAxis in connectedAxisNodes: # connected is the occurrence if connectedAxis is not None: edge = SimulationEdge(JointRelationship.GROUND, connectedAxis) simNode.edges.append(edge) - self._recurseLink(connectedAxis) - def _lookForGroundedJoints(self) -> None: - grounded_token = self.grounded.entityToken + recurse_result = self._recurseLink(connectedAxis) + if recurse_result.is_err() and recurse_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return recurse_result + return Ok(None) + + def _lookForGroundedJoints(self) -> Result[None]: + # grounded_token = self.grounded.entityToken rootDynamicJoint = self.groundSimNode.data + if rootDynamicJoint is None: + return Err("Found None rootDynamicJoint", ErrorSeverity.Fatal) for grounded_connect in self.groundedConnections: self.currentTraversal = dict() - self._populateNode( + _ = self._populateNode( grounded_connect, rootDynamicJoint, OccurrenceRelationship.CONNECTION, is_ground=False, ) + return Ok(None) def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint) -> None: occ = self.design.findEntityByToken(occ_token)[0] - if occ is None: return self.currentTraversal = dict() - rootNode = self._populateNode(occ, None, None) + populate_node_result = self._populateNode(occ, None, None) + if populate_node_result.is_err(): # We need the value to proceed + return populate_node_result + rootNode = populate_node_result.unwrap() if rootNode is not None: axisNode = SimulationNode(rootNode, joint) self.simulationNodesRef[occ_token] = axisNode + # TODO: Verify that this works after the Result-refactor :skull: def _populateNode( self, occ: adsk.fusion.Occurrence, prev: DynamicOccurrenceNode | None, relationship: OccurrenceRelationship | None, is_ground: bool = False, - ) -> DynamicOccurrenceNode | None: + ) -> Result[DynamicOccurrenceNode | None]: if occ.isGrounded and not is_ground: - return None + return Ok(None) elif (relationship == OccurrenceRelationship.NEXT) and (prev is not None): node = DynamicOccurrenceNode(occ) edge = DynamicEdge(relationship, node) prev.edges.append(edge) - return None + return Ok(None) elif ((occ.entityToken in self.dynamicJoints.keys()) and (prev is not None)) or self.currentTraversal.get( occ.entityToken ) is not None: - return None + return Ok(None) node = DynamicOccurrenceNode(occ) self.currentTraversal[occ.entityToken] = True for occurrence in occ.childOccurrences: - self._populateNode(occurrence, node, OccurrenceRelationship.TRANSFORM, is_ground=is_ground) + populate_result = self._populateNode(occurrence, node, OccurrenceRelationship.TRANSFORM, is_ground=is_ground) + if populate_result.is_err() and populate_result.unwrap_err()[1] == ErrorSeverity.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 - try: - 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: - self._populateNode( - connection, - node, - (OccurrenceRelationship.CONNECTION if rigid else OccurrenceRelationship.NEXT), - is_ground=is_ground, - ) + 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: - continue - except: - pass - + 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_err() and populate_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return populate_result + 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 prev is not None: edge = DynamicEdge(relationship, node) prev.edges.append(edge) self.currentTraversal[occ.entityToken] = node - return node + return Ok(node) def searchForGrounded( @@ -422,14 +444,13 @@ def searchForGrounded( occ (adsk.fusion.Occurrence): start point Returns: - Union(adsk.fusion.Occurrence, None): Either a grounded part or nothing + adsk.fusion.Occurrence | None: Either a grounded part or nothing """ if occ.objectType == "adsk::fusion::Component": # this makes it possible to search an object twice (unoptimized) collection = occ.allOccurrences # components cannot be grounded technically - else: # Object is an occurrence if occ.isGrounded: return occ @@ -448,13 +469,13 @@ def searchForGrounded( # ________________________ Build implementation ______________________ # -@logFailure def BuildJointPartHierarchy( design: adsk.fusion.Design, joints: joint_pb2.Joints, options: ExporterOptions, progressDialog: PDMessage, -) -> None: +) -> Result[None]: + # This try-catch is necessary because the JointParser __init__ functon is fallible and throws a RuntimeWarning (__init__ functions cannot return values) try: progressDialog.currentMessage = f"Constructing Simulation Hierarchy" progressDialog.update() @@ -462,7 +483,9 @@ def BuildJointPartHierarchy( jointParser = JointParser(design) rootSimNode = jointParser.groundSimNode - populateJoint(rootSimNode, joints, progressDialog) + populate_joint_result = populateJoint(rootSimNode, joints, progressDialog) + if populate_joint_result.is_err() and populate_joint_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return populate_joint_result # 1. Get Node # 2. Get Transform of current Node @@ -477,11 +500,14 @@ def BuildJointPartHierarchy( if progressDialog.wasCancelled(): raise RuntimeError("User canceled export") + return Ok(None) + + # I'm fairly certain bubbling this back up is the way to go except Warning: - pass + return Err("Instantiation of the JointParser failed, likely due to a lack of a grounded component in the assembly", ErrorSeverity.Fatal) -def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDialog: PDMessage) -> None: +def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDialog: PDMessage) -> Result[None]: if progressDialog.wasCancelled(): raise RuntimeError("User canceled export") @@ -494,8 +520,7 @@ def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDia progressDialog.update() if not proto_joint: - logger.error(f"Could not find protobuf joint for {simNode.name}") - return + return Err(f"Could not find protobuf joint for {simNode.name}", ErrorSeverity.Fatal) root = types_pb2.Node() @@ -506,7 +531,10 @@ def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDia # next in line to be populated for edge in simNode.edges: - populateJoint(cast(SimulationNode, edge.node), joints, progressDialog) + populate_joint_result = populateJoint(cast(SimulationNode, edge.node), joints, progressDialog) + if populate_joint_result.is_err() and populate_joint_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return populate_joint_result + return Ok(None) def createTreeParts( @@ -519,26 +547,30 @@ def createTreeParts( raise RuntimeError("User canceled export") # if it's the next part just exit early for our own sanity + # This shouldn't be fatal nor even an error if relationship == OccurrenceRelationship.NEXT or dynNode.data.isLightBulbOn == False: return # set the occurrence / component id to reference the part - try: - objectType = dynNode.data.objectType - except: + # Fine way to use try-excepts in this language + if dynNode.data.objectType is None: + _ = Err("Found None object type", ErrorSeverity.Warning) objectType = "" - + else: + objectType = dynNode.data.objectType + if objectType == "adsk::fusion::Occurrence": node.value = guid_occurrence(dynNode.data) elif objectType == "adsk::fusion::Component": node.value = guid_component(dynNode.data) else: - try: - node.value = dynNode.data.entityToken - except RuntimeError: + if dynNode.data.entityToken is None: + _ = Err("Found None EntityToken", ErrorSeverity.Warning) # type: ignore node.value = dynNode.data.name - + else: + node.value = dynNode.data.entityToken + # possibly add additional information for the type of connection made # recurse and add all children connections for edge in dynNode.edges: From 968c3d262fdb22ba620d06da64ff451cc352b787 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 16:19:02 -0700 Subject: [PATCH 08/68] feat: add value-based error handling to joints.py --- .../src/Parser/SynthesisParser/Joints.py | 78 ++++++++++++++----- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index 0577da7a41..d3f926de0e 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -29,6 +29,7 @@ import adsk.core import adsk.fusion +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import getLogger from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage @@ -76,20 +77,28 @@ def populateJoints( progressDialog: PDMessage, options: ExporterOptions, assembly: assembly_pb2.Assembly, -) -> None: - fill_info(joints, None) +) -> Result[None]: + info_result = fill_info(joints, None) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result # This is for creating all of the Joint Definition objects # So we need to iterate through the joints and construct them and add them to the map if not options.joints: - return + return Ok(None) # Add the grounded joints object - TODO: rename some of the protobuf stuff for the love of god joint_definition_ground = joints.joint_definitions["grounded"] - construct_info("grounded", joint_definition_ground) + info_result = construct_info("grounded", joint_definition_ground) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + joint_instance_ground = joints.joint_instances["grounded"] - construct_info("grounded", joint_instance_ground) + info_result = construct_info("grounded", joint_instance_ground) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + joint_instance_ground.joint_reference = joint_definition_ground.info.GUID @@ -106,7 +115,8 @@ def populateJoints( if joint.jointMotion.jointType in AcceptedJointTypes: try: - # Fusion has no instances of joints but lets roll with it anyway + # Fusion has no instances of joints but lets roll with it anyway + # ^^^ This majorly confuses me ^^^ # progressDialog.message = f"Exporting Joint configuration {joint.name}" progressDialog.addJoint(joint.name) @@ -122,7 +132,11 @@ def populateJoints( if parse_joints.jointToken == joint.entityToken: guid = str(uuid.uuid4()) signal = signals.signal_map[guid] - construct_info(joint.name, signal, GUID=guid) + + info_result = construct_info(joint.name, signal, GUID=guid) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + signal.io = signal_pb2.IOType.OUTPUT # really could just map the enum to a friggin string @@ -133,7 +147,12 @@ def populateJoints( signal.device_type = signal_pb2.DeviceType.PWM motor = joints.motor_definitions[joint.entityToken] - fill_info(motor, joint) + + info_result = fill_info(motor, joint) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + + simple_motor = motor.simple_motor simple_motor.stall_torque = parse_joints.force simple_motor.max_velocity = parse_joints.speed @@ -144,19 +163,24 @@ def populateJoints( # else: # signals.signal_map.remove(guid) - _addJointInstance(joint, joint_instance, joint_definition, signals, options) + joint_result = _addJointInstance(joint, joint_instance, joint_definition, signals, options) + if joint_result.is_err() and joint_result.severity == ErrorSeverity.Fatal: + return joint_result # adds information for joint motion and limits _motionFromJoint(joint.jointMotion, joint_definition) except: - logger.error("Failed:\n{}".format(traceback.format_exc())) + # TODO: Figure out how to construct and return this (ie, what actually breaks in this try block) + _ = Err("Failed:\n{}".format(traceback.format_exc()), ErrorSeverity.Fatal) continue + return Ok(None) -def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> None: - fill_info(joint_definition, joint) - +def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> Result[None]: + info_result = fill_info(joint_definition, joint) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result jointPivotTranslation = _jointOrigin(joint) if jointPivotTranslation: @@ -168,10 +192,13 @@ def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> No joint_definition.origin.y = 0.0 joint_definition.origin.z = 0.0 - logger.error(f"Cannot find joint origin on joint {joint.name}") + # TODO: We definitely could make this fatal, figure out if we should + _ = Err(f"Cannot find joint origin on joint {joint.name}", ErrorSeverity.Warning) joint_definition.break_magnitude = 0.0 + return Ok(None) + def _addJointInstance( joint: adsk.fusion.Joint, @@ -179,8 +206,11 @@ def _addJointInstance( joint_definition: joint_pb2.Joint, signals: signal_pb2.Signals, options: ExporterOptions, -) -> None: - fill_info(joint_instance, joint) +) -> Result[None]: + info_result = fill_info(joint_instance, joint) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + # because there is only one and we are using the token - should be the same joint_instance.joint_reference = joint_instance.info.GUID @@ -221,7 +251,11 @@ def _addJointInstance( else: # if not then create it and add the signal type guid = str(uuid.uuid4()) signal = signals.signal_map[guid] - construct_info("joint_signal", signal, GUID=guid) + + info_result = construct_info("joint_signal", signal, GUID=guid) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + signal.io = signal_pb2.IOType.OUTPUT joint_instance.signal_reference = signal.info.GUID @@ -232,7 +266,7 @@ def _addJointInstance( signal.device_type = signal_pb2.DeviceType.PWM else: joint_instance.signal_reference = "" - + return Ok(None) def _addRigidGroup(joint: adsk.fusion.Joint, assembly: assembly_pb2.Assembly) -> None: if joint.jointMotion.jointType != 0 or not ( @@ -422,7 +456,7 @@ def notImplementedPlaceholder(*argv: Any) -> None: ... def _searchForGrounded( occ: adsk.fusion.Occurrence, -) -> Union[adsk.fusion.Occurrence, None]: +) -> adsk.fusion.Occurrence | None: """Search for a grounded component or occurrence in the assembly Args: @@ -512,7 +546,7 @@ def createJointGraph( _wheels: list[Wheel], jointTree: types_pb2.GraphContainer, progressDialog: PDMessage, -) -> None: +) -> Result[None]: # progressDialog.message = f"Building Joint Graph Map from given joints" progressDialog.currentMessage = f"Building Joint Graph Map from given joints" @@ -542,11 +576,13 @@ def createJointGraph( elif nodeMap[suppliedJoint.parent.value] is not None and nodeMap[suppliedJoint.jointToken] is not None: nodeMap[str(suppliedJoint.parent)].children.append(nodeMap[suppliedJoint.jointToken]) else: - logger.error(f"Cannot construct hierarhcy because of detached tree at : {suppliedJoint.jointToken}") + # TODO: This might not need to be fatal + return Err(f"Cannot construct hierarchy because of detached tree at : {suppliedJoint.jointToken}", ErrorSeverity.Fatal) for node in nodeMap.values(): # append everything at top level to isolate kinematics jointTree.nodes.append(node) + return Ok(None) def addWheelsToGraph(wheels: list[Wheel], rootNode: types_pb2.Node, jointTree: types_pb2.GraphContainer) -> None: From 7ec8865a82023904e00d5377e1e04db2f2c1f017 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 27 Jun 2025 09:04:09 -0700 Subject: [PATCH 09/68] fix: finish wrapping joint function calls in err handling --- .../src/Parser/SynthesisParser/Parser.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 968b0d8738..35c261c6fb 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -118,27 +118,27 @@ def export(self) -> None: assembly_out.design_hierarchy.nodes.append(rootNode) # Problem Child - Joints.populateJoints( + handle_err_top(Joints.populateJoints( design, assembly_out.data.joints, assembly_out.data.signals, self.pdMessage, self.exporterOptions, assembly_out, - ) + )) # add condition in here for advanced joints maybe idk # should pre-process to find if there are any grounded joints at all # that or add code to existing parser to determine leftovers - Joints.createJointGraph( + handle_err_top(Joints.createJointGraph( self.exporterOptions.joints, self.exporterOptions.wheels, assembly_out.joint_hierarchy, self.pdMessage, - ) + )) - JointHierarchy.BuildJointPartHierarchy(design, assembly_out.data.joints, self.exporterOptions, self.pdMessage) + handle_err_top(JointHierarchy.BuildJointPartHierarchy(design, assembly_out.data.joints, self.exporterOptions, self.pdMessage)) # These don't have an effect, I forgot how this is suppose to work # progressDialog.message = "Taking Photo for thumbnail..." @@ -198,10 +198,10 @@ def export(self) -> None: if self.exporterOptions.compressOutput: logger.debug("Compressing file") with gzip.open(str(self.exporterOptions.fileLocation), "wb", 9) as f: - f.write(assembly_out.SerializeToString()) + _ = f.write(assembly_out.SerializeToString()) else: with open(str(self.exporterOptions.fileLocation), "wb") as f: - f.write(assembly_out.SerializeToString()) + _ = f.write(assembly_out.SerializeToString()) _ = progressDialog.hide() From 37fddb643f9708b197e5abc87e44362feee02dba Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 27 Jun 2025 14:47:49 -0700 Subject: [PATCH 10/68] fix: logging bug --- exporter/SynthesisFusionAddin/Synthesis.py | 14 ++++---------- exporter/SynthesisFusionAddin/logs/.gitkeep | 0 .../SynthesisFusionAddin/src/ErrorHandling.py | 9 +++++---- .../src/Parser/SynthesisParser/JointHierarchy.py | 4 ++-- .../src/Parser/SynthesisParser/Materials.py | 4 ++-- .../src/Parser/SynthesisParser/Parser.py | 15 +++------------ .../SynthesisFusionAddin/src/UI/ConfigCommand.py | 3 +-- exporter/SynthesisFusionAddin/src/__init__.py | 1 + 8 files changed, 18 insertions(+), 32 deletions(-) delete mode 100644 exporter/SynthesisFusionAddin/logs/.gitkeep diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 109460f808..984360ee26 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -9,8 +9,8 @@ from src.Dependencies import resolveDependencies from src.Logging import logFailure, setupLogger - logger = setupLogger() +from src.ErrorHandling import Err, ErrorSeverity try: # Attempt to import required pip dependencies to verify their installation. @@ -32,17 +32,9 @@ from src import APP_NAME, DESCRIPTION, INTERNAL_ID, gm -from src.UI import ( - HUI, - Camera, - ConfigCommand, - MarkingMenu, - ShowAPSAuthCommand, - ShowWebsiteCommand, -) +from src.UI import (HUI, Camera, ConfigCommand, MarkingMenu, ShowAPSAuthCommand, ShowWebsiteCommand) from src.UI.Toolbar import Toolbar - @logFailure def run(_context: dict[str, Any]) -> None: """## Entry point to application from Fusion. @@ -51,6 +43,7 @@ def run(_context: dict[str, Any]) -> None: **context** *context* -- Fusion context to derive app and UI. """ + # Remove all items prior to start just to make sure unregister_all() @@ -70,6 +63,7 @@ def stop(_context: dict[str, Any]) -> None: Arguments: **context** *context* -- Fusion Data. """ + unregister_all() app = adsk.core.Application.get() diff --git a/exporter/SynthesisFusionAddin/logs/.gitkeep b/exporter/SynthesisFusionAddin/logs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 2d1594f879..b43d9aa56f 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -2,12 +2,14 @@ from enum import Enum from typing import Generic, TypeVar +logger = getLogger() + # NOTE # Severity refers to to the error's affect on the parser as a whole, rather than on the function itself # If an error is non-fatal to the function that generated it, it should be declared but not return, which prints it to the screen class ErrorSeverity(Enum): - Fatal = 1 - Warning = 2 + Fatal = 50 # Critical Error + Warning = 30 # Warning T = TypeVar('T') @@ -49,6 +51,5 @@ def __repr__(self): return f"Err({self.message})" def write_error(self) -> None: - logger = getLogger() # Figure out how to integrate severity with the logger - logger.log(1, self.message) + logger.log(self.severity.value, self.message) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index b0ff7e5bbe..8c73c07815 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -272,12 +272,12 @@ def __getAllJoints(self) -> Result[None]: _ = Err("Found joint without two occurences", ErrorSeverity.Warning) if occurrenceOne is None: - if joint.geometryOrOriginOne.entityOne.assemblyContext is None + if joint.geometryOrOriginOne.entityOne.assemblyContext is None: return Err("occurrenceOne and entityOne's assembly context are None", ErrorSeverity.Fatal) occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext if occurrenceTwo is None: - if joint.geometryOrOriginTwo.entityTwo.assemblyContext is None + if joint.geometryOrOriginTwo.entityTwo.assemblyContext is None: return Err("occurrenceOne and entityTwo's assembly context are None", ErrorSeverity.Fatal) occurrenceTwo = joint.geometryOrOriginTwo.entityTwo.assemblyContext diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 8d10d79a75..e579147756 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -143,7 +143,7 @@ def getPhysicalMaterialData( missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] #ignore: type if missingProperties.__len__() > 0: - return Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) + _ = Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) """ Strength Properties @@ -153,7 +153,7 @@ def getPhysicalMaterialData( missingProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] #ignore: type if missingProperties.__len__() > 0: - return Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) + _ = Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) """ strengthProperties.thermal_treatment = materialProperties.itemById( diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 35c261c6fb..cd11e90099 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -1,24 +1,16 @@ import gzip import pathlib -from google.protobuf import message - import adsk.core import adsk.fusion -from google.protobuf.json_format import MessageToJson from src import gm from src.APS.APS import getAuth, upload_mirabuf from src.ErrorHandling import ErrorSeverity, Result -from src.Logging import getLogger, logFailure, timed from src.Parser.ExporterOptions import ExporterOptions -from src.Parser.SynthesisParser import ( - Components, - JointHierarchy, - Joints, - Materials, - PDMessage, -) +from src.Parser.SynthesisParser import (Components, JointHierarchy, Joints, Materials, PDMessage) + +from src.Logging import getLogger, logFailure, timed from src.Parser.SynthesisParser.Utilities import fill_info from src.Proto import assembly_pb2, types_pb2 from src.Types import ExportLocation, ExportMode @@ -26,7 +18,6 @@ logger = getLogger() - class Parser: def __init__(self, options: ExporterOptions): """Creates a new parser with the supplied options diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 75ac2364e2..084ccd5141 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -10,9 +10,9 @@ import adsk.core import adsk.fusion +from src.Logging import logFailure from src import APP_WEBSITE_URL, gm from src.APS.APS import getAuth, getUserInfo -from src.Logging import getLogger, logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.Parser import Parser from src.Types import SELECTABLE_JOINT_TYPES, ExportLocation, ExportMode @@ -26,7 +26,6 @@ jointConfigTab: JointConfigTab gamepieceConfigTab: GamepieceConfigTab -logger = getLogger() INPUTS_ROOT: adsk.core.CommandInputs diff --git a/exporter/SynthesisFusionAddin/src/__init__.py b/exporter/SynthesisFusionAddin/src/__init__.py index 42d1e6d391..3f761b65c0 100644 --- a/exporter/SynthesisFusionAddin/src/__init__.py +++ b/exporter/SynthesisFusionAddin/src/__init__.py @@ -5,6 +5,7 @@ from src.GlobalManager import GlobalManager from src.Util import makeDirectories + APP_NAME = "Synthesis" APP_TITLE = "Synthesis Robot Exporter" APP_WEBSITE_URL = "https://synthesis.autodesk.com/fission/" From 279cc5dc1ce8eb5fcba79612ca26163e8c2f2f9b Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 27 Jun 2025 15:17:10 -0700 Subject: [PATCH 11/68] feat: refactor physical properties err handling --- .../SynthesisParser/PhysicalProperties.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 9c52670d21..380995cbbe 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -16,29 +16,33 @@ """ -from typing import Union - import adsk +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import logFailure from src.Proto import types_pb2 -@logFailure def GetPhysicalProperties( - fusionObject: Union[adsk.fusion.BRepBody, adsk.fusion.Occurrence, adsk.fusion.Component], + fusionObject: adsk.fusion.BRepBody | adsk.fusion.Occurrence | adsk.fusion.Component, physicalProperties: types_pb2.PhysicalProperties, level: int = 1, -) -> None: +) -> Result[None]: """Will populate a physical properties section of an exported file Args: - fusionObject (Union[adsk.fusion.BRepBody, adsk.fusion.Occurrence, adsk.fusion.Component]): The base fusion object + fusionObject (adsk.fusion.BRepBody | adsk.fusion.Occurrence, adsk.fusion.Component): The base fusion object physicalProperties (any): Unity Joint object for now level (int): Level of accurracy """ physical = fusionObject.getPhysicalProperties(level) + missing_properties = [prop is None for prop in physical] + if physical is None: + return Err("Physical properties object is None", ErrorSeverity.Warning) + if any(missing_properties.): + _ = Err(f"Missing some physical properties", ErrorSeverity.Warning) + physicalProperties.density = physical.density physicalProperties.mass = physical.mass physicalProperties.volume = physical.volume @@ -51,3 +55,7 @@ def GetPhysicalProperties( _com.x = com.x _com.y = com.y _com.z = com.z + else: + _ = Err("com is None", ErrorSeverity.Warning) + + return Ok(None) From 473f6f3b0df388d5bff61312e09d72b3deb421aa Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 27 Jun 2025 15:19:12 -0700 Subject: [PATCH 12/68] chore: change union to | --- .../src/Parser/SynthesisParser/RigidGroup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py index 828f29626a..f49992affc 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py @@ -12,8 +12,6 @@ - Success """ -from typing import Union - import adsk.core import adsk.fusion @@ -26,7 +24,7 @@ # Should be removed later @logFailure def ExportRigidGroups( - fus_occ: Union[adsk.fusion.Occurrence, adsk.fusion.Component], + fus_occ: adsk.fusion.Occurrence | adsk.fusion.Component, hel_occ: assembly_pb2.Occurrence, # type: ignore[name-defined] ) -> None: """Takes a Fusion and Protobuf Occurrence and will assign Rigidbody data per the occurrence if any exist and are not surpressed. From 44846f41e97ae2dab47ae474c48116478da89a6c Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 08:26:02 -0700 Subject: [PATCH 13/68] chore: format files --- exporter/SynthesisFusionAddin/Synthesis.py | 5 +- .../SynthesisFusionAddin/src/ErrorHandling.py | 21 ++- .../src/Parser/SynthesisParser/Components.py | 20 +-- .../Parser/SynthesisParser/JointHierarchy.py | 64 ++++++---- .../src/Parser/SynthesisParser/Joints.py | 9 +- .../src/Parser/SynthesisParser/Materials.py | 8 +- .../src/Parser/SynthesisParser/Parser.py | 120 ++++++++++-------- .../SynthesisParser/PhysicalProperties.py | 9 +- .../src/Parser/SynthesisParser/Utilities.py | 9 +- 9 files changed, 153 insertions(+), 112 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 984360ee26..e8940e669a 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -9,6 +9,7 @@ from src.Dependencies import resolveDependencies from src.Logging import logFailure, setupLogger + logger = setupLogger() from src.ErrorHandling import Err, ErrorSeverity @@ -32,9 +33,10 @@ from src import APP_NAME, DESCRIPTION, INTERNAL_ID, gm -from src.UI import (HUI, Camera, ConfigCommand, MarkingMenu, ShowAPSAuthCommand, ShowWebsiteCommand) +from src.UI import HUI, Camera, ConfigCommand, MarkingMenu, ShowAPSAuthCommand, ShowWebsiteCommand from src.UI.Toolbar import Toolbar + @logFailure def run(_context: dict[str, Any]) -> None: """## Entry point to application from Fusion. @@ -43,7 +45,6 @@ def run(_context: dict[str, Any]) -> None: **context** *context* -- Fusion context to derive app and UI. """ - # Remove all items prior to start just to make sure unregister_all() diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index b43d9aa56f..c8059c4a36 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -4,14 +4,17 @@ logger = getLogger() + # NOTE # Severity refers to to the error's affect on the parser as a whole, rather than on the function itself # If an error is non-fatal to the function that generated it, it should be declared but not return, which prints it to the screen class ErrorSeverity(Enum): - Fatal = 50 # Critical Error - Warning = 30 # Warning + Fatal = 50 # Critical Error + Warning = 30 # Warning + + +T = TypeVar("T") -T = TypeVar('T') class Result(Generic[T]): def is_ok(self) -> bool: @@ -22,25 +25,29 @@ def is_err(self) -> bool: def unwrap(self) -> T: if self.is_ok(): - return self.value # type: ignore - raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore + return self.value # type: ignore + raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore def unwrap_err(self) -> tuple[str, ErrorSeverity]: if self.is_err(): - return (self.message, self.severity) # type: ignore - raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore + return (self.message, self.severity) # type: ignore + raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore + class Ok(Result[T]): value: T + def __init__(self, value: T): self.value = value def __repr__(self): return f"Ok({self.value})" + class Err(Result[T]): message: str severity: ErrorSeverity + def __init__(self, message: str, severity: ErrorSeverity): self.message = message self.severity = severity diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index fd4add0bb2..80bbd123c7 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -16,6 +16,7 @@ from src.Proto import assembly_pb2, joint_pb2, material_pb2, types_pb2 from src.Types import ExportMode + # TODO: Impelement Material overrides def MapAllComponents( design: adsk.fusion.Design, @@ -36,14 +37,12 @@ def MapAllComponents( if fill_info_result.is_err(): return fill_info_result - partDefinition = partsData.part_definitions[comp_ref] fill_info_result = fill_info(partDefinition, component, comp_ref) if fill_info_result.is_err(): return fill_info_result - PhysicalProperties.GetPhysicalProperties(component, partDefinition.physical_data) partDefinition.dynamic = options.exportMode != ExportMode.FIELD @@ -68,7 +67,6 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: return parse_result - appearance_key = "{}_{}".format(body.appearance.name, body.appearance.id) # this should be appearance if appearance_key in materials.appearances: @@ -86,7 +84,6 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non return process_result - def ParseComponentRoot( component: adsk.fusion.Component, progressDialog: PDMessage, @@ -117,7 +114,9 @@ def ParseComponentRoot( if occur.isLightBulbOn: child_node = types_pb2.Node() - parse_child_result = parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + parse_child_result = parseChildOccurrence( + occur, progressDialog, options, partsData, material_map, child_node + ) if parse_child_result.is_err(): return parse_child_result @@ -158,7 +157,8 @@ def parseChildOccurrence( try: part.appearance = "{}_{}".format(occurrence.appearance.name, occurrence.appearance.id) except: - _ = Err("Failed to format part appearance", ErrorSeverity.Warning); # ignore: type + _ = Err("Failed to format part appearance", ErrorSeverity.Warning) + # ignore: type part.appearance = "default" # TODO: Add phyical_material parser @@ -194,8 +194,10 @@ def parseChildOccurrence( if occur.isLightBulbOn: child_node = types_pb2.Node() - parse_child_result = parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) - if parse_child_result.is_err(): + parse_child_result = parseChildOccurrence( + occur, progressDialog, options, partsData, material_map, child_node + ) + if parse_child_result.is_err(): return parse_child_result node.children.append(child_node) @@ -252,12 +254,10 @@ def ParseMesh( if mesh is None: return Err("Component Mesh was None", ErrorSeverity.Fatal) - fill_info_result = fill_info(trimesh, meshBody) if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return fill_info_result - trimesh.has_volume = True plainmesh_out = trimesh.mesh diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 8c73c07815..19adac21e0 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -103,7 +103,7 @@ class DynamicOccurrenceNode(GraphNode): def __init__(self, occurrence: adsk.fusion.Occurrence, isGround: bool = False, previous: GraphNode | None = None): super().__init__(occurrence) self.isGround = isGround - self.name = occurrence.name # type: ignore + self.name = occurrence.name # type: ignore def print(self) -> None: print(f"\n\t-------{self.data.name}-------") @@ -195,24 +195,24 @@ class JointParser: # NOTE This function cannot under the value-based error handling system, since it's an __init__ function @logFailure def __init__(self, design: adsk.fusion.Design) -> None: - """ Create hierarchy with just joint assembly - - Assembly - - Grounded - - Axis 1 - - Axis 2 - - Axis 3 - - 1. Find all Dynamic joint items to isolate [o] - 2. Find the grounded component [x] (possible - not optimized) - 3. Populate tree with all items from each set of joints [x] (done with grounding) - - 3. a) Each Child element with no joints [x] - - 3. b) Each Rigid Joint Connection [x] - 4. Link Joint trees by discovery from root [x] - 5. Record which trees have no children for creating end effectors [x] (next up) - this kinda already exists - - Need to investigate creating an additional button for end effector possibly - It might be possible to have multiple end effectors - Total Number of final elements""" + """Create hierarchy with just joint assembly + - Assembly + - Grounded + - Axis 1 + - Axis 2 + - Axis 3 + + 1. Find all Dynamic joint items to isolate [o] + 2. Find the grounded component [x] (possible - not optimized) + 3. Populate tree with all items from each set of joints [x] (done with grounding) + - 3. a) Each Child element with no joints [x] + - 3. b) Each Rigid Joint Connection [x] + 4. Link Joint trees by discovery from root [x] + 5. Record which trees have no children for creating end effectors [x] (next up) - this kinda already exists + + Need to investigate creating an additional button for end effector possibly + It might be possible to have multiple end effectors + Total Number of final elements""" self.current = None self.previousJoint = None @@ -243,7 +243,7 @@ def __init__(self, design: adsk.fusion.Design) -> None: # dynamic joint node for grounded components and static components populate_node_result = self._populateNode(self.grounded, None, None, is_ground=True) - if populate_node_result.is_err(): # We need the value to proceed + if populate_node_result.is_err(): # We need the value to proceed raise RuntimeWarning(populate_node_result.unwrap_err()[0]) rootNode = populate_node_result.unwrap() @@ -303,7 +303,10 @@ def __getAllJoints(self) -> Result[None]: # TODO: Check if this is fatal or not if occurrenceTwo is None and occurrenceOne is None: - return Err(f"Occurrences that connect joints could not be found\n\t1: {occurrenceOne}\n\t2: {occurrenceTwo}", ErrorSeverity.Fatal) + return 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) @@ -358,7 +361,7 @@ def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint) -> None: self.currentTraversal = dict() populate_node_result = self._populateNode(occ, None, None) - if populate_node_result.is_err(): # We need the value to proceed + if populate_node_result.is_err(): # We need the value to proceed return populate_node_result rootNode = populate_node_result.unwrap() @@ -391,7 +394,9 @@ def _populateNode( self.currentTraversal[occ.entityToken] = True for occurrence in occ.childOccurrences: - populate_result = self._populateNode(occurrence, node, OccurrenceRelationship.TRANSFORM, is_ground=is_ground) + populate_result = self._populateNode( + occurrence, node, OccurrenceRelationship.TRANSFORM, is_ground=is_ground + ) if populate_result.is_err() and populate_result.unwrap_err()[1] == ErrorSeverity.Fatal: return populate_result @@ -426,7 +431,7 @@ def _populateNode( 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 prev is not None: edge = DynamicEdge(relationship, node) prev.edges.append(edge) @@ -504,7 +509,10 @@ def BuildJointPartHierarchy( # I'm fairly certain bubbling this back up is the way to go except Warning: - return Err("Instantiation of the JointParser failed, likely due to a lack of a grounded component in the assembly", ErrorSeverity.Fatal) + return Err( + "Instantiation of the JointParser failed, likely due to a lack of a grounded component in the assembly", + ErrorSeverity.Fatal, + ) def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDialog: PDMessage) -> Result[None]: @@ -559,18 +567,18 @@ def createTreeParts( objectType = "" else: objectType = dynNode.data.objectType - + if objectType == "adsk::fusion::Occurrence": node.value = guid_occurrence(dynNode.data) elif objectType == "adsk::fusion::Component": node.value = guid_component(dynNode.data) else: if dynNode.data.entityToken is None: - _ = Err("Found None EntityToken", ErrorSeverity.Warning) # type: ignore + _ = Err("Found None EntityToken", ErrorSeverity.Warning) # type: ignore node.value = dynNode.data.name else: node.value = dynNode.data.entityToken - + # possibly add additional information for the type of connection made # recurse and add all children connections for edge in dynNode.edges: diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index d3f926de0e..d2f1469e41 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -93,13 +93,11 @@ def populateJoints( if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: return info_result - joint_instance_ground = joints.joint_instances["grounded"] info_result = construct_info("grounded", joint_instance_ground) if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: return info_result - joint_instance_ground.joint_reference = joint_definition_ground.info.GUID # Add the rest of the dynamic objects @@ -152,7 +150,6 @@ def populateJoints( if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: return info_result - simple_motor = motor.simple_motor simple_motor.stall_torque = parse_joints.force simple_motor.max_velocity = parse_joints.speed @@ -268,6 +265,7 @@ def _addJointInstance( joint_instance.signal_reference = "" return Ok(None) + 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 @@ -577,7 +575,10 @@ def createJointGraph( nodeMap[str(suppliedJoint.parent)].children.append(nodeMap[suppliedJoint.jointToken]) else: # TODO: This might not need to be fatal - return Err(f"Cannot construct hierarchy because of detached tree at : {suppliedJoint.jointToken}", ErrorSeverity.Fatal) + return Err( + f"Cannot construct hierarchy because of detached tree at : {suppliedJoint.jointToken}", + ErrorSeverity.Fatal, + ) for node in nodeMap.values(): # append everything at top level to isolate kinematics diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index e579147756..ba626307fc 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -52,8 +52,6 @@ def MapAllPhysicalMaterials( return Ok(None) - - def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions) -> Result[None]: construct_info_result = construct_info("default", physicalMaterial) if construct_info_result.is_err(): @@ -73,6 +71,7 @@ def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: return Ok(None) + def getPhysicalMaterialData( fusionMaterial: adsk.core.Material, physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions ) -> Result[None]: @@ -141,7 +140,7 @@ def getPhysicalMaterialData( mechanicalProperties.density = materialProperties.itemById("structural_Density").value mechanicalProperties.damping_coefficient = materialProperties.itemById("structural_Damping_coefficient").value - missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] #ignore: type + missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] # ignore: type if missingProperties.__len__() > 0: _ = Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) @@ -151,7 +150,7 @@ def getPhysicalMaterialData( strengthProperties.yield_strength = materialProperties.itemById("structural_Minimum_yield_stress").value strengthProperties.tensile_strength = materialProperties.itemById("structural_Minimum_tensile_strength").value - missingProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] #ignore: type + missingProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] # ignore: type if missingProperties.__len__() > 0: _ = Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) @@ -221,6 +220,7 @@ def setDefaultAppearance(appearance: material_pb2.Appearance) -> Result[None]: return Ok(None) + def getMaterialAppearance( fusionAppearance: adsk.core.Appearance, options: ExporterOptions, diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index cd11e90099..de2e358cc5 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -8,7 +8,7 @@ from src.APS.APS import getAuth, upload_mirabuf from src.ErrorHandling import ErrorSeverity, Result from src.Parser.ExporterOptions import ExporterOptions -from src.Parser.SynthesisParser import (Components, JointHierarchy, Joints, Materials, PDMessage) +from src.Parser.SynthesisParser import Components, JointHierarchy, Joints, Materials, PDMessage from src.Logging import getLogger, logFailure, timed from src.Parser.SynthesisParser.Utilities import fill_info @@ -18,6 +18,7 @@ logger = getLogger() + class Parser: def __init__(self, options: ExporterOptions): """Creates a new parser with the supplied options @@ -38,11 +39,13 @@ def export(self) -> None: return assembly_out = assembly_pb2.Assembly() - handle_err_top(fill_info( - assembly_out, - design.rootComponent, - override_guid=design.parentDocument.name, - )) + handle_err_top( + fill_info( + assembly_out, + design.rootComponent, + override_guid=design.parentDocument.name, + ) + ) # set int to 0 in dropdown selection for dynamic assembly_out.dynamic = self.exporterOptions.exportMode == ExportMode.ROBOT @@ -71,65 +74,81 @@ def export(self) -> None: progressDialog, ) - handle_err_top(Materials.MapAllAppearances( - design.appearances, - assembly_out.data.materials, - self.exporterOptions, - self.pdMessage, - )) - - handle_err_top(Materials.MapAllPhysicalMaterials( - design.materials, - assembly_out.data.materials, - self.exporterOptions, - self.pdMessage, - )) - - handle_err_top(Components.MapAllComponents( - design, - self.exporterOptions, - self.pdMessage, - assembly_out.data.parts, - assembly_out.data.materials, - )) + handle_err_top( + Materials.MapAllAppearances( + design.appearances, + assembly_out.data.materials, + self.exporterOptions, + self.pdMessage, + ) + ) + + handle_err_top( + Materials.MapAllPhysicalMaterials( + design.materials, + assembly_out.data.materials, + self.exporterOptions, + self.pdMessage, + ) + ) + + handle_err_top( + Components.MapAllComponents( + design, + self.exporterOptions, + self.pdMessage, + assembly_out.data.parts, + assembly_out.data.materials, + ) + ) rootNode = types_pb2.Node() - handle_err_top(Components.ParseComponentRoot( - design.rootComponent, - self.pdMessage, - self.exporterOptions, - assembly_out.data.parts, - assembly_out.data.materials.appearances, - rootNode, - )) + handle_err_top( + Components.ParseComponentRoot( + design.rootComponent, + self.pdMessage, + self.exporterOptions, + assembly_out.data.parts, + assembly_out.data.materials.appearances, + rootNode, + ) + ) Components.MapRigidGroups(design.rootComponent, assembly_out.data.joints) assembly_out.design_hierarchy.nodes.append(rootNode) # Problem Child - handle_err_top(Joints.populateJoints( - design, - assembly_out.data.joints, - assembly_out.data.signals, - self.pdMessage, - self.exporterOptions, - assembly_out, - )) + handle_err_top( + Joints.populateJoints( + design, + assembly_out.data.joints, + assembly_out.data.signals, + self.pdMessage, + self.exporterOptions, + assembly_out, + ) + ) # add condition in here for advanced joints maybe idk # should pre-process to find if there are any grounded joints at all # that or add code to existing parser to determine leftovers - handle_err_top(Joints.createJointGraph( - self.exporterOptions.joints, - self.exporterOptions.wheels, - assembly_out.joint_hierarchy, - self.pdMessage, - )) + handle_err_top( + Joints.createJointGraph( + self.exporterOptions.joints, + self.exporterOptions.wheels, + assembly_out.joint_hierarchy, + self.pdMessage, + ) + ) - handle_err_top(JointHierarchy.BuildJointPartHierarchy(design, assembly_out.data.joints, self.exporterOptions, self.pdMessage)) + handle_err_top( + JointHierarchy.BuildJointPartHierarchy( + design, assembly_out.data.joints, self.exporterOptions, self.pdMessage + ) + ) # These don't have an effect, I forgot how this is suppose to work # progressDialog.message = "Taking Photo for thumbnail..." @@ -244,6 +263,7 @@ def export(self) -> None: logger.debug(debug_output.strip()) + def handle_err_top[T](err: Result[T]): if err.is_err(): message, severity = err.unwrap_err() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 380995cbbe..d71e398d4e 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -36,12 +36,13 @@ def GetPhysicalProperties( level (int): Level of accurracy """ physical = fusionObject.getPhysicalProperties(level) - - missing_properties = [prop is None for prop in physical] if physical is None: return Err("Physical properties object is None", ErrorSeverity.Warning) - if any(missing_properties.): - _ = Err(f"Missing some physical properties", ErrorSeverity.Warning) + + missing_properties_bools = [prop is None for prop in physical] + if any(prop for prop, i in missing_properties_bools): + missing_properties: list[Unknown] = [physics[i] for i, prop in enumerate(missing_properties) if prop] + _ = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) physicalProperties.density = physical.density physicalProperties.mass = physical.mass diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index 2e45497372..4db87edec4 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -20,7 +20,11 @@ def guid_none(_: None) -> str: return str(uuid.uuid4()) -def fill_info(proto_obj: assembly_pb2.Assembly | material_pb2.Materials, fus_object: adsk.core.Base, override_guid: str | None = None) -> Result[None]: +def fill_info( + proto_obj: assembly_pb2.Assembly | material_pb2.Materials, + fus_object: adsk.core.Base, + override_guid: str | None = None, +) -> Result[None]: return construct_info("", proto_obj, fus_object=fus_object, GUID=override_guid) @@ -66,8 +70,6 @@ def construct_info( return Ok(None) - - def rad_to_deg(rad): # type: ignore """Converts radians to degrees @@ -79,6 +81,7 @@ def rad_to_deg(rad): # type: ignore """ return (rad * 180) / math.pi + def throwZero(): # type: ignore """Errors on incorrect quat values From 69ba005c8b776a99cef3868c40fe086ad527d609 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 08:27:06 -0700 Subject: [PATCH 14/68] chore: format physical properties --- .../src/Parser/SynthesisParser/PhysicalProperties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index d71e398d4e..00d722a75d 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -40,7 +40,7 @@ def GetPhysicalProperties( return Err("Physical properties object is None", ErrorSeverity.Warning) missing_properties_bools = [prop is None for prop in physical] - if any(prop for prop, i in missing_properties_bools): + if any(prop for prop, i in missing_properties_bools): missing_properties: list[Unknown] = [physics[i] for i, prop in enumerate(missing_properties) if prop] _ = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) From 27988a7f8690b8d4bffa4ef7954d991319d8d7d6 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 08:28:57 -0700 Subject: [PATCH 15/68] chore: format with isort --- exporter/SynthesisFusionAddin/Synthesis.py | 9 ++++++++- exporter/SynthesisFusionAddin/src/ErrorHandling.py | 3 ++- .../src/Parser/SynthesisParser/Components.py | 2 +- .../src/Parser/SynthesisParser/JointHierarchy.py | 5 ++--- .../src/Parser/SynthesisParser/Materials.py | 2 +- .../src/Parser/SynthesisParser/Parser.py | 11 ++++++++--- exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py | 2 +- exporter/SynthesisFusionAddin/src/__init__.py | 1 - 8 files changed, 23 insertions(+), 12 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index e8940e669a..20a7b322db 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -33,7 +33,14 @@ from src import APP_NAME, DESCRIPTION, INTERNAL_ID, gm -from src.UI import HUI, Camera, ConfigCommand, MarkingMenu, ShowAPSAuthCommand, ShowWebsiteCommand +from src.UI import ( + HUI, + Camera, + ConfigCommand, + MarkingMenu, + ShowAPSAuthCommand, + ShowWebsiteCommand, +) from src.UI.Toolbar import Toolbar diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index c8059c4a36..2f47f873d9 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -1,7 +1,8 @@ -from .Logging import getLogger from enum import Enum from typing import Generic, TypeVar +from .Logging import getLogger + logger = getLogger() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 80bbd123c7..4c70e8268a 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -1,7 +1,7 @@ # Contains all of the logic for mapping the Components / Occurrences -from requests.models import parse_header_links import adsk.core import adsk.fusion +from requests.models import parse_header_links from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import logFailure diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 19adac21e0..70e44bc1c4 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -2,14 +2,13 @@ from logging import ERROR from typing import Any, Iterator, cast -from google.protobuf.message import Error - import adsk.core import adsk.fusion +from google.protobuf.message import Error from src import gm +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import getLogger, logFailure -from src.ErrorHandling import Result, Err, Ok, ErrorSeverity from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import guid_component, guid_occurrence diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index ba626307fc..f5a09d14f3 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -1,10 +1,10 @@ import adsk.core +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import construct_info, fill_info from src.Proto import material_pb2 -from src.ErrorHandling import ErrorSeverity, Result, Ok, Err OPACITY_RAMPING_CONSTANT = 14.0 diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index de2e358cc5..30da9147df 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -7,10 +7,15 @@ from src import gm from src.APS.APS import getAuth, upload_mirabuf from src.ErrorHandling import ErrorSeverity, Result -from src.Parser.ExporterOptions import ExporterOptions -from src.Parser.SynthesisParser import Components, JointHierarchy, Joints, Materials, PDMessage - from src.Logging import getLogger, logFailure, timed +from src.Parser.ExporterOptions import ExporterOptions +from src.Parser.SynthesisParser import ( + Components, + JointHierarchy, + Joints, + Materials, + PDMessage, +) from src.Parser.SynthesisParser.Utilities import fill_info from src.Proto import assembly_pb2, types_pb2 from src.Types import ExportLocation, ExportMode diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 084ccd5141..3d847d9dc6 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -10,9 +10,9 @@ import adsk.core import adsk.fusion -from src.Logging import logFailure from src import APP_WEBSITE_URL, gm from src.APS.APS import getAuth, getUserInfo +from src.Logging import logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.Parser import Parser from src.Types import SELECTABLE_JOINT_TYPES, ExportLocation, ExportMode diff --git a/exporter/SynthesisFusionAddin/src/__init__.py b/exporter/SynthesisFusionAddin/src/__init__.py index 3f761b65c0..42d1e6d391 100644 --- a/exporter/SynthesisFusionAddin/src/__init__.py +++ b/exporter/SynthesisFusionAddin/src/__init__.py @@ -5,7 +5,6 @@ from src.GlobalManager import GlobalManager from src.Util import makeDirectories - APP_NAME = "Synthesis" APP_TITLE = "Synthesis Robot Exporter" APP_WEBSITE_URL = "https://synthesis.autodesk.com/fission/" From bc5b20daff848ab9794c250111b189347af08c5c Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 08:48:52 -0700 Subject: [PATCH 16/68] fix: typing --- .../SynthesisFusionAddin/src/ErrorHandling.py | 4 ++-- .../src/Parser/SynthesisParser/Components.py | 19 +++++++++++++------ .../Parser/SynthesisParser/JointHierarchy.py | 17 +++++++++++------ .../src/Parser/SynthesisParser/Joints.py | 18 +++++++++--------- .../src/Parser/SynthesisParser/Materials.py | 4 ++-- .../src/Parser/SynthesisParser/Parser.py | 3 ++- .../SynthesisParser/PhysicalProperties.py | 7 ++++--- 7 files changed, 43 insertions(+), 29 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 2f47f873d9..667669330f 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -41,7 +41,7 @@ class Ok(Result[T]): def __init__(self, value: T): self.value = value - def __repr__(self): + def __repr__(self) -> str: return f"Ok({self.value})" @@ -55,7 +55,7 @@ def __init__(self, message: str, severity: ErrorSeverity): self.write_error() - def __repr__(self): + def __repr__(self) -> str: return f"Err({self.message})" def write_error(self) -> None: diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 4c70e8268a..befe5cbd6e 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -1,4 +1,5 @@ # Contains all of the logic for mapping the Components / Occurrences +from platform import python_build import adsk.core import adsk.fusion from requests.models import parse_header_links @@ -43,7 +44,9 @@ def MapAllComponents( if fill_info_result.is_err(): return fill_info_result - PhysicalProperties.GetPhysicalProperties(component, partDefinition.physical_data) + physical_properties_result = PhysicalProperties.GetPhysicalProperties(component, partDefinition.physical_data) + if physical_properties_result.is_err() and physical_properties_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return physical_properties_result partDefinition.dynamic = options.exportMode != ExportMode.FIELD @@ -60,11 +63,11 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non if isinstance(body, adsk.fusion.BRepBody): parse_result = ParseBRep(body, options, part_body.triangle_mesh) - if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: + if parse_result.is_err() and parse_result.unwrap_err()[1] == ErrorSeverity.Fatal: return parse_result else: parse_result = ParseMesh(body, options, part_body.triangle_mesh) - if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: + if parse_result.is_err() and parse_result.unwrap_err()[1] == ErrorSeverity.Fatal: return parse_result appearance_key = "{}_{}".format(body.appearance.name, body.appearance.id) @@ -74,15 +77,19 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non else: part_body.appearance_override = "default" + return Ok(None) + for body in component.bRepBodies: process_result = processBody(body) - if process_result.is_err() and process_result.unwrap_err()[0] == ErrorSeverity.Fatal: + if process_result.is_err() and process_result.unwrap_err()[1] == ErrorSeverity.Fatal: return process_result for body in component.meshBodies: process_result = processBody(body) - if process_result.is_err() and process_result.unwrap_err()[0] == ErrorSeverity.Fatal: + if process_result.is_err() and process_result.unwrap_err()[1] == ErrorSeverity.Fatal: return process_result + return Ok(None) + def ParseComponentRoot( component: adsk.fusion.Component, @@ -146,7 +153,7 @@ def parseChildOccurrence( node.value = mapConstant fill_info_result = fill_info(part, occurrence, mapConstant) - if fill_info_result.is_err() and fill_info_result.unwrap_err() == ErrorSeverity.Fatal: + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return fill_info_result collision_attr = occurrence.attributes.itemByName("synthesis", "collision_off") diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 70e44bc1c4..31b4642a51 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -102,7 +102,7 @@ class DynamicOccurrenceNode(GraphNode): def __init__(self, occurrence: adsk.fusion.Occurrence, isGround: bool = False, previous: GraphNode | None = None): super().__init__(occurrence) self.isGround = isGround - self.name = occurrence.name # type: ignore + self.name = occurrence.name def print(self) -> None: print(f"\n\t-------{self.data.name}-------") @@ -255,7 +255,9 @@ def __init__(self, design: adsk.fusion.Design) -> None: # creates the axis elements - adds all elements to axisNodes for key, value in self.dynamicJoints.items(): - self._populateAxis(key, value) + populate_axis_result = self._populateAxis(key, value) + if populate_axis_result.is_err(): + raise RuntimeError(populate_axis_result.unwrap_err()[0]) self._linkAllAxis() @@ -352,22 +354,25 @@ def _lookForGroundedJoints(self) -> Result[None]: ) return Ok(None) - def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint) -> None: + def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint) -> Result[None]: occ = self.design.findEntityByToken(occ_token)[0] if occ is None: - return + return Ok(None) self.currentTraversal = dict() populate_node_result = self._populateNode(occ, None, None) if populate_node_result.is_err(): # We need the value to proceed - return populate_node_result + unwrapped = populate_node_result.unwrap_err() + return Err(unwrapped[0], unwrapped[1]) rootNode = populate_node_result.unwrap() if rootNode is not None: axisNode = SimulationNode(rootNode, joint) self.simulationNodesRef[occ_token] = axisNode + return Ok(None) + # TODO: Verify that this works after the Result-refactor :skull: def _populateNode( self, @@ -573,7 +578,7 @@ def createTreeParts( node.value = guid_component(dynNode.data) else: if dynNode.data.entityToken is None: - _ = Err("Found None EntityToken", ErrorSeverity.Warning) # type: ignore + _ = Err("Found None EntityToken", ErrorSeverity.Warning) node.value = dynNode.data.name else: node.value = dynNode.data.entityToken diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index d2f1469e41..37d8bce424 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -79,7 +79,7 @@ def populateJoints( assembly: assembly_pb2.Assembly, ) -> Result[None]: info_result = fill_info(joints, None) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result # This is for creating all of the Joint Definition objects @@ -90,12 +90,12 @@ def populateJoints( # Add the grounded joints object - TODO: rename some of the protobuf stuff for the love of god joint_definition_ground = joints.joint_definitions["grounded"] info_result = construct_info("grounded", joint_definition_ground) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result joint_instance_ground = joints.joint_instances["grounded"] info_result = construct_info("grounded", joint_instance_ground) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result joint_instance_ground.joint_reference = joint_definition_ground.info.GUID @@ -132,7 +132,7 @@ def populateJoints( signal = signals.signal_map[guid] info_result = construct_info(joint.name, signal, GUID=guid) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result signal.io = signal_pb2.IOType.OUTPUT @@ -147,7 +147,7 @@ def populateJoints( motor = joints.motor_definitions[joint.entityToken] info_result = fill_info(motor, joint) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result simple_motor = motor.simple_motor @@ -161,7 +161,7 @@ def populateJoints( # signals.signal_map.remove(guid) joint_result = _addJointInstance(joint, joint_instance, joint_definition, signals, options) - if joint_result.is_err() and joint_result.severity == ErrorSeverity.Fatal: + if joint_result.is_err() and joint_result.unwrap_err()[1] == ErrorSeverity.Fatal: return joint_result # adds information for joint motion and limits @@ -176,7 +176,7 @@ def populateJoints( def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> Result[None]: info_result = fill_info(joint_definition, joint) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result jointPivotTranslation = _jointOrigin(joint) @@ -205,7 +205,7 @@ def _addJointInstance( options: ExporterOptions, ) -> Result[None]: info_result = fill_info(joint_instance, joint) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result # because there is only one and we are using the token - should be the same @@ -250,7 +250,7 @@ def _addJointInstance( signal = signals.signal_map[guid] info_result = construct_info("joint_signal", signal, GUID=guid) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result signal.io = signal_pb2.IOType.OUTPUT diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index f5a09d14f3..7a4ede8fcd 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -150,8 +150,8 @@ def getPhysicalMaterialData( strengthProperties.yield_strength = materialProperties.itemById("structural_Minimum_yield_stress").value strengthProperties.tensile_strength = materialProperties.itemById("structural_Minimum_tensile_strength").value - missingProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] # ignore: type - if missingProperties.__len__() > 0: + missingStrengthProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] # ignore: type + if missingStrengthProperties.__len__() > 0: _ = Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) """ diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 30da9147df..01f5f335d0 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -269,8 +269,9 @@ def export(self) -> None: logger.debug(debug_output.strip()) -def handle_err_top[T](err: Result[T]): +def handle_err_top[T](err: Result[T]) -> None: if err.is_err(): message, severity = err.unwrap_err() if severity == ErrorSeverity.Fatal: + app = adsk.core.Application.get() app.userInterface.messageBox(f"Fatal Error Encountered: {message}") diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 00d722a75d..683b2829f6 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -16,6 +16,7 @@ """ +from typing import Any import adsk from src.ErrorHandling import Err, ErrorSeverity, Ok, Result @@ -39,9 +40,9 @@ def GetPhysicalProperties( if physical is None: return Err("Physical properties object is None", ErrorSeverity.Warning) - missing_properties_bools = [prop is None for prop in physical] - if any(prop for prop, i in missing_properties_bools): - missing_properties: list[Unknown] = [physics[i] for i, prop in enumerate(missing_properties) if prop] + missing_properties_bools: list[bool] = [prop is None for prop in physical] + if any(prop for prop in missing_properties_bools): + missing_properties: list[Any] = [physical[i] for i, prop in enumerate(missing_properties_bools) if prop] _ = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) physicalProperties.density = physical.density From a19f3a204e37ddf27f9e725224a030df91a2949d Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 08:49:43 -0700 Subject: [PATCH 17/68] chore: format again :| --- .../src/Parser/SynthesisParser/Components.py | 1 + .../src/Parser/SynthesisParser/PhysicalProperties.py | 1 + 2 files changed, 2 insertions(+) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index befe5cbd6e..ecb06484c1 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -1,5 +1,6 @@ # Contains all of the logic for mapping the Components / Occurrences from platform import python_build + import adsk.core import adsk.fusion from requests.models import parse_header_links diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 683b2829f6..104587aab6 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -17,6 +17,7 @@ """ from typing import Any + import adsk from src.ErrorHandling import Err, ErrorSeverity, Ok, Result From 13c5e2ffe822e4524c59b2f8175577882e3dedaf Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 11:23:46 -0700 Subject: [PATCH 18/68] doc: result and it's variants --- .../SynthesisFusionAddin/src/ErrorHandling.py | 54 +++++++++++++++++- .../src/Parser/SynthesisParser/Components.py | 4 +- .../Parser/SynthesisParser/JointHierarchy.py | 6 +- .../src/Parser/SynthesisParser/Joints.py | 4 +- .../src/Parser/SynthesisParser/Materials.py | 4 +- .../SynthesisParser/PhysicalProperties.py | 6 +- .../src/Resources/PWM_icon/16x16-disabled.png | Bin 0 -> 422 bytes .../src/Resources/PWM_icon/32x32-disabled.png | Bin 0 -> 182 bytes .../src/Resources/PWM_icon/32x32-normal.png | Bin 0 -> 545 bytes 9 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 exporter/SynthesisFusionAddin/src/Resources/PWM_icon/16x16-disabled.png create mode 100644 exporter/SynthesisFusionAddin/src/Resources/PWM_icon/32x32-disabled.png create mode 100644 exporter/SynthesisFusionAddin/src/Resources/PWM_icon/32x32-normal.png diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 667669330f..e5dd0fbf0b 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -11,6 +11,7 @@ # If an error is non-fatal to the function that generated it, it should be declared but not return, which prints it to the screen class ErrorSeverity(Enum): Fatal = 50 # Critical Error + Error = 40 # Non-critical Error Warning = 30 # Warning @@ -18,24 +19,56 @@ class ErrorSeverity(Enum): class Result(Generic[T]): + """ + Result class for error handling, similar to the Result enum in Rust. The `Err` and `Ok` variants are child types, rather than enum variants though. Another difference is that the error variant is necessarily packaged with a message and a severity, rather than being arbitrary. + Since python3 has no match statements, use the `is_ok()` or `is_err()` function to check the variant, then `unwrap()` or `unwrap_err()` to get the value or error message and severity. + + ## Example + ```py + foo_result = foo() + if foo_result.is_err() and foo_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return foo_result + ``` + + Please see the `Ok` and `Err` child class documentation for instructions on instantiating errors and ok-values respectively + """ + def is_ok(self) -> bool: + """ + Returns if the Result is the Ok variant + """ return isinstance(self, Ok) def is_err(self) -> bool: + """ + Returns if the Result is the Err variant + """ + return isinstance(self, Err) def unwrap(self) -> T: + """ + Returns the value contained in the Ok variant of the result, or raises an exception if unwrapping an Err variant. Be sure to check first. + """ if self.is_ok(): return self.value # type: ignore raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore def unwrap_err(self) -> tuple[str, ErrorSeverity]: + """ + Returns the error message and severity contained in the Err variant of the result, or raises an exception if unwrapping an Ok variant. Be sure to check first. + """ + if self.is_err(): return (self.message, self.severity) # type: ignore raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore class Ok(Result[T]): + """ + The non-error variant of the Result class. Contains the value of the happy path of the function. Return when the function has executed successfully. + """ + value: T def __init__(self, value: T): @@ -46,6 +79,26 @@ def __repr__(self) -> str: class Err(Result[T]): + """ + The error variant of the Result class. Contains an error message and severity. Severity is the `ErrorSeverity` enum and is either Fatal, Error, or Warning, each corresponding to a logger severity level, Critical Error (50) and Warning (30) respectively. When an `Err` is instantiated, it is automatically logged in the current synthesis logfile. + + If an error is fatal to the entire program (or the parent function), it should be returned and marked as Fatal: + ```python + return Err("Foo not found", ErrorSeverity.Fatal) + ``` + + If an error is fatal to the current function, it should be returned and marked as Error, as the parent could recover it: + ```python + return Err("Bar not found", ErrorSeverity.Error) + ``` + + If an error is not fatal to the current function, but ought to be logged, it should be marked as Warning and instantiated but not returned, as to not break control flow: + ```python + _: Err[T] = Err("Baz not found", ErrorSeverity.Warning) + ``` + Note that the lattermost example will raise a warning if not explicitely typed + """ + message: str severity: ErrorSeverity @@ -59,5 +112,4 @@ def __repr__(self) -> str: return f"Err({self.message})" def write_error(self) -> None: - # Figure out how to integrate severity with the logger logger.log(self.severity.value, self.message) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index ecb06484c1..dded196b3c 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -165,7 +165,7 @@ def parseChildOccurrence( try: part.appearance = "{}_{}".format(occurrence.appearance.name, occurrence.appearance.id) except: - _ = Err("Failed to format part appearance", ErrorSeverity.Warning) + _: Err[None] = Err("Failed to format part appearance", ErrorSeverity.Warning) # ignore: type part.appearance = "default" # TODO: Add phyical_material parser @@ -174,7 +174,7 @@ def parseChildOccurrence( if occurrence.component.material: part.physical_material = occurrence.component.material.id else: - _ = Err(f"Component Material is None", ErrorSeverity.Warning) + __: Err[None] = Err(f"Component Material is None", ErrorSeverity.Warning) def_map = partsData.part_definitions diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 31b4642a51..4393eb1a14 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -270,7 +270,7 @@ def __getAllJoints(self) -> Result[None]: occurrenceTwo = joint.occurrenceTwo else: # Non-fatal since it's recovered in the next two statements - _ = Err("Found joint without two occurences", ErrorSeverity.Warning) + _: Err[None] = Err("Found joint without two occurences", ErrorSeverity.Warning) if occurrenceOne is None: if joint.geometryOrOriginOne.entityOne.assemblyContext is None: @@ -567,7 +567,7 @@ def createTreeParts( # Fine way to use try-excepts in this language if dynNode.data.objectType is None: - _ = Err("Found None object type", ErrorSeverity.Warning) + _: Err[None] = Err("Found None object type", ErrorSeverity.Warning) objectType = "" else: objectType = dynNode.data.objectType @@ -578,7 +578,7 @@ def createTreeParts( node.value = guid_component(dynNode.data) else: if dynNode.data.entityToken is None: - _ = Err("Found None EntityToken", ErrorSeverity.Warning) + __: Err[None] = Err("Found None EntityToken", ErrorSeverity.Warning) node.value = dynNode.data.name else: node.value = dynNode.data.entityToken diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index 37d8bce424..21274fb4d2 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -169,7 +169,7 @@ def populateJoints( except: # TODO: Figure out how to construct and return this (ie, what actually breaks in this try block) - _ = Err("Failed:\n{}".format(traceback.format_exc()), ErrorSeverity.Fatal) + _: Err[None] = Err("Failed:\n{}".format(traceback.format_exc()), ErrorSeverity.Fatal) continue return Ok(None) @@ -190,7 +190,7 @@ def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> Re joint_definition.origin.z = 0.0 # TODO: We definitely could make this fatal, figure out if we should - _ = Err(f"Cannot find joint origin on joint {joint.name}", ErrorSeverity.Warning) + _: Err[None] = Err(f"Cannot find joint origin on joint {joint.name}", ErrorSeverity.Warning) joint_definition.break_magnitude = 0.0 diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 7a4ede8fcd..3b2f9b7a1b 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -142,7 +142,7 @@ def getPhysicalMaterialData( missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] # ignore: type if missingProperties.__len__() > 0: - _ = Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) + _: Err[None] = Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) """ Strength Properties @@ -152,7 +152,7 @@ def getPhysicalMaterialData( missingStrengthProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] # ignore: type if missingStrengthProperties.__len__() > 0: - _ = Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) + __: Err[None] = Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) """ strengthProperties.thermal_treatment = materialProperties.itemById( diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 104587aab6..bfe4bede87 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -39,12 +39,12 @@ def GetPhysicalProperties( """ physical = fusionObject.getPhysicalProperties(level) if physical is None: - return Err("Physical properties object is None", ErrorSeverity.Warning) + return Err("Physical properties object is None", ErrorSeverity.Error) missing_properties_bools: list[bool] = [prop is None for prop in physical] if any(prop for prop in missing_properties_bools): missing_properties: list[Any] = [physical[i] for i, prop in enumerate(missing_properties_bools) if prop] - _ = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) + _: Err[None] = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) physicalProperties.density = physical.density physicalProperties.mass = physical.mass @@ -59,6 +59,6 @@ def GetPhysicalProperties( _com.y = com.y _com.z = com.z else: - _ = Err("com is None", ErrorSeverity.Warning) + __: Err[None] = Err("com is None", ErrorSeverity.Warning) return Ok(None) diff --git a/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/16x16-disabled.png b/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/16x16-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..785693934c1e5b5981821965902402bd18336602 GIT binary patch literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^YCtT&!3HF&aX}sYeZu~){mBo_zrk%q!Co3Uq&d#}$ zqUP|VJZHH1kz@bU^XUm2ejGY*z(!DUnn`bCVzo7M^AXvW8K<1s*#3pFwdGD1Oq&1q z;J;K6>ySxNflNXlFPEoic<6YscpEn}ndtU5KD=DS#&*wxjqM>f4^PZRf$|2%Y(s+u zo;>P6Z>26YtCpW~{36h^oCO|{#S9F5he4R}c>anMpkTJAi(`mK=hdr*Tn7|*94^W) z|8{L>dGs5h>60$#cGe5C$O{IBh-P0>W|Y|WqKRoUPu0S(;y>?KYBt%f?;xnUDYg literal 0 HcmV?d00001 diff --git a/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/32x32-disabled.png b/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/32x32-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..834074252e9aafb6beb6e11766172b651e92cfaf GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND45~t z;usRq`u18N*8v3{<_lVNXQIE~)4DCXN}@^cr`Tr=F`svNXHv3uGca)^q@_N3)6L+b z&Y;=Pl<+i=!DMyA$7+XkW+`ch`3JVWjoiCp6C=O$23>asA!!EJGk@3>I+X7>@F z40F%Jct&I7=MT7u;;o^}H&!SW_UaWr jXY@BnG|)f;|2KXD6uPAk{urCr00000NkvXXu0mjfq}KO! literal 0 HcmV?d00001 From 982c9b6bfcdfad8fbb2bbd7fdeba1e4eeb82f1d1 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 11:26:48 -0700 Subject: [PATCH 19/68] chore(wip): bump typing validation python version --- .github/workflows/FusionTyping.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/FusionTyping.yml b/.github/workflows/FusionTyping.yml index 13de49fc2f..16a87da037 100644 --- a/.github/workflows/FusionTyping.yml +++ b/.github/workflows/FusionTyping.yml @@ -3,9 +3,9 @@ name: Fusion - mypy Typing Validation on: workflow_dispatch: {} push: - branches: [ prod, dev ] + branches: [prod, dev] pull_request: - branches: [ prod, dev ] + branches: [prod, dev] jobs: mypy: @@ -18,6 +18,6 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: "3.12" - run: pip install -r requirements-mypy.txt - run: mypy From 60613c50450a30a947b9f73682a1b8ccb38f2519 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 11:35:41 -0700 Subject: [PATCH 20/68] chore: remove unecessary comment --- .../src/Parser/SynthesisParser/Components.py | 1 - 1 file changed, 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index dded196b3c..996dde51de 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -166,7 +166,6 @@ def parseChildOccurrence( part.appearance = "{}_{}".format(occurrence.appearance.name, occurrence.appearance.id) except: _: Err[None] = Err("Failed to format part appearance", ErrorSeverity.Warning) - # ignore: type part.appearance = "default" # TODO: Add phyical_material parser From 77318920a746ebc4684bb90db606d6510db03cdf Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 1 Jul 2025 10:53:49 -0700 Subject: [PATCH 21/68] fix: remove comments and unneeded import --- exporter/SynthesisFusionAddin/Synthesis.py | 1 - .../SynthesisFusionAddin/src/ErrorHandling.py | 15 +-------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 20a7b322db..9729b49a89 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -11,7 +11,6 @@ from src.Logging import logFailure, setupLogger logger = setupLogger() -from src.ErrorHandling import Err, ErrorSeverity try: # Attempt to import required pip dependencies to verify their installation. diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index e5dd0fbf0b..a2d0b5faa1 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -34,31 +34,18 @@ class Result(Generic[T]): """ def is_ok(self) -> bool: - """ - Returns if the Result is the Ok variant - """ return isinstance(self, Ok) def is_err(self) -> bool: - """ - Returns if the Result is the Err variant - """ - return isinstance(self, Err) def unwrap(self) -> T: - """ - Returns the value contained in the Ok variant of the result, or raises an exception if unwrapping an Err variant. Be sure to check first. - """ if self.is_ok(): return self.value # type: ignore raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore def unwrap_err(self) -> tuple[str, ErrorSeverity]: - """ - Returns the error message and severity contained in the Err variant of the result, or raises an exception if unwrapping an Ok variant. Be sure to check first. - """ - + if self.is_err(): return (self.message, self.severity) # type: ignore raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore From a3e96f745b058d50950835aae4fbd5a7055cccb7 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 1 Jul 2025 10:54:45 -0700 Subject: [PATCH 22/68] chore: format --- exporter/SynthesisFusionAddin/src/ErrorHandling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index a2d0b5faa1..4f7944672f 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -45,7 +45,7 @@ def unwrap(self) -> T: raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore def unwrap_err(self) -> tuple[str, ErrorSeverity]: - + if self.is_err(): return (self.message, self.severity) # type: ignore raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore From ebfd1563aa9bfd73a2d9f4fd1e7a667cef9edb05 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 25 Jun 2025 16:46:31 -0700 Subject: [PATCH 23/68] feat: basic result error handling system --- .../SynthesisFusionAddin/src/ErrorHandling.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 exporter/SynthesisFusionAddin/src/ErrorHandling.py diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py new file mode 100644 index 0000000000..1988b21649 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -0,0 +1,43 @@ +from enum import Enum +from typing import Generic, TypeVar + +class ErrorSeverity(Enum): + Fatal = 1 + Warning = 2 +type ErrorMessage = str +type Error = tuple[ErrorSeverity, ErrorMessage] + +T = TypeVar('T') + +class Result(Generic[T]): + def is_ok(self) -> bool: + return isinstance(self, Ok) + + def is_err(self) -> bool: + return isinstance(self, Err) + + def unwrap(self) -> T: + if self.is_ok(): + return self.value # type: ignore + raise Exception(f"Called unwrap on Err: {self.error}") # type: ignore + + def unwrap_err(self) -> Error: + if self.is_err(): + return self.error # type: ignore + raise Exception("Called unwrap_err on Ok: {self.value}") + +class Ok(Result[T]): + value: T + def __init__(self, value: T): + self.value = value + + def __repr__(self): + return f"Ok({self.value})" + +class Err(Result[T]): + error: Error + def __init__(self, error: Error): + self.error = error + + def __repr__(self): + return f"Err({self.error})" From c1743f14cae7b69093fa471e9fc81211fa1cff43 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 25 Jun 2025 18:04:01 -0700 Subject: [PATCH 24/68] feat: start applying value-based error handling system to materials file --- .../SynthesisFusionAddin/src/ErrorHandling.py | 12 +-- .../src/Parser/SynthesisParser/Materials.py | 31 ++++++-- .../src/Parser/SynthesisParser/Utilities.py | 79 +++---------------- 3 files changed, 46 insertions(+), 76 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 1988b21649..752e0b9354 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -1,11 +1,11 @@ from enum import Enum from typing import Generic, TypeVar +# TODO Figure out if we need an error severity system +# Warnings are kind of useless if they break control flow anyways, so why not just replace warnings with writing to a log file and have errors break control flow class ErrorSeverity(Enum): Fatal = 1 Warning = 2 -type ErrorMessage = str -type Error = tuple[ErrorSeverity, ErrorMessage] T = TypeVar('T') @@ -35,9 +35,11 @@ def __repr__(self): return f"Ok({self.value})" class Err(Result[T]): - error: Error - def __init__(self, error: Error): - self.error = error + message: str + severity: ErrorSeverity + def __init__(self, message: str, severity: ErrorSeverity): + self.message = message + self.severity = severity def __repr__(self): return f"Err({self.error})" diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 08c5e17bb3..133136e52e 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -5,6 +5,7 @@ from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import construct_info, fill_info from src.Proto import material_pb2 +from ErrorHandling import ErrorMessage, ErrorSeverity, Result, Ok, Err OPACITY_RAMPING_CONSTANT = 14.0 @@ -44,8 +45,10 @@ def _MapAllPhysicalMaterials( getPhysicalMaterialData(material, newmaterial, options) -def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions) -> None: - construct_info("default", physicalMaterial) +def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions) -> Result[None]: + construct_info_result = construct_info("default", physicalMaterial) + if construct_info_result.is_err(): + return construct_info_result physicalMaterial.description = "A default physical material" if options.frictionOverride: @@ -59,11 +62,12 @@ def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: physicalMaterial.deformable = False physicalMaterial.matType = 0 # type: ignore[assignment] + return Ok(None) @logFailure def getPhysicalMaterialData( fusionMaterial: adsk.core.Material, physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions -) -> None: +) -> Result[None]: """Gets the material data and adds it to protobuf Args: @@ -71,7 +75,9 @@ def getPhysicalMaterialData( proto_material (protomaterial): proto material mirabuf options (parseoptions): parse options """ - construct_info("", physicalMaterial, fus_object=fusionMaterial) + construct_info_result = construct_info("", physicalMaterial, fus_object=fusionMaterial) + if construct_info_result.is_err(): + return construct_info_result physicalMaterial.deformable = False physicalMaterial.matType = 0 # type: ignore[assignment] @@ -127,17 +133,28 @@ def getPhysicalMaterialData( mechanicalProperties.density = materialProperties.itemById("structural_Density").value mechanicalProperties.damping_coefficient = materialProperties.itemById("structural_Damping_coefficient").value + missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] #ignore: type + if missingProperties.__len__() > 0: + return Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) + """ Strength Properties """ strengthProperties.yield_strength = materialProperties.itemById("structural_Minimum_yield_stress").value strengthProperties.tensile_strength = materialProperties.itemById("structural_Minimum_tensile_strength").value + + missingProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] #ignore: type + if missingProperties.__len__() > 0: + return Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) + """ strengthProperties.thermal_treatment = materialProperties.itemById( "structural_Thermally_treated" ).value """ + return Ok(None) + def _MapAllAppearances( appearances: list[material_pb2.Appearance], @@ -154,6 +171,8 @@ def _MapAllAppearances( for appearance in appearances: progressDialog.addAppearance(appearance.name) + # NOTE I'm not sure if this should be integrated with the error handling system or not, since it's fully intentional and immediantly aborts, which is the desired behavior + # TODO Talk to Brandon about this if progressDialog.wasCancelled(): raise RuntimeError("User canceled export") @@ -169,7 +188,9 @@ def setDefaultAppearance(appearance: material_pb2.Appearance) -> None: """ # add info - construct_info("default", appearance) + construct_info_result = construct_info("default", appearance) + if construct_info_result.is_err(): + return construct_info_result appearance.roughness = 0.5 appearance.metallic = 0.5 diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index d8f38d921f..6b84d5e8ac 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -4,7 +4,8 @@ import adsk.core import adsk.fusion -from src.Proto import assembly_pb2 +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result +from src.Proto import assembly_pb2, types_pb2 def guid_component(comp: adsk.fusion.Component) -> str: @@ -19,8 +20,8 @@ def guid_none(_: None) -> str: return str(uuid.uuid4()) -def fill_info(proto_obj: assembly_pb2.Assembly, fus_object: adsk.core.Base, override_guid: str | None = None) -> None: - construct_info("", proto_obj, fus_object=fus_object, GUID=override_guid) +def fill_info(proto_obj: assembly_pb2.Assembly, fus_object: adsk.core.Base, override_guid: str | None = None) -> Result[None]: + return construct_info("", proto_obj, fus_object=fus_object, GUID=override_guid) def construct_info( @@ -29,7 +30,8 @@ def construct_info( version: int = 5, fus_object: adsk.core.Base | None = None, GUID: str | None = None, -) -> None: +) -> Result[None]: + # TODO Fix out of date documentation """Constructs a info object from either a name or a fus_object Args: @@ -49,8 +51,10 @@ def construct_info( if fus_object is not None: proto_obj.info.name = fus_object.name - else: + elif name != "": proto_obj.info.name = name + else: + return Err("Attempted to set proto_obj.info.name to None", ErrorSeverity.Warning) if GUID is not None: proto_obj.info.GUID = str(GUID) @@ -59,29 +63,13 @@ def construct_info( else: proto_obj.info.GUID = str(uuid.uuid4()) + return Ok(None) + -# Transition: AARD-1765 -# Will likely be removed later as this is no longer used. Avoiding adding typing for now. -# My previous function was alot more optimized however now I realize the bug was this doesn't work well with degrees -def euler_to_quaternion(r): # type: ignore - (yaw, pitch, roll) = (r[0], r[1], r[2]) - qx = math.sin(roll / 2) * math.cos(pitch / 2) * math.cos(yaw / 2) - math.cos(roll / 2) * math.sin( - pitch / 2 - ) * math.sin(yaw / 2) - qy = math.cos(roll / 2) * math.sin(pitch / 2) * math.cos(yaw / 2) + math.sin(roll / 2) * math.cos( - pitch / 2 - ) * math.sin(yaw / 2) - qz = math.cos(roll / 2) * math.cos(pitch / 2) * math.sin(yaw / 2) - math.sin(roll / 2) * math.sin( - pitch / 2 - ) * math.cos(yaw / 2) - qw = math.cos(roll / 2) * math.cos(pitch / 2) * math.cos(yaw / 2) + math.sin(roll / 2) * math.sin( - pitch / 2 - ) * math.sin(yaw / 2) - return [qx, qy, qz, qw] def rad_to_deg(rad): # type: ignore - """Very simple method to convert Radians to degrees + """Converts radians to degrees Args: rad (float): radians unit @@ -91,49 +79,8 @@ def rad_to_deg(rad): # type: ignore """ return (rad * 180) / math.pi - -def quaternion_to_euler(qx, qy, qz, qw): # type: ignore - """Takes in quat values and converts to degrees - - - roll is x axis - atan2(2(qwqy + qzqw), 1-2(qy^2 + qz^2)) - - pitch is y axis - asin(2(qxqz - qwqy)) - - yaw is z axis - atan2(2(qxqw + qyqz), 1-2(qz^2+qw^3)) - - Args: - qx (float): quat_x - qy (float): quat_y - qz (float): quat_z - qw (float): quat_w - - Returns: - roll: x value in degrees - pitch: y value in degrees - yaw: z value in degrees - """ - # roll - sr_cp = 2 * ((qw * qx) + (qy * qz)) - cr_cp = 1 - (2 * ((qx * qx) + (qy * qy))) - roll = math.atan2(sr_cp, cr_cp) - # pitch - sp = 2 * ((qw * qy) - (qz * qx)) - if abs(sp) >= 1: - pitch = math.copysign(math.pi / 2, sp) - else: - pitch = math.asin(sp) - # yaw - sy_cp = 2 * ((qw * qz) + (qx * qy)) - cy_cp = 1 - (2 * ((qy * qy) + (qz * qz))) - yaw = math.atan2(sy_cp, cy_cp) - # convert to degrees - roll = rad_to_deg(roll) - pitch = rad_to_deg(pitch) - yaw = rad_to_deg(yaw) - # round and return - return round(roll, 4), round(pitch, 4), round(yaw, 4) - - def throwZero(): # type: ignore - """Simple function to report incorrect quat values + """Errors on incorrect quat values Raises: RuntimeError: Error describing the issue From 2513d8d994a79511e81138fed3ba1b0995e1b245 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 10:44:37 -0700 Subject: [PATCH 25/68] feat: error handling in components and materials --- .../SynthesisFusionAddin/src/ErrorHandling.py | 23 ++-- .../src/Parser/SynthesisParser/Components.py | 110 ++++++++++++------ .../src/Parser/SynthesisParser/Materials.py | 51 +++++--- .../src/Parser/SynthesisParser/Parser.py | 29 +++-- .../src/Parser/SynthesisParser/Utilities.py | 6 +- 5 files changed, 150 insertions(+), 69 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 752e0b9354..dc08bdcbe7 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -1,8 +1,10 @@ +from .Logging import getLogger from enum import Enum from typing import Generic, TypeVar -# TODO Figure out if we need an error severity system -# Warnings are kind of useless if they break control flow anyways, so why not just replace warnings with writing to a log file and have errors break control flow +# NOTE +# Severity refers to to the error's affect on the parser as a whole, rather than on the function itself +# If an error is non-fatal to the function that generated it, it should be declared but not return, which prints it to the screen class ErrorSeverity(Enum): Fatal = 1 Warning = 2 @@ -19,12 +21,12 @@ def is_err(self) -> bool: def unwrap(self) -> T: if self.is_ok(): return self.value # type: ignore - raise Exception(f"Called unwrap on Err: {self.error}") # type: ignore + raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore - def unwrap_err(self) -> Error: + def unwrap_err(self) -> tuple[str, ErrorSeverity]: if self.is_err(): - return self.error # type: ignore - raise Exception("Called unwrap_err on Ok: {self.value}") + return tuple[self.message, self.severity] # type: ignore + raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore class Ok(Result[T]): value: T @@ -41,5 +43,12 @@ def __init__(self, message: str, severity: ErrorSeverity): self.message = message self.severity = severity + self.write_error() + def __repr__(self): - return f"Err({self.error})" + return f"Err({self.message})" + + def write_error(self) -> None: + logger = getLogger() + # Figure out how to integrate severity with the logger + logger.log(1, self.message) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 6a78ad7cdf..c6d206e7a4 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -1,7 +1,9 @@ # Contains all of the logic for mapping the Components / Occurrences +from requests.models import parse_header_links import adsk.core import adsk.fusion +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser import PhysicalProperties @@ -15,15 +17,13 @@ from src.Types import ExportMode # TODO: Impelement Material overrides - - -def _MapAllComponents( +def MapAllComponents( design: adsk.fusion.Design, options: ExporterOptions, progressDialog: PDMessage, partsData: assembly_pb2.Parts, materials: material_pb2.Materials, -) -> None: +) -> Result[None]: for component in design.allComponents: adsk.doEvents() if progressDialog.wasCancelled(): @@ -32,31 +32,42 @@ def _MapAllComponents( comp_ref = guid_component(component) - fill_info(partsData, None) + fill_info_result = fill_info(partsData, None) + if fill_info_result.is_err(): + return fill_info_result + partDefinition = partsData.part_definitions[comp_ref] - fill_info(partDefinition, component, comp_ref) + fill_info_result = fill_info(partDefinition, component, comp_ref) + if fill_info_result.is_err(): + return fill_info_result + PhysicalProperties.GetPhysicalProperties(component, partDefinition.physical_data) - if options.exportMode == ExportMode.FIELD: - partDefinition.dynamic = False - else: - partDefinition.dynamic = True + partDefinition.dynamic = options.exportMode != ExportMode.FIELD - def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> None: + def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[None]: if progressDialog.wasCancelled(): raise RuntimeError("User canceled export") if body.isLightBulbOn: part_body = partDefinition.bodies.add() - fill_info(part_body, body) part_body.part = comp_ref + fill_info_result = fill_info(part_body, body) + if fill_info_result.is_err(): + return fill_info_result + if isinstance(body, adsk.fusion.BRepBody): - _ParseBRep(body, options, part_body.triangle_mesh) + parse_result = _ParseBRep(body, options, part_body.triangle_mesh) + if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: + return parse_result else: - _ParseMesh(body, options, part_body.triangle_mesh) + parse_result = _ParseMesh(body, options, part_body.triangle_mesh) + if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: + return parse_result + appearance_key = "{}_{}".format(body.appearance.name, body.appearance.id) # this should be appearance @@ -66,13 +77,17 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> None: part_body.appearance_override = "default" for body in component.bRepBodies: - processBody(body) - + process_result = processBody(body) + if process_result.is_err() and process_result.unwrap_err()[0] == ErrorSeverity.Fatal: + return process_result for body in component.meshBodies: - processBody(body) + process_result = processBody(body) + if process_result.is_err() and process_result.unwrap_err()[0] == ErrorSeverity.Fatal: + return process_result + -def _ParseComponentRoot( +def ParseComponentRoot( component: adsk.fusion.Component, progressDialog: PDMessage, options: ExporterOptions, @@ -86,7 +101,9 @@ def _ParseComponentRoot( node.value = mapConstant - fill_info(part, component, mapConstant) + fill_info_result = fill_info(part, component, mapConstant) + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return fill_info_result def_map = partsData.part_definitions @@ -99,18 +116,22 @@ def _ParseComponentRoot( if occur.isLightBulbOn: child_node = types_pb2.Node() - __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + + parse_child_result = __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + if parse_child_result.is_err(): + return parse_child_result + node.children.append(child_node) -def __parseChildOccurrence( +def parseChildOccurrence( occurrence: adsk.fusion.Occurrence, progressDialog: PDMessage, options: ExporterOptions, partsData: assembly_pb2.Parts, material_map: dict[str, material_pb2.Appearance], node: types_pb2.Node, -) -> None: +) -> Result[None]: if occurrence.isLightBulbOn is False: return @@ -124,7 +145,9 @@ def __parseChildOccurrence( node.value = mapConstant - fill_info(part, occurrence, mapConstant) + fill_info_result = fill_info(part, occurrence, mapConstant) + if fill_info_result.is_err() and fill_info_result.unwrap_err() == ErrorSeverity.Fatal: + return fill_info_result collision_attr = occurrence.attributes.itemByName("synthesis", "collision_off") if collision_attr != None: @@ -134,11 +157,15 @@ def __parseChildOccurrence( try: part.appearance = "{}_{}".format(occurrence.appearance.name, occurrence.appearance.id) except: + _ = Err("Failed to format part appearance", ErrorSeverity.Warning); # ignore: type part.appearance = "default" # TODO: Add phyical_material parser + # TODO: I'm fairly sure that this should be a fatal error if occurrence.component.material: part.physical_material = occurrence.component.material.id + else: + _ = Err(f"Component Material is None", ErrorSeverity.Warning) def_map = partsData.part_definitions @@ -165,7 +192,11 @@ def __parseChildOccurrence( if occur.isLightBulbOn: child_node = types_pb2.Node() - __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + + parse_child_result = __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + if parse_child_result.is_err(): + return parse_child_result + node.children.append(child_node) @@ -180,12 +211,11 @@ def GetMatrixWorld(occurrence: adsk.fusion.Occurrence) -> adsk.core.Matrix3D: return matrix -@logFailure -def _ParseBRep( +def ParseBRep( body: adsk.fusion.BRepBody, options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, -) -> None: +) -> Result[None]: meshManager = body.meshManager calc = meshManager.createMeshCalculator() # Disabling for now. We need the user to be able to adjust this, otherwise it gets locked @@ -196,26 +226,36 @@ def _ParseBRep( # calc.surfaceTolerance = 0.5 mesh = calc.calculate() - fill_info(trimesh, body) + fill_info_result = fill_info(trimesh, body) + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return fill_info_result + trimesh.has_volume = True plainmesh_out = trimesh.mesh - plainmesh_out.verts.extend(mesh.nodeCoordinatesAsFloat) plainmesh_out.normals.extend(mesh.normalVectorsAsFloat) plainmesh_out.indices.extend(mesh.nodeIndices) plainmesh_out.uv.extend(mesh.textureCoordinatesAsFloat) + return Ok(None) + -@logFailure -def _ParseMesh( +def ParseMesh( meshBody: adsk.fusion.MeshBody, options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, -) -> None: +) -> Result[None]: mesh = meshBody.displayMesh + if mesh is None: + return Err("Component Mesh was None", ErrorSeverity.Fatal) + + + fill_info_result = fill_info(trimesh, meshBody) + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return fill_info_result + - fill_info(trimesh, meshBody) trimesh.has_volume = True plainmesh_out = trimesh.mesh @@ -225,8 +265,10 @@ def _ParseMesh( plainmesh_out.indices.extend(mesh.nodeIndices) plainmesh_out.uv.extend(mesh.textureCoordinatesAsFloat) + return Ok(None) + -def _MapRigidGroups(rootComponent: adsk.fusion.Component, joints: joint_pb2.Joints) -> None: +def MapRigidGroups(rootComponent: adsk.fusion.Component, joints: joint_pb2.Joints) -> None: groups = rootComponent.allRigidGroups for group in groups: mira_group = joint_pb2.RigidGroup() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 133136e52e..3456683c7b 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -1,11 +1,10 @@ import adsk.core -from src.Logging import logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import construct_info, fill_info from src.Proto import material_pb2 -from ErrorHandling import ErrorMessage, ErrorSeverity, Result, Ok, Err +from src.ErrorHandling import ErrorSeverity, Result, Ok, Err OPACITY_RAMPING_CONSTANT = 14.0 @@ -27,22 +26,32 @@ } -def _MapAllPhysicalMaterials( +def MapAllPhysicalMaterials( physicalMaterials: list[material_pb2.PhysicalMaterial], materials: material_pb2.Materials, options: ExporterOptions, progressDialog: PDMessage, -) -> None: - setDefaultMaterial(materials.physicalMaterials["default"], options) +) -> Result[None]: + set_result = setDefaultMaterial(materials.physicalMaterials["default"], options) + if set_result.is_err() and set_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return set_result for material in physicalMaterials: - progressDialog.addMaterial(material.name) + if material.name is None or material.id is None: + return Err("Material missing id or name", ErrorSeverity.Fatal) + progressDialog.addMaterial(material.name) if progressDialog.wasCancelled(): raise RuntimeError("User canceled export") newmaterial = materials.physicalMaterials[material.id] - getPhysicalMaterialData(material, newmaterial, options) + material_result = getPhysicalMaterialData(material, newmaterial, options) + if material_result.is_err() and material_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return material_result + + return Ok(None) + + def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions) -> Result[None]: @@ -64,7 +73,6 @@ def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: return Ok(None) -@logFailure def getPhysicalMaterialData( fusionMaterial: adsk.core.Material, physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions ) -> Result[None]: @@ -156,17 +164,19 @@ def getPhysicalMaterialData( return Ok(None) -def _MapAllAppearances( +def MapAllAppearances( appearances: list[material_pb2.Appearance], materials: material_pb2.Materials, options: ExporterOptions, progressDialog: PDMessage, -) -> None: +) -> Result[None]: # in case there are no appearances on a body # this is just a color tho setDefaultAppearance(materials.appearances["default"]) - fill_info(materials, None) + fill_info_result = fill_info(materials, None) + if fill_info_result.is_err(): + return fill_info_result for appearance in appearances: progressDialog.addAppearance(appearance.name) @@ -177,10 +187,14 @@ def _MapAllAppearances( raise RuntimeError("User canceled export") material = materials.appearances["{}_{}".format(appearance.name, appearance.id)] - getMaterialAppearance(appearance, options, material) + material_result = getMaterialAppearance(appearance, options, material) + if material_result.is_err() and material_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return material_result + + return Ok(None) -def setDefaultAppearance(appearance: material_pb2.Appearance) -> None: +def setDefaultAppearance(appearance: material_pb2.Appearance) -> Result[None]: """Get a default color for the appearance Returns: @@ -202,18 +216,21 @@ def setDefaultAppearance(appearance: material_pb2.Appearance) -> None: color.B = 127 color.A = 255 + return Ok(None) def getMaterialAppearance( fusionAppearance: adsk.core.Appearance, options: ExporterOptions, appearance: material_pb2.Appearance, -) -> None: +) -> Result[None]: """Takes in a Fusion Mesh and converts it to a usable unity mesh Args: fusionAppearance (adsk.core.Appearance): Fusion appearance material """ - construct_info("", appearance, fus_object=fusionAppearance) + construct_info_result = construct_info("", appearance, fus_object=fusionAppearance) + if construct_info_result.is_err(): + return construct_info_result appearance.roughness = 0.9 appearance.metallic = 0.3 @@ -227,12 +244,15 @@ def getMaterialAppearance( color.A = 127 properties = fusionAppearance.appearanceProperties + if properties is None: + return Err("Apperarance Properties were None", ErrorSeverity.Fatal) roughnessProp = properties.itemById("surface_roughness") if roughnessProp: appearance.roughness = roughnessProp.value # Thank Liam for this. + # TODO Test if this is should be an error that we're just ignoring, or if it's actually just something we can skip over modelItem = properties.itemById("interior_model") if modelItem: matModelType = modelItem.value @@ -281,3 +301,4 @@ def getMaterialAppearance( color.B = baseColor.blue color.A = baseColor.opacity break + return Ok(None) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index b085b48d32..5bd22c631d 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -1,12 +1,15 @@ import gzip import pathlib +from google.protobuf import message + import adsk.core import adsk.fusion from google.protobuf.json_format import MessageToJson from src import gm from src.APS.APS import getAuth, upload_mirabuf +from src.ErrorHandling import ErrorSeverity, Result from src.Logging import getLogger, logFailure, timed from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser import ( @@ -44,11 +47,11 @@ def export(self) -> None: return assembly_out = assembly_pb2.Assembly() - fill_info( + handle_err_top(fill_info( assembly_out, design.rootComponent, override_guid=design.parentDocument.name, - ) + )) # set int to 0 in dropdown selection for dynamic assembly_out.dynamic = self.exporterOptions.exportMode == ExportMode.ROBOT @@ -77,21 +80,21 @@ def export(self) -> None: progressDialog, ) - Materials._MapAllAppearances( + handle_err_top(Materials._MapAllAppearances( design.appearances, assembly_out.data.materials, self.exporterOptions, self.pdMessage, - ) - - Materials._MapAllPhysicalMaterials( + )) + + handle_err_top(Materials.MapAllPhysicalMaterials( design.materials, assembly_out.data.materials, self.exporterOptions, self.pdMessage, - ) + )) - Components._MapAllComponents( + Components.MapAllComponents( design, self.exporterOptions, self.pdMessage, @@ -101,7 +104,7 @@ def export(self) -> None: rootNode = types_pb2.Node() - Components._ParseComponentRoot( + Components.ParseComponentRoot( design.rootComponent, self.pdMessage, self.exporterOptions, @@ -110,7 +113,7 @@ def export(self) -> None: rootNode, ) - Components._MapRigidGroups(design.rootComponent, assembly_out.data.joints) + Components.MapRigidGroups(design.rootComponent, assembly_out.data.joints) assembly_out.design_hierarchy.nodes.append(rootNode) @@ -249,3 +252,9 @@ def export(self) -> None: ) logger.debug(debug_output.strip()) + +def handle_err_top[T](err: Result[T]): + if err.is_err(): + message, severity = err.unwrap_err() + if severity == ErrorSeverity.Fatal: + app.userInterface.messageBox(f"Fatal Error Encountered: {message}") diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index 6b84d5e8ac..2e45497372 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -5,7 +5,7 @@ import adsk.fusion from src.ErrorHandling import Err, ErrorSeverity, Ok, Result -from src.Proto import assembly_pb2, types_pb2 +from src.Proto import assembly_pb2, material_pb2, types_pb2 def guid_component(comp: adsk.fusion.Component) -> str: @@ -20,13 +20,13 @@ def guid_none(_: None) -> str: return str(uuid.uuid4()) -def fill_info(proto_obj: assembly_pb2.Assembly, fus_object: adsk.core.Base, override_guid: str | None = None) -> Result[None]: +def fill_info(proto_obj: assembly_pb2.Assembly | material_pb2.Materials, fus_object: adsk.core.Base, override_guid: str | None = None) -> Result[None]: return construct_info("", proto_obj, fus_object=fus_object, GUID=override_guid) def construct_info( name: str, - proto_obj: assembly_pb2.Assembly, + proto_obj: assembly_pb2.Assembly | material_pb2.Materials | material_pb2.PhysicalMaterial, version: int = 5, fus_object: adsk.core.Base | None = None, GUID: str | None = None, From 79e2a393407e3c93ac431615742b8f2b61195098 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 10:52:25 -0700 Subject: [PATCH 26/68] fix: typing and function name --- .../src/Parser/SynthesisParser/Components.py | 2 +- .../src/Parser/SynthesisParser/Parser.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index c6d206e7a4..85c5f994ef 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -94,7 +94,7 @@ def ParseComponentRoot( partsData: assembly_pb2.Parts, material_map: dict[str, material_pb2.Appearance], node: types_pb2.Node, -) -> None: +) -> Result[None]: mapConstant = guid_component(component) part = partsData.part_instances[mapConstant] diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 5bd22c631d..968b0d8738 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -80,7 +80,7 @@ def export(self) -> None: progressDialog, ) - handle_err_top(Materials._MapAllAppearances( + handle_err_top(Materials.MapAllAppearances( design.appearances, assembly_out.data.materials, self.exporterOptions, @@ -94,24 +94,24 @@ def export(self) -> None: self.pdMessage, )) - Components.MapAllComponents( + handle_err_top(Components.MapAllComponents( design, self.exporterOptions, self.pdMessage, assembly_out.data.parts, assembly_out.data.materials, - ) + )) rootNode = types_pb2.Node() - Components.ParseComponentRoot( + handle_err_top(Components.ParseComponentRoot( design.rootComponent, self.pdMessage, self.exporterOptions, assembly_out.data.parts, assembly_out.data.materials.appearances, rootNode, - ) + )) Components.MapRigidGroups(design.rootComponent, assembly_out.data.joints) From 4710e1169095fb2220b2f7f042c323d55ef4daf5 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 13:06:49 -0700 Subject: [PATCH 27/68] fix: tuple literal --- exporter/SynthesisFusionAddin/src/ErrorHandling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index dc08bdcbe7..2d1594f879 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -25,7 +25,7 @@ def unwrap(self) -> T: def unwrap_err(self) -> tuple[str, ErrorSeverity]: if self.is_err(): - return tuple[self.message, self.severity] # type: ignore + return (self.message, self.severity) # type: ignore raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore class Ok(Result[T]): From d37528207bf7d9999482449ae99765880c8a498b Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 13:47:02 -0700 Subject: [PATCH 28/68] fix: updated old variable names (Components and Materials fully done :D) --- .../src/Parser/SynthesisParser/Components.py | 12 +++++++----- .../src/Parser/SynthesisParser/Materials.py | 5 ++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 85c5f994ef..fd4add0bb2 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -60,11 +60,11 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non return fill_info_result if isinstance(body, adsk.fusion.BRepBody): - parse_result = _ParseBRep(body, options, part_body.triangle_mesh) + parse_result = ParseBRep(body, options, part_body.triangle_mesh) if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: return parse_result else: - parse_result = _ParseMesh(body, options, part_body.triangle_mesh) + parse_result = ParseMesh(body, options, part_body.triangle_mesh) if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: return parse_result @@ -117,11 +117,12 @@ def ParseComponentRoot( if occur.isLightBulbOn: child_node = types_pb2.Node() - parse_child_result = __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + parse_child_result = parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) if parse_child_result.is_err(): return parse_child_result node.children.append(child_node) + return Ok(None) def parseChildOccurrence( @@ -133,7 +134,7 @@ def parseChildOccurrence( node: types_pb2.Node, ) -> Result[None]: if occurrence.isLightBulbOn is False: - return + return Ok(None) progressDialog.addOccurrence(occurrence.name) @@ -193,11 +194,12 @@ def parseChildOccurrence( if occur.isLightBulbOn: child_node = types_pb2.Node() - parse_child_result = __parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + parse_child_result = parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) if parse_child_result.is_err(): return parse_child_result node.children.append(child_node) + return Ok(None) # saw online someone used this to get the correct context but oh boy does it look pricey diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 3456683c7b..8d10d79a75 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -172,7 +172,9 @@ def MapAllAppearances( ) -> Result[None]: # in case there are no appearances on a body # this is just a color tho - setDefaultAppearance(materials.appearances["default"]) + set_default_result = setDefaultAppearance(materials.appearances["default"]) + if set_default_result.is_err() and set_default_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return set_default_result fill_info_result = fill_info(materials, None) if fill_info_result.is_err(): @@ -202,6 +204,7 @@ def setDefaultAppearance(appearance: material_pb2.Appearance) -> Result[None]: """ # add info + # TODO: Check if appearance actually can be passed in here in place of an assembly or smth construct_info_result = construct_info("default", appearance) if construct_info_result.is_err(): return construct_info_result From 667b629bddc51a4a6aa40be4a26239d86736db9e Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 15:26:16 -0700 Subject: [PATCH 29/68] feat: value-based error handling in JointHierarchy.py where possible --- .../Parser/SynthesisParser/JointHierarchy.py | 226 ++++++++++-------- 1 file changed, 129 insertions(+), 97 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 0b5be182c2..b0ff7e5bbe 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -1,11 +1,15 @@ import enum +from logging import ERROR from typing import Any, Iterator, cast +from google.protobuf.message import Error + import adsk.core import adsk.fusion from src import gm from src.Logging import getLogger, logFailure +from src.ErrorHandling import Result, Err, Ok, ErrorSeverity from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import guid_component, guid_occurrence @@ -99,7 +103,7 @@ class DynamicOccurrenceNode(GraphNode): def __init__(self, occurrence: adsk.fusion.Occurrence, isGround: bool = False, previous: GraphNode | None = None): super().__init__(occurrence) self.isGround = isGround - self.name = occurrence.name + self.name = occurrence.name # type: ignore def print(self) -> None: print(f"\n\t-------{self.data.name}-------") @@ -188,26 +192,27 @@ class SimulationEdge(GraphEdge): ... class JointParser: grounded: adsk.fusion.Occurrence + # NOTE This function cannot under the value-based error handling system, since it's an __init__ function @logFailure def __init__(self, design: adsk.fusion.Design) -> None: - # Create hierarchy with just joint assembly - # - Assembly - # - Grounded - # - Axis 1 - # - Axis 2 - # - Axis 3 - - # 1. Find all Dynamic joint items to isolate [o] - # 2. Find the grounded component [x] (possible - not optimized) - # 3. Populate tree with all items from each set of joints [x] (done with grounding) - # - 3. a) Each Child element with no joints [x] - # - 3. b) Each Rigid Joint Connection [x] - # 4. Link Joint trees by discovery from root [x] - # 5. Record which trees have no children for creating end effectors [x] (next up) - this kinda already exists - - # Need to investigate creating an additional button for end effector possibly - # It might be possible to have multiple end effectors - # Total Number of final elements + """ Create hierarchy with just joint assembly + - Assembly + - Grounded + - Axis 1 + - Axis 2 + - Axis 3 + + 1. Find all Dynamic joint items to isolate [o] + 2. Find the grounded component [x] (possible - not optimized) + 3. Populate tree with all items from each set of joints [x] (done with grounding) + - 3. a) Each Child element with no joints [x] + - 3. b) Each Rigid Joint Connection [x] + 4. Link Joint trees by discovery from root [x] + 5. Record which trees have no children for creating end effectors [x] (next up) - this kinda already exists + + Need to investigate creating an additional button for end effector possibly + It might be possible to have multiple end effectors + Total Number of final elements""" self.current = None self.previousJoint = None @@ -237,7 +242,11 @@ def __init__(self, design: adsk.fusion.Design) -> None: self.__getAllJoints() # dynamic joint node for grounded components and static components - rootNode = self._populateNode(self.grounded, None, None, is_ground=True) + populate_node_result = self._populateNode(self.grounded, None, None, is_ground=True) + if populate_node_result.is_err(): # We need the value to proceed + raise RuntimeWarning(populate_node_result.unwrap_err()[0]) + + rootNode = populate_node_result.unwrap() self.groundSimNode = SimulationNode(rootNode, None, grounded=True) self.simulationNodesRef["GROUND"] = self.groundSimNode @@ -253,30 +262,29 @@ def __init__(self, design: adsk.fusion.Design) -> None: # self.groundSimNode.printLink() - @logFailure - def __getAllJoints(self) -> None: + def __getAllJoints(self) -> Result[None]: for joint in list(self.design.rootComponent.allJoints) + list(self.design.rootComponent.allAsBuiltJoints): if joint and joint.occurrenceOne and joint.occurrenceTwo: occurrenceOne = joint.occurrenceOne occurrenceTwo = joint.occurrenceTwo else: - return + # Non-fatal since it's recovered in the next two statements + _ = Err("Found joint without two occurences", ErrorSeverity.Warning) if occurrenceOne is None: - try: - occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext - except: - pass + if joint.geometryOrOriginOne.entityOne.assemblyContext is None + return Err("occurrenceOne and entityOne's assembly context are None", ErrorSeverity.Fatal) + occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext if occurrenceTwo is None: - try: - occurrenceTwo = joint.geometryOrOriginTwo.entityOne.assemblyContext - except: - pass + if joint.geometryOrOriginTwo.entityTwo.assemblyContext is None + return Err("occurrenceOne and entityTwo's assembly context are None", ErrorSeverity.Fatal) + occurrenceTwo = joint.geometryOrOriginTwo.entityTwo.assemblyContext oneEntityToken = "" twoEntityToken = "" + # TODO: Fix change to if statement with Result returning try: oneEntityToken = occurrenceOne.entityToken except: @@ -293,124 +301,138 @@ def __getAllJoints(self) -> None: 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: - logger.error( - f"Occurrences that connect joints could not be found\n\t1: {occurrenceOne}\n\t2: {occurrenceTwo}" - ) - return + return 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) + return Ok(None) - def _linkAllAxis(self) -> None: + def _linkAllAxis(self) -> Result[None]: # looks through each simulation nood starting with ground and orders them using edges # self.groundSimNode is ground - self._recurseLink(self.groundSimNode) + return self._recurseLink(self.groundSimNode) - def _recurseLink(self, simNode: SimulationNode) -> None: + def _recurseLink(self, simNode: SimulationNode) -> Result[None]: connectedAxisNodes = [ self.simulationNodesRef.get(componentKeys, None) for componentKeys in simNode.data.getConnectedAxisTokens() ] + if any([node is None for node in connectedAxisNodes]): + return Err(f"Found None Connected Access Node", ErrorSeverity.Fatal) + for connectedAxis in connectedAxisNodes: # connected is the occurrence if connectedAxis is not None: edge = SimulationEdge(JointRelationship.GROUND, connectedAxis) simNode.edges.append(edge) - self._recurseLink(connectedAxis) - def _lookForGroundedJoints(self) -> None: - grounded_token = self.grounded.entityToken + recurse_result = self._recurseLink(connectedAxis) + if recurse_result.is_err() and recurse_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return recurse_result + return Ok(None) + + def _lookForGroundedJoints(self) -> Result[None]: + # grounded_token = self.grounded.entityToken rootDynamicJoint = self.groundSimNode.data + if rootDynamicJoint is None: + return Err("Found None rootDynamicJoint", ErrorSeverity.Fatal) for grounded_connect in self.groundedConnections: self.currentTraversal = dict() - self._populateNode( + _ = self._populateNode( grounded_connect, rootDynamicJoint, OccurrenceRelationship.CONNECTION, is_ground=False, ) + return Ok(None) def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint) -> None: occ = self.design.findEntityByToken(occ_token)[0] - if occ is None: return self.currentTraversal = dict() - rootNode = self._populateNode(occ, None, None) + populate_node_result = self._populateNode(occ, None, None) + if populate_node_result.is_err(): # We need the value to proceed + return populate_node_result + rootNode = populate_node_result.unwrap() if rootNode is not None: axisNode = SimulationNode(rootNode, joint) self.simulationNodesRef[occ_token] = axisNode + # TODO: Verify that this works after the Result-refactor :skull: def _populateNode( self, occ: adsk.fusion.Occurrence, prev: DynamicOccurrenceNode | None, relationship: OccurrenceRelationship | None, is_ground: bool = False, - ) -> DynamicOccurrenceNode | None: + ) -> Result[DynamicOccurrenceNode | None]: if occ.isGrounded and not is_ground: - return None + return Ok(None) elif (relationship == OccurrenceRelationship.NEXT) and (prev is not None): node = DynamicOccurrenceNode(occ) edge = DynamicEdge(relationship, node) prev.edges.append(edge) - return None + return Ok(None) elif ((occ.entityToken in self.dynamicJoints.keys()) and (prev is not None)) or self.currentTraversal.get( occ.entityToken ) is not None: - return None + return Ok(None) node = DynamicOccurrenceNode(occ) self.currentTraversal[occ.entityToken] = True for occurrence in occ.childOccurrences: - self._populateNode(occurrence, node, OccurrenceRelationship.TRANSFORM, is_ground=is_ground) + populate_result = self._populateNode(occurrence, node, OccurrenceRelationship.TRANSFORM, is_ground=is_ground) + if populate_result.is_err() and populate_result.unwrap_err()[1] == ErrorSeverity.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 - try: - 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: - self._populateNode( - connection, - node, - (OccurrenceRelationship.CONNECTION if rigid else OccurrenceRelationship.NEXT), - is_ground=is_ground, - ) + 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: - continue - except: - pass - + 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_err() and populate_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return populate_result + 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 prev is not None: edge = DynamicEdge(relationship, node) prev.edges.append(edge) self.currentTraversal[occ.entityToken] = node - return node + return Ok(node) def searchForGrounded( @@ -422,14 +444,13 @@ def searchForGrounded( occ (adsk.fusion.Occurrence): start point Returns: - Union(adsk.fusion.Occurrence, None): Either a grounded part or nothing + adsk.fusion.Occurrence | None: Either a grounded part or nothing """ if occ.objectType == "adsk::fusion::Component": # this makes it possible to search an object twice (unoptimized) collection = occ.allOccurrences # components cannot be grounded technically - else: # Object is an occurrence if occ.isGrounded: return occ @@ -448,13 +469,13 @@ def searchForGrounded( # ________________________ Build implementation ______________________ # -@logFailure def BuildJointPartHierarchy( design: adsk.fusion.Design, joints: joint_pb2.Joints, options: ExporterOptions, progressDialog: PDMessage, -) -> None: +) -> Result[None]: + # This try-catch is necessary because the JointParser __init__ functon is fallible and throws a RuntimeWarning (__init__ functions cannot return values) try: progressDialog.currentMessage = f"Constructing Simulation Hierarchy" progressDialog.update() @@ -462,7 +483,9 @@ def BuildJointPartHierarchy( jointParser = JointParser(design) rootSimNode = jointParser.groundSimNode - populateJoint(rootSimNode, joints, progressDialog) + populate_joint_result = populateJoint(rootSimNode, joints, progressDialog) + if populate_joint_result.is_err() and populate_joint_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return populate_joint_result # 1. Get Node # 2. Get Transform of current Node @@ -477,11 +500,14 @@ def BuildJointPartHierarchy( if progressDialog.wasCancelled(): raise RuntimeError("User canceled export") + return Ok(None) + + # I'm fairly certain bubbling this back up is the way to go except Warning: - pass + return Err("Instantiation of the JointParser failed, likely due to a lack of a grounded component in the assembly", ErrorSeverity.Fatal) -def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDialog: PDMessage) -> None: +def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDialog: PDMessage) -> Result[None]: if progressDialog.wasCancelled(): raise RuntimeError("User canceled export") @@ -494,8 +520,7 @@ def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDia progressDialog.update() if not proto_joint: - logger.error(f"Could not find protobuf joint for {simNode.name}") - return + return Err(f"Could not find protobuf joint for {simNode.name}", ErrorSeverity.Fatal) root = types_pb2.Node() @@ -506,7 +531,10 @@ def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDia # next in line to be populated for edge in simNode.edges: - populateJoint(cast(SimulationNode, edge.node), joints, progressDialog) + populate_joint_result = populateJoint(cast(SimulationNode, edge.node), joints, progressDialog) + if populate_joint_result.is_err() and populate_joint_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return populate_joint_result + return Ok(None) def createTreeParts( @@ -519,26 +547,30 @@ def createTreeParts( raise RuntimeError("User canceled export") # if it's the next part just exit early for our own sanity + # This shouldn't be fatal nor even an error if relationship == OccurrenceRelationship.NEXT or dynNode.data.isLightBulbOn == False: return # set the occurrence / component id to reference the part - try: - objectType = dynNode.data.objectType - except: + # Fine way to use try-excepts in this language + if dynNode.data.objectType is None: + _ = Err("Found None object type", ErrorSeverity.Warning) objectType = "" - + else: + objectType = dynNode.data.objectType + if objectType == "adsk::fusion::Occurrence": node.value = guid_occurrence(dynNode.data) elif objectType == "adsk::fusion::Component": node.value = guid_component(dynNode.data) else: - try: - node.value = dynNode.data.entityToken - except RuntimeError: + if dynNode.data.entityToken is None: + _ = Err("Found None EntityToken", ErrorSeverity.Warning) # type: ignore node.value = dynNode.data.name - + else: + node.value = dynNode.data.entityToken + # possibly add additional information for the type of connection made # recurse and add all children connections for edge in dynNode.edges: From 1b2f65f0dc0d52268dfd5438161f3361e763a8cb Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 26 Jun 2025 16:19:02 -0700 Subject: [PATCH 30/68] feat: add value-based error handling to joints.py --- .../src/Parser/SynthesisParser/Joints.py | 78 ++++++++++++++----- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index 0577da7a41..d3f926de0e 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -29,6 +29,7 @@ import adsk.core import adsk.fusion +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import getLogger from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage @@ -76,20 +77,28 @@ def populateJoints( progressDialog: PDMessage, options: ExporterOptions, assembly: assembly_pb2.Assembly, -) -> None: - fill_info(joints, None) +) -> Result[None]: + info_result = fill_info(joints, None) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result # This is for creating all of the Joint Definition objects # So we need to iterate through the joints and construct them and add them to the map if not options.joints: - return + return Ok(None) # Add the grounded joints object - TODO: rename some of the protobuf stuff for the love of god joint_definition_ground = joints.joint_definitions["grounded"] - construct_info("grounded", joint_definition_ground) + info_result = construct_info("grounded", joint_definition_ground) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + joint_instance_ground = joints.joint_instances["grounded"] - construct_info("grounded", joint_instance_ground) + info_result = construct_info("grounded", joint_instance_ground) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + joint_instance_ground.joint_reference = joint_definition_ground.info.GUID @@ -106,7 +115,8 @@ def populateJoints( if joint.jointMotion.jointType in AcceptedJointTypes: try: - # Fusion has no instances of joints but lets roll with it anyway + # Fusion has no instances of joints but lets roll with it anyway + # ^^^ This majorly confuses me ^^^ # progressDialog.message = f"Exporting Joint configuration {joint.name}" progressDialog.addJoint(joint.name) @@ -122,7 +132,11 @@ def populateJoints( if parse_joints.jointToken == joint.entityToken: guid = str(uuid.uuid4()) signal = signals.signal_map[guid] - construct_info(joint.name, signal, GUID=guid) + + info_result = construct_info(joint.name, signal, GUID=guid) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + signal.io = signal_pb2.IOType.OUTPUT # really could just map the enum to a friggin string @@ -133,7 +147,12 @@ def populateJoints( signal.device_type = signal_pb2.DeviceType.PWM motor = joints.motor_definitions[joint.entityToken] - fill_info(motor, joint) + + info_result = fill_info(motor, joint) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + + simple_motor = motor.simple_motor simple_motor.stall_torque = parse_joints.force simple_motor.max_velocity = parse_joints.speed @@ -144,19 +163,24 @@ def populateJoints( # else: # signals.signal_map.remove(guid) - _addJointInstance(joint, joint_instance, joint_definition, signals, options) + joint_result = _addJointInstance(joint, joint_instance, joint_definition, signals, options) + if joint_result.is_err() and joint_result.severity == ErrorSeverity.Fatal: + return joint_result # adds information for joint motion and limits _motionFromJoint(joint.jointMotion, joint_definition) except: - logger.error("Failed:\n{}".format(traceback.format_exc())) + # TODO: Figure out how to construct and return this (ie, what actually breaks in this try block) + _ = Err("Failed:\n{}".format(traceback.format_exc()), ErrorSeverity.Fatal) continue + return Ok(None) -def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> None: - fill_info(joint_definition, joint) - +def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> Result[None]: + info_result = fill_info(joint_definition, joint) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result jointPivotTranslation = _jointOrigin(joint) if jointPivotTranslation: @@ -168,10 +192,13 @@ def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> No joint_definition.origin.y = 0.0 joint_definition.origin.z = 0.0 - logger.error(f"Cannot find joint origin on joint {joint.name}") + # TODO: We definitely could make this fatal, figure out if we should + _ = Err(f"Cannot find joint origin on joint {joint.name}", ErrorSeverity.Warning) joint_definition.break_magnitude = 0.0 + return Ok(None) + def _addJointInstance( joint: adsk.fusion.Joint, @@ -179,8 +206,11 @@ def _addJointInstance( joint_definition: joint_pb2.Joint, signals: signal_pb2.Signals, options: ExporterOptions, -) -> None: - fill_info(joint_instance, joint) +) -> Result[None]: + info_result = fill_info(joint_instance, joint) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + # because there is only one and we are using the token - should be the same joint_instance.joint_reference = joint_instance.info.GUID @@ -221,7 +251,11 @@ def _addJointInstance( else: # if not then create it and add the signal type guid = str(uuid.uuid4()) signal = signals.signal_map[guid] - construct_info("joint_signal", signal, GUID=guid) + + info_result = construct_info("joint_signal", signal, GUID=guid) + if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + return info_result + signal.io = signal_pb2.IOType.OUTPUT joint_instance.signal_reference = signal.info.GUID @@ -232,7 +266,7 @@ def _addJointInstance( signal.device_type = signal_pb2.DeviceType.PWM else: joint_instance.signal_reference = "" - + return Ok(None) def _addRigidGroup(joint: adsk.fusion.Joint, assembly: assembly_pb2.Assembly) -> None: if joint.jointMotion.jointType != 0 or not ( @@ -422,7 +456,7 @@ def notImplementedPlaceholder(*argv: Any) -> None: ... def _searchForGrounded( occ: adsk.fusion.Occurrence, -) -> Union[adsk.fusion.Occurrence, None]: +) -> adsk.fusion.Occurrence | None: """Search for a grounded component or occurrence in the assembly Args: @@ -512,7 +546,7 @@ def createJointGraph( _wheels: list[Wheel], jointTree: types_pb2.GraphContainer, progressDialog: PDMessage, -) -> None: +) -> Result[None]: # progressDialog.message = f"Building Joint Graph Map from given joints" progressDialog.currentMessage = f"Building Joint Graph Map from given joints" @@ -542,11 +576,13 @@ def createJointGraph( elif nodeMap[suppliedJoint.parent.value] is not None and nodeMap[suppliedJoint.jointToken] is not None: nodeMap[str(suppliedJoint.parent)].children.append(nodeMap[suppliedJoint.jointToken]) else: - logger.error(f"Cannot construct hierarhcy because of detached tree at : {suppliedJoint.jointToken}") + # TODO: This might not need to be fatal + return Err(f"Cannot construct hierarchy because of detached tree at : {suppliedJoint.jointToken}", ErrorSeverity.Fatal) for node in nodeMap.values(): # append everything at top level to isolate kinematics jointTree.nodes.append(node) + return Ok(None) def addWheelsToGraph(wheels: list[Wheel], rootNode: types_pb2.Node, jointTree: types_pb2.GraphContainer) -> None: From ea75edfeec1f6657d3576de52a7527c056c2d851 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 27 Jun 2025 09:04:09 -0700 Subject: [PATCH 31/68] fix: finish wrapping joint function calls in err handling --- .../src/Parser/SynthesisParser/Parser.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 968b0d8738..35c261c6fb 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -118,27 +118,27 @@ def export(self) -> None: assembly_out.design_hierarchy.nodes.append(rootNode) # Problem Child - Joints.populateJoints( + handle_err_top(Joints.populateJoints( design, assembly_out.data.joints, assembly_out.data.signals, self.pdMessage, self.exporterOptions, assembly_out, - ) + )) # add condition in here for advanced joints maybe idk # should pre-process to find if there are any grounded joints at all # that or add code to existing parser to determine leftovers - Joints.createJointGraph( + handle_err_top(Joints.createJointGraph( self.exporterOptions.joints, self.exporterOptions.wheels, assembly_out.joint_hierarchy, self.pdMessage, - ) + )) - JointHierarchy.BuildJointPartHierarchy(design, assembly_out.data.joints, self.exporterOptions, self.pdMessage) + handle_err_top(JointHierarchy.BuildJointPartHierarchy(design, assembly_out.data.joints, self.exporterOptions, self.pdMessage)) # These don't have an effect, I forgot how this is suppose to work # progressDialog.message = "Taking Photo for thumbnail..." @@ -198,10 +198,10 @@ def export(self) -> None: if self.exporterOptions.compressOutput: logger.debug("Compressing file") with gzip.open(str(self.exporterOptions.fileLocation), "wb", 9) as f: - f.write(assembly_out.SerializeToString()) + _ = f.write(assembly_out.SerializeToString()) else: with open(str(self.exporterOptions.fileLocation), "wb") as f: - f.write(assembly_out.SerializeToString()) + _ = f.write(assembly_out.SerializeToString()) _ = progressDialog.hide() From 4e6261b55b2b09e5eef4266e011e71748db8dc51 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 27 Jun 2025 14:47:49 -0700 Subject: [PATCH 32/68] fix: logging bug --- exporter/SynthesisFusionAddin/Synthesis.py | 14 ++++---------- exporter/SynthesisFusionAddin/logs/.gitkeep | 0 .../SynthesisFusionAddin/src/ErrorHandling.py | 9 +++++---- .../src/Parser/SynthesisParser/JointHierarchy.py | 4 ++-- .../src/Parser/SynthesisParser/Materials.py | 4 ++-- .../src/Parser/SynthesisParser/Parser.py | 15 +++------------ .../SynthesisFusionAddin/src/UI/ConfigCommand.py | 3 +-- exporter/SynthesisFusionAddin/src/__init__.py | 1 + 8 files changed, 18 insertions(+), 32 deletions(-) delete mode 100644 exporter/SynthesisFusionAddin/logs/.gitkeep diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 109460f808..984360ee26 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -9,8 +9,8 @@ from src.Dependencies import resolveDependencies from src.Logging import logFailure, setupLogger - logger = setupLogger() +from src.ErrorHandling import Err, ErrorSeverity try: # Attempt to import required pip dependencies to verify their installation. @@ -32,17 +32,9 @@ from src import APP_NAME, DESCRIPTION, INTERNAL_ID, gm -from src.UI import ( - HUI, - Camera, - ConfigCommand, - MarkingMenu, - ShowAPSAuthCommand, - ShowWebsiteCommand, -) +from src.UI import (HUI, Camera, ConfigCommand, MarkingMenu, ShowAPSAuthCommand, ShowWebsiteCommand) from src.UI.Toolbar import Toolbar - @logFailure def run(_context: dict[str, Any]) -> None: """## Entry point to application from Fusion. @@ -51,6 +43,7 @@ def run(_context: dict[str, Any]) -> None: **context** *context* -- Fusion context to derive app and UI. """ + # Remove all items prior to start just to make sure unregister_all() @@ -70,6 +63,7 @@ def stop(_context: dict[str, Any]) -> None: Arguments: **context** *context* -- Fusion Data. """ + unregister_all() app = adsk.core.Application.get() diff --git a/exporter/SynthesisFusionAddin/logs/.gitkeep b/exporter/SynthesisFusionAddin/logs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 2d1594f879..b43d9aa56f 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -2,12 +2,14 @@ from enum import Enum from typing import Generic, TypeVar +logger = getLogger() + # NOTE # Severity refers to to the error's affect on the parser as a whole, rather than on the function itself # If an error is non-fatal to the function that generated it, it should be declared but not return, which prints it to the screen class ErrorSeverity(Enum): - Fatal = 1 - Warning = 2 + Fatal = 50 # Critical Error + Warning = 30 # Warning T = TypeVar('T') @@ -49,6 +51,5 @@ def __repr__(self): return f"Err({self.message})" def write_error(self) -> None: - logger = getLogger() # Figure out how to integrate severity with the logger - logger.log(1, self.message) + logger.log(self.severity.value, self.message) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index b0ff7e5bbe..8c73c07815 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -272,12 +272,12 @@ def __getAllJoints(self) -> Result[None]: _ = Err("Found joint without two occurences", ErrorSeverity.Warning) if occurrenceOne is None: - if joint.geometryOrOriginOne.entityOne.assemblyContext is None + if joint.geometryOrOriginOne.entityOne.assemblyContext is None: return Err("occurrenceOne and entityOne's assembly context are None", ErrorSeverity.Fatal) occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext if occurrenceTwo is None: - if joint.geometryOrOriginTwo.entityTwo.assemblyContext is None + if joint.geometryOrOriginTwo.entityTwo.assemblyContext is None: return Err("occurrenceOne and entityTwo's assembly context are None", ErrorSeverity.Fatal) occurrenceTwo = joint.geometryOrOriginTwo.entityTwo.assemblyContext diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 8d10d79a75..e579147756 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -143,7 +143,7 @@ def getPhysicalMaterialData( missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] #ignore: type if missingProperties.__len__() > 0: - return Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) + _ = Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) """ Strength Properties @@ -153,7 +153,7 @@ def getPhysicalMaterialData( missingProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] #ignore: type if missingProperties.__len__() > 0: - return Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) + _ = Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) """ strengthProperties.thermal_treatment = materialProperties.itemById( diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 35c261c6fb..cd11e90099 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -1,24 +1,16 @@ import gzip import pathlib -from google.protobuf import message - import adsk.core import adsk.fusion -from google.protobuf.json_format import MessageToJson from src import gm from src.APS.APS import getAuth, upload_mirabuf from src.ErrorHandling import ErrorSeverity, Result -from src.Logging import getLogger, logFailure, timed from src.Parser.ExporterOptions import ExporterOptions -from src.Parser.SynthesisParser import ( - Components, - JointHierarchy, - Joints, - Materials, - PDMessage, -) +from src.Parser.SynthesisParser import (Components, JointHierarchy, Joints, Materials, PDMessage) + +from src.Logging import getLogger, logFailure, timed from src.Parser.SynthesisParser.Utilities import fill_info from src.Proto import assembly_pb2, types_pb2 from src.Types import ExportLocation, ExportMode @@ -26,7 +18,6 @@ logger = getLogger() - class Parser: def __init__(self, options: ExporterOptions): """Creates a new parser with the supplied options diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 75ac2364e2..084ccd5141 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -10,9 +10,9 @@ import adsk.core import adsk.fusion +from src.Logging import logFailure from src import APP_WEBSITE_URL, gm from src.APS.APS import getAuth, getUserInfo -from src.Logging import getLogger, logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.Parser import Parser from src.Types import SELECTABLE_JOINT_TYPES, ExportLocation, ExportMode @@ -26,7 +26,6 @@ jointConfigTab: JointConfigTab gamepieceConfigTab: GamepieceConfigTab -logger = getLogger() INPUTS_ROOT: adsk.core.CommandInputs diff --git a/exporter/SynthesisFusionAddin/src/__init__.py b/exporter/SynthesisFusionAddin/src/__init__.py index 42d1e6d391..3f761b65c0 100644 --- a/exporter/SynthesisFusionAddin/src/__init__.py +++ b/exporter/SynthesisFusionAddin/src/__init__.py @@ -5,6 +5,7 @@ from src.GlobalManager import GlobalManager from src.Util import makeDirectories + APP_NAME = "Synthesis" APP_TITLE = "Synthesis Robot Exporter" APP_WEBSITE_URL = "https://synthesis.autodesk.com/fission/" From 9ef19d66e98fdc31b6c20a85a544bfc9cbe82011 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 27 Jun 2025 15:17:10 -0700 Subject: [PATCH 33/68] feat: refactor physical properties err handling --- .../SynthesisParser/PhysicalProperties.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 9c52670d21..380995cbbe 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -16,29 +16,33 @@ """ -from typing import Union - import adsk +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import logFailure from src.Proto import types_pb2 -@logFailure def GetPhysicalProperties( - fusionObject: Union[adsk.fusion.BRepBody, adsk.fusion.Occurrence, adsk.fusion.Component], + fusionObject: adsk.fusion.BRepBody | adsk.fusion.Occurrence | adsk.fusion.Component, physicalProperties: types_pb2.PhysicalProperties, level: int = 1, -) -> None: +) -> Result[None]: """Will populate a physical properties section of an exported file Args: - fusionObject (Union[adsk.fusion.BRepBody, adsk.fusion.Occurrence, adsk.fusion.Component]): The base fusion object + fusionObject (adsk.fusion.BRepBody | adsk.fusion.Occurrence, adsk.fusion.Component): The base fusion object physicalProperties (any): Unity Joint object for now level (int): Level of accurracy """ physical = fusionObject.getPhysicalProperties(level) + missing_properties = [prop is None for prop in physical] + if physical is None: + return Err("Physical properties object is None", ErrorSeverity.Warning) + if any(missing_properties.): + _ = Err(f"Missing some physical properties", ErrorSeverity.Warning) + physicalProperties.density = physical.density physicalProperties.mass = physical.mass physicalProperties.volume = physical.volume @@ -51,3 +55,7 @@ def GetPhysicalProperties( _com.x = com.x _com.y = com.y _com.z = com.z + else: + _ = Err("com is None", ErrorSeverity.Warning) + + return Ok(None) From d6ac04f3e536c3be7b3e7949d74f7149b7bc5268 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Fri, 27 Jun 2025 15:19:12 -0700 Subject: [PATCH 34/68] chore: change union to | --- .../src/Parser/SynthesisParser/RigidGroup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py index 828f29626a..f49992affc 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py @@ -12,8 +12,6 @@ - Success """ -from typing import Union - import adsk.core import adsk.fusion @@ -26,7 +24,7 @@ # Should be removed later @logFailure def ExportRigidGroups( - fus_occ: Union[adsk.fusion.Occurrence, adsk.fusion.Component], + fus_occ: adsk.fusion.Occurrence | adsk.fusion.Component, hel_occ: assembly_pb2.Occurrence, # type: ignore[name-defined] ) -> None: """Takes a Fusion and Protobuf Occurrence and will assign Rigidbody data per the occurrence if any exist and are not surpressed. From d4141baf8ec1ad7f759981eeb87d93f5d8586fd7 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 08:26:02 -0700 Subject: [PATCH 35/68] chore: format files --- exporter/SynthesisFusionAddin/Synthesis.py | 5 +- .../SynthesisFusionAddin/src/ErrorHandling.py | 21 ++- .../src/Parser/SynthesisParser/Components.py | 20 +-- .../Parser/SynthesisParser/JointHierarchy.py | 64 ++++++---- .../src/Parser/SynthesisParser/Joints.py | 9 +- .../src/Parser/SynthesisParser/Materials.py | 8 +- .../src/Parser/SynthesisParser/Parser.py | 120 ++++++++++-------- .../SynthesisParser/PhysicalProperties.py | 9 +- .../src/Parser/SynthesisParser/Utilities.py | 9 +- 9 files changed, 153 insertions(+), 112 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 984360ee26..e8940e669a 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -9,6 +9,7 @@ from src.Dependencies import resolveDependencies from src.Logging import logFailure, setupLogger + logger = setupLogger() from src.ErrorHandling import Err, ErrorSeverity @@ -32,9 +33,10 @@ from src import APP_NAME, DESCRIPTION, INTERNAL_ID, gm -from src.UI import (HUI, Camera, ConfigCommand, MarkingMenu, ShowAPSAuthCommand, ShowWebsiteCommand) +from src.UI import HUI, Camera, ConfigCommand, MarkingMenu, ShowAPSAuthCommand, ShowWebsiteCommand from src.UI.Toolbar import Toolbar + @logFailure def run(_context: dict[str, Any]) -> None: """## Entry point to application from Fusion. @@ -43,7 +45,6 @@ def run(_context: dict[str, Any]) -> None: **context** *context* -- Fusion context to derive app and UI. """ - # Remove all items prior to start just to make sure unregister_all() diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index b43d9aa56f..c8059c4a36 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -4,14 +4,17 @@ logger = getLogger() + # NOTE # Severity refers to to the error's affect on the parser as a whole, rather than on the function itself # If an error is non-fatal to the function that generated it, it should be declared but not return, which prints it to the screen class ErrorSeverity(Enum): - Fatal = 50 # Critical Error - Warning = 30 # Warning + Fatal = 50 # Critical Error + Warning = 30 # Warning + + +T = TypeVar("T") -T = TypeVar('T') class Result(Generic[T]): def is_ok(self) -> bool: @@ -22,25 +25,29 @@ def is_err(self) -> bool: def unwrap(self) -> T: if self.is_ok(): - return self.value # type: ignore - raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore + return self.value # type: ignore + raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore def unwrap_err(self) -> tuple[str, ErrorSeverity]: if self.is_err(): - return (self.message, self.severity) # type: ignore - raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore + return (self.message, self.severity) # type: ignore + raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore + class Ok(Result[T]): value: T + def __init__(self, value: T): self.value = value def __repr__(self): return f"Ok({self.value})" + class Err(Result[T]): message: str severity: ErrorSeverity + def __init__(self, message: str, severity: ErrorSeverity): self.message = message self.severity = severity diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index fd4add0bb2..80bbd123c7 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -16,6 +16,7 @@ from src.Proto import assembly_pb2, joint_pb2, material_pb2, types_pb2 from src.Types import ExportMode + # TODO: Impelement Material overrides def MapAllComponents( design: adsk.fusion.Design, @@ -36,14 +37,12 @@ def MapAllComponents( if fill_info_result.is_err(): return fill_info_result - partDefinition = partsData.part_definitions[comp_ref] fill_info_result = fill_info(partDefinition, component, comp_ref) if fill_info_result.is_err(): return fill_info_result - PhysicalProperties.GetPhysicalProperties(component, partDefinition.physical_data) partDefinition.dynamic = options.exportMode != ExportMode.FIELD @@ -68,7 +67,6 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: return parse_result - appearance_key = "{}_{}".format(body.appearance.name, body.appearance.id) # this should be appearance if appearance_key in materials.appearances: @@ -86,7 +84,6 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non return process_result - def ParseComponentRoot( component: adsk.fusion.Component, progressDialog: PDMessage, @@ -117,7 +114,9 @@ def ParseComponentRoot( if occur.isLightBulbOn: child_node = types_pb2.Node() - parse_child_result = parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) + parse_child_result = parseChildOccurrence( + occur, progressDialog, options, partsData, material_map, child_node + ) if parse_child_result.is_err(): return parse_child_result @@ -158,7 +157,8 @@ def parseChildOccurrence( try: part.appearance = "{}_{}".format(occurrence.appearance.name, occurrence.appearance.id) except: - _ = Err("Failed to format part appearance", ErrorSeverity.Warning); # ignore: type + _ = Err("Failed to format part appearance", ErrorSeverity.Warning) + # ignore: type part.appearance = "default" # TODO: Add phyical_material parser @@ -194,8 +194,10 @@ def parseChildOccurrence( if occur.isLightBulbOn: child_node = types_pb2.Node() - parse_child_result = parseChildOccurrence(occur, progressDialog, options, partsData, material_map, child_node) - if parse_child_result.is_err(): + parse_child_result = parseChildOccurrence( + occur, progressDialog, options, partsData, material_map, child_node + ) + if parse_child_result.is_err(): return parse_child_result node.children.append(child_node) @@ -252,12 +254,10 @@ def ParseMesh( if mesh is None: return Err("Component Mesh was None", ErrorSeverity.Fatal) - fill_info_result = fill_info(trimesh, meshBody) if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return fill_info_result - trimesh.has_volume = True plainmesh_out = trimesh.mesh diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 8c73c07815..19adac21e0 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -103,7 +103,7 @@ class DynamicOccurrenceNode(GraphNode): def __init__(self, occurrence: adsk.fusion.Occurrence, isGround: bool = False, previous: GraphNode | None = None): super().__init__(occurrence) self.isGround = isGround - self.name = occurrence.name # type: ignore + self.name = occurrence.name # type: ignore def print(self) -> None: print(f"\n\t-------{self.data.name}-------") @@ -195,24 +195,24 @@ class JointParser: # NOTE This function cannot under the value-based error handling system, since it's an __init__ function @logFailure def __init__(self, design: adsk.fusion.Design) -> None: - """ Create hierarchy with just joint assembly - - Assembly - - Grounded - - Axis 1 - - Axis 2 - - Axis 3 - - 1. Find all Dynamic joint items to isolate [o] - 2. Find the grounded component [x] (possible - not optimized) - 3. Populate tree with all items from each set of joints [x] (done with grounding) - - 3. a) Each Child element with no joints [x] - - 3. b) Each Rigid Joint Connection [x] - 4. Link Joint trees by discovery from root [x] - 5. Record which trees have no children for creating end effectors [x] (next up) - this kinda already exists - - Need to investigate creating an additional button for end effector possibly - It might be possible to have multiple end effectors - Total Number of final elements""" + """Create hierarchy with just joint assembly + - Assembly + - Grounded + - Axis 1 + - Axis 2 + - Axis 3 + + 1. Find all Dynamic joint items to isolate [o] + 2. Find the grounded component [x] (possible - not optimized) + 3. Populate tree with all items from each set of joints [x] (done with grounding) + - 3. a) Each Child element with no joints [x] + - 3. b) Each Rigid Joint Connection [x] + 4. Link Joint trees by discovery from root [x] + 5. Record which trees have no children for creating end effectors [x] (next up) - this kinda already exists + + Need to investigate creating an additional button for end effector possibly + It might be possible to have multiple end effectors + Total Number of final elements""" self.current = None self.previousJoint = None @@ -243,7 +243,7 @@ def __init__(self, design: adsk.fusion.Design) -> None: # dynamic joint node for grounded components and static components populate_node_result = self._populateNode(self.grounded, None, None, is_ground=True) - if populate_node_result.is_err(): # We need the value to proceed + if populate_node_result.is_err(): # We need the value to proceed raise RuntimeWarning(populate_node_result.unwrap_err()[0]) rootNode = populate_node_result.unwrap() @@ -303,7 +303,10 @@ def __getAllJoints(self) -> Result[None]: # TODO: Check if this is fatal or not if occurrenceTwo is None and occurrenceOne is None: - return Err(f"Occurrences that connect joints could not be found\n\t1: {occurrenceOne}\n\t2: {occurrenceTwo}", ErrorSeverity.Fatal) + return 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) @@ -358,7 +361,7 @@ def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint) -> None: self.currentTraversal = dict() populate_node_result = self._populateNode(occ, None, None) - if populate_node_result.is_err(): # We need the value to proceed + if populate_node_result.is_err(): # We need the value to proceed return populate_node_result rootNode = populate_node_result.unwrap() @@ -391,7 +394,9 @@ def _populateNode( self.currentTraversal[occ.entityToken] = True for occurrence in occ.childOccurrences: - populate_result = self._populateNode(occurrence, node, OccurrenceRelationship.TRANSFORM, is_ground=is_ground) + populate_result = self._populateNode( + occurrence, node, OccurrenceRelationship.TRANSFORM, is_ground=is_ground + ) if populate_result.is_err() and populate_result.unwrap_err()[1] == ErrorSeverity.Fatal: return populate_result @@ -426,7 +431,7 @@ def _populateNode( 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 prev is not None: edge = DynamicEdge(relationship, node) prev.edges.append(edge) @@ -504,7 +509,10 @@ def BuildJointPartHierarchy( # I'm fairly certain bubbling this back up is the way to go except Warning: - return Err("Instantiation of the JointParser failed, likely due to a lack of a grounded component in the assembly", ErrorSeverity.Fatal) + return Err( + "Instantiation of the JointParser failed, likely due to a lack of a grounded component in the assembly", + ErrorSeverity.Fatal, + ) def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDialog: PDMessage) -> Result[None]: @@ -559,18 +567,18 @@ def createTreeParts( objectType = "" else: objectType = dynNode.data.objectType - + if objectType == "adsk::fusion::Occurrence": node.value = guid_occurrence(dynNode.data) elif objectType == "adsk::fusion::Component": node.value = guid_component(dynNode.data) else: if dynNode.data.entityToken is None: - _ = Err("Found None EntityToken", ErrorSeverity.Warning) # type: ignore + _ = Err("Found None EntityToken", ErrorSeverity.Warning) # type: ignore node.value = dynNode.data.name else: node.value = dynNode.data.entityToken - + # possibly add additional information for the type of connection made # recurse and add all children connections for edge in dynNode.edges: diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index d3f926de0e..d2f1469e41 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -93,13 +93,11 @@ def populateJoints( if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: return info_result - joint_instance_ground = joints.joint_instances["grounded"] info_result = construct_info("grounded", joint_instance_ground) if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: return info_result - joint_instance_ground.joint_reference = joint_definition_ground.info.GUID # Add the rest of the dynamic objects @@ -152,7 +150,6 @@ def populateJoints( if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: return info_result - simple_motor = motor.simple_motor simple_motor.stall_torque = parse_joints.force simple_motor.max_velocity = parse_joints.speed @@ -268,6 +265,7 @@ def _addJointInstance( joint_instance.signal_reference = "" return Ok(None) + 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 @@ -577,7 +575,10 @@ def createJointGraph( nodeMap[str(suppliedJoint.parent)].children.append(nodeMap[suppliedJoint.jointToken]) else: # TODO: This might not need to be fatal - return Err(f"Cannot construct hierarchy because of detached tree at : {suppliedJoint.jointToken}", ErrorSeverity.Fatal) + return Err( + f"Cannot construct hierarchy because of detached tree at : {suppliedJoint.jointToken}", + ErrorSeverity.Fatal, + ) for node in nodeMap.values(): # append everything at top level to isolate kinematics diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index e579147756..ba626307fc 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -52,8 +52,6 @@ def MapAllPhysicalMaterials( return Ok(None) - - def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions) -> Result[None]: construct_info_result = construct_info("default", physicalMaterial) if construct_info_result.is_err(): @@ -73,6 +71,7 @@ def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: return Ok(None) + def getPhysicalMaterialData( fusionMaterial: adsk.core.Material, physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions ) -> Result[None]: @@ -141,7 +140,7 @@ def getPhysicalMaterialData( mechanicalProperties.density = materialProperties.itemById("structural_Density").value mechanicalProperties.damping_coefficient = materialProperties.itemById("structural_Damping_coefficient").value - missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] #ignore: type + missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] # ignore: type if missingProperties.__len__() > 0: _ = Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) @@ -151,7 +150,7 @@ def getPhysicalMaterialData( strengthProperties.yield_strength = materialProperties.itemById("structural_Minimum_yield_stress").value strengthProperties.tensile_strength = materialProperties.itemById("structural_Minimum_tensile_strength").value - missingProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] #ignore: type + missingProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] # ignore: type if missingProperties.__len__() > 0: _ = Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) @@ -221,6 +220,7 @@ def setDefaultAppearance(appearance: material_pb2.Appearance) -> Result[None]: return Ok(None) + def getMaterialAppearance( fusionAppearance: adsk.core.Appearance, options: ExporterOptions, diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index cd11e90099..de2e358cc5 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -8,7 +8,7 @@ from src.APS.APS import getAuth, upload_mirabuf from src.ErrorHandling import ErrorSeverity, Result from src.Parser.ExporterOptions import ExporterOptions -from src.Parser.SynthesisParser import (Components, JointHierarchy, Joints, Materials, PDMessage) +from src.Parser.SynthesisParser import Components, JointHierarchy, Joints, Materials, PDMessage from src.Logging import getLogger, logFailure, timed from src.Parser.SynthesisParser.Utilities import fill_info @@ -18,6 +18,7 @@ logger = getLogger() + class Parser: def __init__(self, options: ExporterOptions): """Creates a new parser with the supplied options @@ -38,11 +39,13 @@ def export(self) -> None: return assembly_out = assembly_pb2.Assembly() - handle_err_top(fill_info( - assembly_out, - design.rootComponent, - override_guid=design.parentDocument.name, - )) + handle_err_top( + fill_info( + assembly_out, + design.rootComponent, + override_guid=design.parentDocument.name, + ) + ) # set int to 0 in dropdown selection for dynamic assembly_out.dynamic = self.exporterOptions.exportMode == ExportMode.ROBOT @@ -71,65 +74,81 @@ def export(self) -> None: progressDialog, ) - handle_err_top(Materials.MapAllAppearances( - design.appearances, - assembly_out.data.materials, - self.exporterOptions, - self.pdMessage, - )) - - handle_err_top(Materials.MapAllPhysicalMaterials( - design.materials, - assembly_out.data.materials, - self.exporterOptions, - self.pdMessage, - )) - - handle_err_top(Components.MapAllComponents( - design, - self.exporterOptions, - self.pdMessage, - assembly_out.data.parts, - assembly_out.data.materials, - )) + handle_err_top( + Materials.MapAllAppearances( + design.appearances, + assembly_out.data.materials, + self.exporterOptions, + self.pdMessage, + ) + ) + + handle_err_top( + Materials.MapAllPhysicalMaterials( + design.materials, + assembly_out.data.materials, + self.exporterOptions, + self.pdMessage, + ) + ) + + handle_err_top( + Components.MapAllComponents( + design, + self.exporterOptions, + self.pdMessage, + assembly_out.data.parts, + assembly_out.data.materials, + ) + ) rootNode = types_pb2.Node() - handle_err_top(Components.ParseComponentRoot( - design.rootComponent, - self.pdMessage, - self.exporterOptions, - assembly_out.data.parts, - assembly_out.data.materials.appearances, - rootNode, - )) + handle_err_top( + Components.ParseComponentRoot( + design.rootComponent, + self.pdMessage, + self.exporterOptions, + assembly_out.data.parts, + assembly_out.data.materials.appearances, + rootNode, + ) + ) Components.MapRigidGroups(design.rootComponent, assembly_out.data.joints) assembly_out.design_hierarchy.nodes.append(rootNode) # Problem Child - handle_err_top(Joints.populateJoints( - design, - assembly_out.data.joints, - assembly_out.data.signals, - self.pdMessage, - self.exporterOptions, - assembly_out, - )) + handle_err_top( + Joints.populateJoints( + design, + assembly_out.data.joints, + assembly_out.data.signals, + self.pdMessage, + self.exporterOptions, + assembly_out, + ) + ) # add condition in here for advanced joints maybe idk # should pre-process to find if there are any grounded joints at all # that or add code to existing parser to determine leftovers - handle_err_top(Joints.createJointGraph( - self.exporterOptions.joints, - self.exporterOptions.wheels, - assembly_out.joint_hierarchy, - self.pdMessage, - )) + handle_err_top( + Joints.createJointGraph( + self.exporterOptions.joints, + self.exporterOptions.wheels, + assembly_out.joint_hierarchy, + self.pdMessage, + ) + ) - handle_err_top(JointHierarchy.BuildJointPartHierarchy(design, assembly_out.data.joints, self.exporterOptions, self.pdMessage)) + handle_err_top( + JointHierarchy.BuildJointPartHierarchy( + design, assembly_out.data.joints, self.exporterOptions, self.pdMessage + ) + ) # These don't have an effect, I forgot how this is suppose to work # progressDialog.message = "Taking Photo for thumbnail..." @@ -244,6 +263,7 @@ def export(self) -> None: logger.debug(debug_output.strip()) + def handle_err_top[T](err: Result[T]): if err.is_err(): message, severity = err.unwrap_err() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 380995cbbe..d71e398d4e 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -36,12 +36,13 @@ def GetPhysicalProperties( level (int): Level of accurracy """ physical = fusionObject.getPhysicalProperties(level) - - missing_properties = [prop is None for prop in physical] if physical is None: return Err("Physical properties object is None", ErrorSeverity.Warning) - if any(missing_properties.): - _ = Err(f"Missing some physical properties", ErrorSeverity.Warning) + + missing_properties_bools = [prop is None for prop in physical] + if any(prop for prop, i in missing_properties_bools): + missing_properties: list[Unknown] = [physics[i] for i, prop in enumerate(missing_properties) if prop] + _ = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) physicalProperties.density = physical.density physicalProperties.mass = physical.mass diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index 2e45497372..4db87edec4 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -20,7 +20,11 @@ def guid_none(_: None) -> str: return str(uuid.uuid4()) -def fill_info(proto_obj: assembly_pb2.Assembly | material_pb2.Materials, fus_object: adsk.core.Base, override_guid: str | None = None) -> Result[None]: +def fill_info( + proto_obj: assembly_pb2.Assembly | material_pb2.Materials, + fus_object: adsk.core.Base, + override_guid: str | None = None, +) -> Result[None]: return construct_info("", proto_obj, fus_object=fus_object, GUID=override_guid) @@ -66,8 +70,6 @@ def construct_info( return Ok(None) - - def rad_to_deg(rad): # type: ignore """Converts radians to degrees @@ -79,6 +81,7 @@ def rad_to_deg(rad): # type: ignore """ return (rad * 180) / math.pi + def throwZero(): # type: ignore """Errors on incorrect quat values From a4592a5775d5075ef30d44e10a79cad2af0dd8c6 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 08:27:06 -0700 Subject: [PATCH 36/68] chore: format physical properties --- .../src/Parser/SynthesisParser/PhysicalProperties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index d71e398d4e..00d722a75d 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -40,7 +40,7 @@ def GetPhysicalProperties( return Err("Physical properties object is None", ErrorSeverity.Warning) missing_properties_bools = [prop is None for prop in physical] - if any(prop for prop, i in missing_properties_bools): + if any(prop for prop, i in missing_properties_bools): missing_properties: list[Unknown] = [physics[i] for i, prop in enumerate(missing_properties) if prop] _ = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) From 9c6dd298ff9ad833be1865013bf86ee12f6c168a Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 08:28:57 -0700 Subject: [PATCH 37/68] chore: format with isort --- exporter/SynthesisFusionAddin/Synthesis.py | 9 ++++++++- exporter/SynthesisFusionAddin/src/ErrorHandling.py | 3 ++- .../src/Parser/SynthesisParser/Components.py | 2 +- .../src/Parser/SynthesisParser/JointHierarchy.py | 5 ++--- .../src/Parser/SynthesisParser/Materials.py | 2 +- .../src/Parser/SynthesisParser/Parser.py | 11 ++++++++--- exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py | 2 +- exporter/SynthesisFusionAddin/src/__init__.py | 1 - 8 files changed, 23 insertions(+), 12 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index e8940e669a..20a7b322db 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -33,7 +33,14 @@ from src import APP_NAME, DESCRIPTION, INTERNAL_ID, gm -from src.UI import HUI, Camera, ConfigCommand, MarkingMenu, ShowAPSAuthCommand, ShowWebsiteCommand +from src.UI import ( + HUI, + Camera, + ConfigCommand, + MarkingMenu, + ShowAPSAuthCommand, + ShowWebsiteCommand, +) from src.UI.Toolbar import Toolbar diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index c8059c4a36..2f47f873d9 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -1,7 +1,8 @@ -from .Logging import getLogger from enum import Enum from typing import Generic, TypeVar +from .Logging import getLogger + logger = getLogger() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 80bbd123c7..4c70e8268a 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -1,7 +1,7 @@ # Contains all of the logic for mapping the Components / Occurrences -from requests.models import parse_header_links import adsk.core import adsk.fusion +from requests.models import parse_header_links from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import logFailure diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 19adac21e0..70e44bc1c4 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -2,14 +2,13 @@ from logging import ERROR from typing import Any, Iterator, cast -from google.protobuf.message import Error - import adsk.core import adsk.fusion +from google.protobuf.message import Error from src import gm +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import getLogger, logFailure -from src.ErrorHandling import Result, Err, Ok, ErrorSeverity from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import guid_component, guid_occurrence diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index ba626307fc..f5a09d14f3 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -1,10 +1,10 @@ import adsk.core +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import construct_info, fill_info from src.Proto import material_pb2 -from src.ErrorHandling import ErrorSeverity, Result, Ok, Err OPACITY_RAMPING_CONSTANT = 14.0 diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index de2e358cc5..30da9147df 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -7,10 +7,15 @@ from src import gm from src.APS.APS import getAuth, upload_mirabuf from src.ErrorHandling import ErrorSeverity, Result -from src.Parser.ExporterOptions import ExporterOptions -from src.Parser.SynthesisParser import Components, JointHierarchy, Joints, Materials, PDMessage - from src.Logging import getLogger, logFailure, timed +from src.Parser.ExporterOptions import ExporterOptions +from src.Parser.SynthesisParser import ( + Components, + JointHierarchy, + Joints, + Materials, + PDMessage, +) from src.Parser.SynthesisParser.Utilities import fill_info from src.Proto import assembly_pb2, types_pb2 from src.Types import ExportLocation, ExportMode diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 084ccd5141..3d847d9dc6 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -10,9 +10,9 @@ import adsk.core import adsk.fusion -from src.Logging import logFailure from src import APP_WEBSITE_URL, gm from src.APS.APS import getAuth, getUserInfo +from src.Logging import logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.Parser import Parser from src.Types import SELECTABLE_JOINT_TYPES, ExportLocation, ExportMode diff --git a/exporter/SynthesisFusionAddin/src/__init__.py b/exporter/SynthesisFusionAddin/src/__init__.py index 3f761b65c0..42d1e6d391 100644 --- a/exporter/SynthesisFusionAddin/src/__init__.py +++ b/exporter/SynthesisFusionAddin/src/__init__.py @@ -5,7 +5,6 @@ from src.GlobalManager import GlobalManager from src.Util import makeDirectories - APP_NAME = "Synthesis" APP_TITLE = "Synthesis Robot Exporter" APP_WEBSITE_URL = "https://synthesis.autodesk.com/fission/" From 88c817e3e6a896a1ddfbf6fb0776fbabf546c4b1 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 08:48:52 -0700 Subject: [PATCH 38/68] fix: typing --- .../SynthesisFusionAddin/src/ErrorHandling.py | 4 ++-- .../src/Parser/SynthesisParser/Components.py | 19 +++++++++++++------ .../Parser/SynthesisParser/JointHierarchy.py | 17 +++++++++++------ .../src/Parser/SynthesisParser/Joints.py | 18 +++++++++--------- .../src/Parser/SynthesisParser/Materials.py | 4 ++-- .../src/Parser/SynthesisParser/Parser.py | 3 ++- .../SynthesisParser/PhysicalProperties.py | 7 ++++--- 7 files changed, 43 insertions(+), 29 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 2f47f873d9..667669330f 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -41,7 +41,7 @@ class Ok(Result[T]): def __init__(self, value: T): self.value = value - def __repr__(self): + def __repr__(self) -> str: return f"Ok({self.value})" @@ -55,7 +55,7 @@ def __init__(self, message: str, severity: ErrorSeverity): self.write_error() - def __repr__(self): + def __repr__(self) -> str: return f"Err({self.message})" def write_error(self) -> None: diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 4c70e8268a..befe5cbd6e 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -1,4 +1,5 @@ # Contains all of the logic for mapping the Components / Occurrences +from platform import python_build import adsk.core import adsk.fusion from requests.models import parse_header_links @@ -43,7 +44,9 @@ def MapAllComponents( if fill_info_result.is_err(): return fill_info_result - PhysicalProperties.GetPhysicalProperties(component, partDefinition.physical_data) + physical_properties_result = PhysicalProperties.GetPhysicalProperties(component, partDefinition.physical_data) + if physical_properties_result.is_err() and physical_properties_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return physical_properties_result partDefinition.dynamic = options.exportMode != ExportMode.FIELD @@ -60,11 +63,11 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non if isinstance(body, adsk.fusion.BRepBody): parse_result = ParseBRep(body, options, part_body.triangle_mesh) - if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: + if parse_result.is_err() and parse_result.unwrap_err()[1] == ErrorSeverity.Fatal: return parse_result else: parse_result = ParseMesh(body, options, part_body.triangle_mesh) - if parse_result.is_err() and parse_result.unwrap_err()[0] == ErrorSeverity.Fatal: + if parse_result.is_err() and parse_result.unwrap_err()[1] == ErrorSeverity.Fatal: return parse_result appearance_key = "{}_{}".format(body.appearance.name, body.appearance.id) @@ -74,15 +77,19 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non else: part_body.appearance_override = "default" + return Ok(None) + for body in component.bRepBodies: process_result = processBody(body) - if process_result.is_err() and process_result.unwrap_err()[0] == ErrorSeverity.Fatal: + if process_result.is_err() and process_result.unwrap_err()[1] == ErrorSeverity.Fatal: return process_result for body in component.meshBodies: process_result = processBody(body) - if process_result.is_err() and process_result.unwrap_err()[0] == ErrorSeverity.Fatal: + if process_result.is_err() and process_result.unwrap_err()[1] == ErrorSeverity.Fatal: return process_result + return Ok(None) + def ParseComponentRoot( component: adsk.fusion.Component, @@ -146,7 +153,7 @@ def parseChildOccurrence( node.value = mapConstant fill_info_result = fill_info(part, occurrence, mapConstant) - if fill_info_result.is_err() and fill_info_result.unwrap_err() == ErrorSeverity.Fatal: + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return fill_info_result collision_attr = occurrence.attributes.itemByName("synthesis", "collision_off") diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 70e44bc1c4..31b4642a51 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -102,7 +102,7 @@ class DynamicOccurrenceNode(GraphNode): def __init__(self, occurrence: adsk.fusion.Occurrence, isGround: bool = False, previous: GraphNode | None = None): super().__init__(occurrence) self.isGround = isGround - self.name = occurrence.name # type: ignore + self.name = occurrence.name def print(self) -> None: print(f"\n\t-------{self.data.name}-------") @@ -255,7 +255,9 @@ def __init__(self, design: adsk.fusion.Design) -> None: # creates the axis elements - adds all elements to axisNodes for key, value in self.dynamicJoints.items(): - self._populateAxis(key, value) + populate_axis_result = self._populateAxis(key, value) + if populate_axis_result.is_err(): + raise RuntimeError(populate_axis_result.unwrap_err()[0]) self._linkAllAxis() @@ -352,22 +354,25 @@ def _lookForGroundedJoints(self) -> Result[None]: ) return Ok(None) - def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint) -> None: + def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint) -> Result[None]: occ = self.design.findEntityByToken(occ_token)[0] if occ is None: - return + return Ok(None) self.currentTraversal = dict() populate_node_result = self._populateNode(occ, None, None) if populate_node_result.is_err(): # We need the value to proceed - return populate_node_result + unwrapped = populate_node_result.unwrap_err() + return Err(unwrapped[0], unwrapped[1]) rootNode = populate_node_result.unwrap() if rootNode is not None: axisNode = SimulationNode(rootNode, joint) self.simulationNodesRef[occ_token] = axisNode + return Ok(None) + # TODO: Verify that this works after the Result-refactor :skull: def _populateNode( self, @@ -573,7 +578,7 @@ def createTreeParts( node.value = guid_component(dynNode.data) else: if dynNode.data.entityToken is None: - _ = Err("Found None EntityToken", ErrorSeverity.Warning) # type: ignore + _ = Err("Found None EntityToken", ErrorSeverity.Warning) node.value = dynNode.data.name else: node.value = dynNode.data.entityToken diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index d2f1469e41..37d8bce424 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -79,7 +79,7 @@ def populateJoints( assembly: assembly_pb2.Assembly, ) -> Result[None]: info_result = fill_info(joints, None) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result # This is for creating all of the Joint Definition objects @@ -90,12 +90,12 @@ def populateJoints( # Add the grounded joints object - TODO: rename some of the protobuf stuff for the love of god joint_definition_ground = joints.joint_definitions["grounded"] info_result = construct_info("grounded", joint_definition_ground) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result joint_instance_ground = joints.joint_instances["grounded"] info_result = construct_info("grounded", joint_instance_ground) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result joint_instance_ground.joint_reference = joint_definition_ground.info.GUID @@ -132,7 +132,7 @@ def populateJoints( signal = signals.signal_map[guid] info_result = construct_info(joint.name, signal, GUID=guid) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result signal.io = signal_pb2.IOType.OUTPUT @@ -147,7 +147,7 @@ def populateJoints( motor = joints.motor_definitions[joint.entityToken] info_result = fill_info(motor, joint) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result simple_motor = motor.simple_motor @@ -161,7 +161,7 @@ def populateJoints( # signals.signal_map.remove(guid) joint_result = _addJointInstance(joint, joint_instance, joint_definition, signals, options) - if joint_result.is_err() and joint_result.severity == ErrorSeverity.Fatal: + if joint_result.is_err() and joint_result.unwrap_err()[1] == ErrorSeverity.Fatal: return joint_result # adds information for joint motion and limits @@ -176,7 +176,7 @@ def populateJoints( def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> Result[None]: info_result = fill_info(joint_definition, joint) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result jointPivotTranslation = _jointOrigin(joint) @@ -205,7 +205,7 @@ def _addJointInstance( options: ExporterOptions, ) -> Result[None]: info_result = fill_info(joint_instance, joint) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result # because there is only one and we are using the token - should be the same @@ -250,7 +250,7 @@ def _addJointInstance( signal = signals.signal_map[guid] info_result = construct_info("joint_signal", signal, GUID=guid) - if info_result.is_err() and info_result.severity == ErrorSeverity.Fatal: + if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return info_result signal.io = signal_pb2.IOType.OUTPUT diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index f5a09d14f3..7a4ede8fcd 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -150,8 +150,8 @@ def getPhysicalMaterialData( strengthProperties.yield_strength = materialProperties.itemById("structural_Minimum_yield_stress").value strengthProperties.tensile_strength = materialProperties.itemById("structural_Minimum_tensile_strength").value - missingProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] # ignore: type - if missingProperties.__len__() > 0: + missingStrengthProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] # ignore: type + if missingStrengthProperties.__len__() > 0: _ = Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) """ diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 30da9147df..01f5f335d0 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -269,8 +269,9 @@ def export(self) -> None: logger.debug(debug_output.strip()) -def handle_err_top[T](err: Result[T]): +def handle_err_top[T](err: Result[T]) -> None: if err.is_err(): message, severity = err.unwrap_err() if severity == ErrorSeverity.Fatal: + app = adsk.core.Application.get() app.userInterface.messageBox(f"Fatal Error Encountered: {message}") diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 00d722a75d..683b2829f6 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -16,6 +16,7 @@ """ +from typing import Any import adsk from src.ErrorHandling import Err, ErrorSeverity, Ok, Result @@ -39,9 +40,9 @@ def GetPhysicalProperties( if physical is None: return Err("Physical properties object is None", ErrorSeverity.Warning) - missing_properties_bools = [prop is None for prop in physical] - if any(prop for prop, i in missing_properties_bools): - missing_properties: list[Unknown] = [physics[i] for i, prop in enumerate(missing_properties) if prop] + missing_properties_bools: list[bool] = [prop is None for prop in physical] + if any(prop for prop in missing_properties_bools): + missing_properties: list[Any] = [physical[i] for i, prop in enumerate(missing_properties_bools) if prop] _ = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) physicalProperties.density = physical.density From 9850a006b150e217b16764f46d4f98d3a6546b57 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 08:49:43 -0700 Subject: [PATCH 39/68] chore: format again :| --- .../src/Parser/SynthesisParser/Components.py | 1 + .../src/Parser/SynthesisParser/PhysicalProperties.py | 1 + 2 files changed, 2 insertions(+) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index befe5cbd6e..ecb06484c1 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -1,5 +1,6 @@ # Contains all of the logic for mapping the Components / Occurrences from platform import python_build + import adsk.core import adsk.fusion from requests.models import parse_header_links diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 683b2829f6..104587aab6 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -17,6 +17,7 @@ """ from typing import Any + import adsk from src.ErrorHandling import Err, ErrorSeverity, Ok, Result From 321c8dd92e9c40f0c9e5aeb3cea4ebefc2d48717 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 11:23:46 -0700 Subject: [PATCH 40/68] doc: result and it's variants --- .../SynthesisFusionAddin/src/ErrorHandling.py | 54 +++++++++++++++++- .../src/Parser/SynthesisParser/Components.py | 4 +- .../Parser/SynthesisParser/JointHierarchy.py | 6 +- .../src/Parser/SynthesisParser/Joints.py | 4 +- .../src/Parser/SynthesisParser/Materials.py | 4 +- .../SynthesisParser/PhysicalProperties.py | 6 +- .../src/Resources/PWM_icon/16x16-disabled.png | Bin 0 -> 422 bytes .../src/Resources/PWM_icon/32x32-disabled.png | Bin 0 -> 182 bytes .../src/Resources/PWM_icon/32x32-normal.png | Bin 0 -> 545 bytes 9 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 exporter/SynthesisFusionAddin/src/Resources/PWM_icon/16x16-disabled.png create mode 100644 exporter/SynthesisFusionAddin/src/Resources/PWM_icon/32x32-disabled.png create mode 100644 exporter/SynthesisFusionAddin/src/Resources/PWM_icon/32x32-normal.png diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 667669330f..e5dd0fbf0b 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -11,6 +11,7 @@ # If an error is non-fatal to the function that generated it, it should be declared but not return, which prints it to the screen class ErrorSeverity(Enum): Fatal = 50 # Critical Error + Error = 40 # Non-critical Error Warning = 30 # Warning @@ -18,24 +19,56 @@ class ErrorSeverity(Enum): class Result(Generic[T]): + """ + Result class for error handling, similar to the Result enum in Rust. The `Err` and `Ok` variants are child types, rather than enum variants though. Another difference is that the error variant is necessarily packaged with a message and a severity, rather than being arbitrary. + Since python3 has no match statements, use the `is_ok()` or `is_err()` function to check the variant, then `unwrap()` or `unwrap_err()` to get the value or error message and severity. + + ## Example + ```py + foo_result = foo() + if foo_result.is_err() and foo_result.unwrap_err()[1] == ErrorSeverity.Fatal: + return foo_result + ``` + + Please see the `Ok` and `Err` child class documentation for instructions on instantiating errors and ok-values respectively + """ + def is_ok(self) -> bool: + """ + Returns if the Result is the Ok variant + """ return isinstance(self, Ok) def is_err(self) -> bool: + """ + Returns if the Result is the Err variant + """ + return isinstance(self, Err) def unwrap(self) -> T: + """ + Returns the value contained in the Ok variant of the result, or raises an exception if unwrapping an Err variant. Be sure to check first. + """ if self.is_ok(): return self.value # type: ignore raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore def unwrap_err(self) -> tuple[str, ErrorSeverity]: + """ + Returns the error message and severity contained in the Err variant of the result, or raises an exception if unwrapping an Ok variant. Be sure to check first. + """ + if self.is_err(): return (self.message, self.severity) # type: ignore raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore class Ok(Result[T]): + """ + The non-error variant of the Result class. Contains the value of the happy path of the function. Return when the function has executed successfully. + """ + value: T def __init__(self, value: T): @@ -46,6 +79,26 @@ def __repr__(self) -> str: class Err(Result[T]): + """ + The error variant of the Result class. Contains an error message and severity. Severity is the `ErrorSeverity` enum and is either Fatal, Error, or Warning, each corresponding to a logger severity level, Critical Error (50) and Warning (30) respectively. When an `Err` is instantiated, it is automatically logged in the current synthesis logfile. + + If an error is fatal to the entire program (or the parent function), it should be returned and marked as Fatal: + ```python + return Err("Foo not found", ErrorSeverity.Fatal) + ``` + + If an error is fatal to the current function, it should be returned and marked as Error, as the parent could recover it: + ```python + return Err("Bar not found", ErrorSeverity.Error) + ``` + + If an error is not fatal to the current function, but ought to be logged, it should be marked as Warning and instantiated but not returned, as to not break control flow: + ```python + _: Err[T] = Err("Baz not found", ErrorSeverity.Warning) + ``` + Note that the lattermost example will raise a warning if not explicitely typed + """ + message: str severity: ErrorSeverity @@ -59,5 +112,4 @@ def __repr__(self) -> str: return f"Err({self.message})" def write_error(self) -> None: - # Figure out how to integrate severity with the logger logger.log(self.severity.value, self.message) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index ecb06484c1..dded196b3c 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -165,7 +165,7 @@ def parseChildOccurrence( try: part.appearance = "{}_{}".format(occurrence.appearance.name, occurrence.appearance.id) except: - _ = Err("Failed to format part appearance", ErrorSeverity.Warning) + _: Err[None] = Err("Failed to format part appearance", ErrorSeverity.Warning) # ignore: type part.appearance = "default" # TODO: Add phyical_material parser @@ -174,7 +174,7 @@ def parseChildOccurrence( if occurrence.component.material: part.physical_material = occurrence.component.material.id else: - _ = Err(f"Component Material is None", ErrorSeverity.Warning) + __: Err[None] = Err(f"Component Material is None", ErrorSeverity.Warning) def_map = partsData.part_definitions diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 31b4642a51..4393eb1a14 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -270,7 +270,7 @@ def __getAllJoints(self) -> Result[None]: occurrenceTwo = joint.occurrenceTwo else: # Non-fatal since it's recovered in the next two statements - _ = Err("Found joint without two occurences", ErrorSeverity.Warning) + _: Err[None] = Err("Found joint without two occurences", ErrorSeverity.Warning) if occurrenceOne is None: if joint.geometryOrOriginOne.entityOne.assemblyContext is None: @@ -567,7 +567,7 @@ def createTreeParts( # Fine way to use try-excepts in this language if dynNode.data.objectType is None: - _ = Err("Found None object type", ErrorSeverity.Warning) + _: Err[None] = Err("Found None object type", ErrorSeverity.Warning) objectType = "" else: objectType = dynNode.data.objectType @@ -578,7 +578,7 @@ def createTreeParts( node.value = guid_component(dynNode.data) else: if dynNode.data.entityToken is None: - _ = Err("Found None EntityToken", ErrorSeverity.Warning) + __: Err[None] = Err("Found None EntityToken", ErrorSeverity.Warning) node.value = dynNode.data.name else: node.value = dynNode.data.entityToken diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index 37d8bce424..21274fb4d2 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -169,7 +169,7 @@ def populateJoints( except: # TODO: Figure out how to construct and return this (ie, what actually breaks in this try block) - _ = Err("Failed:\n{}".format(traceback.format_exc()), ErrorSeverity.Fatal) + _: Err[None] = Err("Failed:\n{}".format(traceback.format_exc()), ErrorSeverity.Fatal) continue return Ok(None) @@ -190,7 +190,7 @@ def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> Re joint_definition.origin.z = 0.0 # TODO: We definitely could make this fatal, figure out if we should - _ = Err(f"Cannot find joint origin on joint {joint.name}", ErrorSeverity.Warning) + _: Err[None] = Err(f"Cannot find joint origin on joint {joint.name}", ErrorSeverity.Warning) joint_definition.break_magnitude = 0.0 diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 7a4ede8fcd..3b2f9b7a1b 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -142,7 +142,7 @@ def getPhysicalMaterialData( missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] # ignore: type if missingProperties.__len__() > 0: - _ = Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) + _: Err[None] = Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) """ Strength Properties @@ -152,7 +152,7 @@ def getPhysicalMaterialData( missingStrengthProperties: list[str] = [k for k, v in vars(strengthProperties).items() if v is None] # ignore: type if missingStrengthProperties.__len__() > 0: - _ = Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) + __: Err[None] = Err(f"Missing Strength Properties {missingProperties}", ErrorSeverity.Warning) """ strengthProperties.thermal_treatment = materialProperties.itemById( diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 104587aab6..bfe4bede87 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -39,12 +39,12 @@ def GetPhysicalProperties( """ physical = fusionObject.getPhysicalProperties(level) if physical is None: - return Err("Physical properties object is None", ErrorSeverity.Warning) + return Err("Physical properties object is None", ErrorSeverity.Error) missing_properties_bools: list[bool] = [prop is None for prop in physical] if any(prop for prop in missing_properties_bools): missing_properties: list[Any] = [physical[i] for i, prop in enumerate(missing_properties_bools) if prop] - _ = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) + _: Err[None] = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) physicalProperties.density = physical.density physicalProperties.mass = physical.mass @@ -59,6 +59,6 @@ def GetPhysicalProperties( _com.y = com.y _com.z = com.z else: - _ = Err("com is None", ErrorSeverity.Warning) + __: Err[None] = Err("com is None", ErrorSeverity.Warning) return Ok(None) diff --git a/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/16x16-disabled.png b/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/16x16-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..785693934c1e5b5981821965902402bd18336602 GIT binary patch literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^YCtT&!3HF&aX}sYeZu~){mBo_zrk%q!Co3Uq&d#}$ zqUP|VJZHH1kz@bU^XUm2ejGY*z(!DUnn`bCVzo7M^AXvW8K<1s*#3pFwdGD1Oq&1q z;J;K6>ySxNflNXlFPEoic<6YscpEn}ndtU5KD=DS#&*wxjqM>f4^PZRf$|2%Y(s+u zo;>P6Z>26YtCpW~{36h^oCO|{#S9F5he4R}c>anMpkTJAi(`mK=hdr*Tn7|*94^W) z|8{L>dGs5h>60$#cGe5C$O{IBh-P0>W|Y|WqKRoUPu0S(;y>?KYBt%f?;xnUDYg literal 0 HcmV?d00001 diff --git a/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/32x32-disabled.png b/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/32x32-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..834074252e9aafb6beb6e11766172b651e92cfaf GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND45~t z;usRq`u18N*8v3{<_lVNXQIE~)4DCXN}@^cr`Tr=F`svNXHv3uGca)^q@_N3)6L+b z&Y;=Pl<+i=!DMyA$7+XkW+`ch`3JVWjoiCp6C=O$23>asA!!EJGk@3>I+X7>@F z40F%Jct&I7=MT7u;;o^}H&!SW_UaWr jXY@BnG|)f;|2KXD6uPAk{urCr00000NkvXXu0mjfq}KO! literal 0 HcmV?d00001 From 8fcaa9db08b609ce30e3d149491facade13b42d5 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 11:26:48 -0700 Subject: [PATCH 41/68] chore(wip): bump typing validation python version --- .github/workflows/FusionTyping.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/FusionTyping.yml b/.github/workflows/FusionTyping.yml index 13de49fc2f..16a87da037 100644 --- a/.github/workflows/FusionTyping.yml +++ b/.github/workflows/FusionTyping.yml @@ -3,9 +3,9 @@ name: Fusion - mypy Typing Validation on: workflow_dispatch: {} push: - branches: [ prod, dev ] + branches: [prod, dev] pull_request: - branches: [ prod, dev ] + branches: [prod, dev] jobs: mypy: @@ -18,6 +18,6 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: "3.12" - run: pip install -r requirements-mypy.txt - run: mypy From dc3f9ebc81abe36f6f243e7acd63b71dd6e1686b Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 30 Jun 2025 11:35:41 -0700 Subject: [PATCH 42/68] chore: remove unecessary comment --- .../src/Parser/SynthesisParser/Components.py | 1 - 1 file changed, 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index dded196b3c..996dde51de 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -166,7 +166,6 @@ def parseChildOccurrence( part.appearance = "{}_{}".format(occurrence.appearance.name, occurrence.appearance.id) except: _: Err[None] = Err("Failed to format part appearance", ErrorSeverity.Warning) - # ignore: type part.appearance = "default" # TODO: Add phyical_material parser From d48411626d121e9561bec3234a28702cf246c068 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 1 Jul 2025 10:53:49 -0700 Subject: [PATCH 43/68] fix: remove comments and unneeded import --- exporter/SynthesisFusionAddin/Synthesis.py | 1 - .../SynthesisFusionAddin/src/ErrorHandling.py | 15 +-------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 20a7b322db..9729b49a89 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -11,7 +11,6 @@ from src.Logging import logFailure, setupLogger logger = setupLogger() -from src.ErrorHandling import Err, ErrorSeverity try: # Attempt to import required pip dependencies to verify their installation. diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index e5dd0fbf0b..a2d0b5faa1 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -34,31 +34,18 @@ class Result(Generic[T]): """ def is_ok(self) -> bool: - """ - Returns if the Result is the Ok variant - """ return isinstance(self, Ok) def is_err(self) -> bool: - """ - Returns if the Result is the Err variant - """ - return isinstance(self, Err) def unwrap(self) -> T: - """ - Returns the value contained in the Ok variant of the result, or raises an exception if unwrapping an Err variant. Be sure to check first. - """ if self.is_ok(): return self.value # type: ignore raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore def unwrap_err(self) -> tuple[str, ErrorSeverity]: - """ - Returns the error message and severity contained in the Err variant of the result, or raises an exception if unwrapping an Ok variant. Be sure to check first. - """ - + if self.is_err(): return (self.message, self.severity) # type: ignore raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore From cc4c2997e90672cabc69d1ed5895ca3caf2c8504 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 1 Jul 2025 10:54:45 -0700 Subject: [PATCH 44/68] chore: format --- exporter/SynthesisFusionAddin/src/ErrorHandling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index a2d0b5faa1..4f7944672f 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -45,7 +45,7 @@ def unwrap(self) -> T: raise Exception(f"Called unwrap on Err: {self.message}") # type: ignore def unwrap_err(self) -> tuple[str, ErrorSeverity]: - + if self.is_err(): return (self.message, self.severity) # type: ignore raise Exception(f"Called unwrap_err on Ok: {self.value}") # type: ignore From 6f433551d24356cfbdaa3ae52e2988820be94b5e Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 1 Jul 2025 16:56:01 -0700 Subject: [PATCH 45/68] fix(wip): started debuggingi exporting occurence & apperance errors (fixed apperance) --- .../src/Parser/SynthesisParser/Components.py | 21 +++++++++++------- .../Parser/SynthesisParser/JointHierarchy.py | 5 +++-- .../SynthesisParser/PhysicalProperties.py | 6 ++--- .../src/Parser/SynthesisParser/Utilities.py | 2 +- .../src/Resources/PWM_icon/16x16-normal.png | Bin 782 -> 494 bytes fission/src/mirabuf/MirabufParser.ts | 2 +- fission/src/systems/physics/PhysicsSystem.ts | 2 +- 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 996dde51de..01d4667a47 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -3,10 +3,11 @@ import adsk.core import adsk.fusion +from google.protobuf.message import Error from requests.models import parse_header_links from src.ErrorHandling import Err, ErrorSeverity, Ok, Result -from src.Logging import logFailure +from src.Logging import getLogger, logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser import PhysicalProperties from src.Parser.SynthesisParser.PDMessage import PDMessage @@ -18,6 +19,8 @@ from src.Proto import assembly_pb2, joint_pb2, material_pb2, types_pb2 from src.Types import ExportMode +logger = getLogger() + # TODO: Impelement Material overrides def MapAllComponents( @@ -27,6 +30,8 @@ def MapAllComponents( partsData: assembly_pb2.Parts, materials: material_pb2.Materials, ) -> Result[None]: + + logger.log(10, f"HELLO") for component in design.allComponents: adsk.doEvents() if progressDialog.wasCancelled(): @@ -36,13 +41,13 @@ def MapAllComponents( comp_ref = guid_component(component) fill_info_result = fill_info(partsData, None) - if fill_info_result.is_err(): + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return fill_info_result partDefinition = partsData.part_definitions[comp_ref] fill_info_result = fill_info(partDefinition, component, comp_ref) - if fill_info_result.is_err(): + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return fill_info_result physical_properties_result = PhysicalProperties.GetPhysicalProperties(component, partDefinition.physical_data) @@ -56,12 +61,13 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non raise RuntimeError("User canceled export") if body.isLightBulbOn: part_body = partDefinition.bodies.add() - part_body.part = comp_ref fill_info_result = fill_info(part_body, body) - if fill_info_result.is_err(): + if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return fill_info_result + part_body.part = comp_ref + if isinstance(body, adsk.fusion.BRepBody): parse_result = ParseBRep(body, options, part_body.triangle_mesh) if parse_result.is_err() and parse_result.unwrap_err()[1] == ErrorSeverity.Fatal: @@ -227,12 +233,11 @@ def ParseBRep( options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, ) -> Result[None]: - meshManager = body.meshManager - calc = meshManager.createMeshCalculator() + calc = body.meshManager.createMeshCalculator() # Disabling for now. We need the user to be able to adjust this, otherwise it gets locked # into whatever the default was at the time it first creates the export options. # calc.setQuality(options.visualQuality) - calc.setQuality(adsk.fusion.TriangleMeshQualityOptions.LowQualityTriangleMesh) + _ = calc.setQuality(adsk.fusion.TriangleMeshQualityOptions.LowQualityTriangleMesh) # calc.maxNormalDeviation = 3.14159 * (1.0 / 6.0) # calc.surfaceTolerance = 0.5 mesh = calc.calculate() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 4393eb1a14..b20a8f9d58 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -251,7 +251,7 @@ def __init__(self, design: adsk.fusion.Design) -> None: self.simulationNodesRef["GROUND"] = self.groundSimNode # combine all ground prior to this possibly - self._lookForGroundedJoints() + _ = self._lookForGroundedJoints() # creates the axis elements - adds all elements to axisNodes for key, value in self.dynamicJoints.items(): @@ -259,11 +259,12 @@ def __init__(self, design: adsk.fusion.Design) -> None: if populate_axis_result.is_err(): raise RuntimeError(populate_axis_result.unwrap_err()[0]) - self._linkAllAxis() + __ = self._linkAllAxis() # self.groundSimNode.printLink() def __getAllJoints(self) -> Result[None]: + logger.log(10, "Getting Joints") for joint in list(self.design.rootComponent.allJoints) + list(self.design.rootComponent.allAsBuiltJoints): if joint and joint.occurrenceOne and joint.occurrenceTwo: occurrenceOne = joint.occurrenceOne diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index bfe4bede87..25ca56f9a1 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -41,10 +41,10 @@ def GetPhysicalProperties( if physical is None: return Err("Physical properties object is None", ErrorSeverity.Error) - missing_properties_bools: list[bool] = [prop is None for prop in physical] + missing_properties_bools: list[bool] = [value is None for prop, value in vars(physical).items() if not prop.startswith('__')] if any(prop for prop in missing_properties_bools): - missing_properties: list[Any] = [physical[i] for i, prop in enumerate(missing_properties_bools) if prop] - _: Err[None] = Err(f"Missing some physical properties: {missing_properties}", ErrorSeverity.Warning) + # missing_properties: list[Any] = [physical[i] for i, prop in enumerate(missing_properties_bools) if prop] + _: Err[None] = Err("Missing some physical properties", ErrorSeverity.Warning) physicalProperties.density = physical.density physicalProperties.mass = physical.mass diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index 4db87edec4..aada12650c 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -58,7 +58,7 @@ def construct_info( elif name != "": proto_obj.info.name = name else: - return Err("Attempted to set proto_obj.info.name to None", ErrorSeverity.Warning) + _: Err[None] = Err("Attempted to set proto_obj.info.name to None", ErrorSeverity.Warning) if GUID is not None: proto_obj.info.GUID = str(GUID) diff --git a/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/16x16-normal.png b/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/16x16-normal.png index 07cd465f72d6f27012f2fd9dad871c162e02105a..2e3175e8a880c7ec51f90939eb9bf03ebfefb40c 100644 GIT binary patch literal 494 zcmV1OJNdEfy`{l7s>o1?&vH25G2EAtvm!;B{{ z-duPmpd9*>MaZ3jg>C8UOUJ$v<1J*DF_}y`u-$O_mA|UdHKyo_S+E#@;zTu5OU`c} znN>f$e*W(7y_+wIF#ry<4Gow$JStnCz4|Zv@7}TUcV{*{CE5T@D+fky+q8^VKZMf% z-Q833_1KK*zkmP!OSAzjYHkJ}nH0Oe-#R+&)4_=ozJGrI8>>2827F>wi2C~S8#mL9 zbqjE5#%TZ(Fc$uN{4DVI5A%D5ukSwN3nxZg65Jwkf((qzT)#d(fBE~@k3YE7G2xPs zvdDE(v#-t+6p`n~=K`z-0Fx)vyHD(acb>6T{9|Mi!e;<8E(vxvw!42n-{1P@!_}+5 ket!GSKn7sM=S3I*0Dxtiu2_e#i2wiq07*qoM6N<$g7N|3S^xk5 literal 782 zcmV+p1M&QcP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0;5SpK~zXf&6Z0~ zQ&AL!KZ*!azy}HxBZ3kYh{QL>$Up}gNem&5oH#Xc$@R=Ay zd`26PhbT}ew6svbwR>(;6S?h>uH@2l&ug!<_B#7ggom{ota;51MbSgjoYH>^g;WMc zuBgyRWssZL<{(tnYF0q9uEOFR#3N9u{<11qn1YpMDBq^ZnzN{}K#A7WK|*skl|o6m z*34*ZR_!vat$@@LEX83p<>IV^;@*?=dhLI)q9;IO2V8joZI_@`PkXmE(@^;i=(_C! zrV?=Z9vnCaBX3~OF}QHc9pXq2oWH3kB0KyV_ML^RkD$3z?{6JhALy9t!iYIlb~VHJ zAPhcn0XnZsNvW_k1ba_GWeqeOheoxl>P76R8#6_!8zI!HvD0p^P@A5^P;*e~ZApjl zgCDW1#>-Mi&X6) z_@j3todA=mC$hwy6-EcT@Zc+b0RYF0fGwHBsHeO3Me9$dh_Ju=WOukr27rE z$O7ASpkevdB_tQ}D`o~Y1;Q_(|1R|15wTx-f8mVWa6-hsyMRq+T%XO&z%I+26mGew zOQh;FGa_Q%SPu9R%l0RA@}WL>F4GP?7D?M!C!Ur{$px1tQ_RQ(bt&wY+E@llF_*sm zGQ8Ph-cv9S{D?VcCgz=2?9_JqtYlqwnN3 zn=_!)N^Q%)ib_TfH>6VaE2(*Lm4QI96s&eu zFr{ptMcYH+tUlTWGV(qs*vjvh$_+;#^EKZ<{FkxxN8D={*uOpS7qM>jiniOs2mk;8 M07*qoM6N<$f{5y5xc~qF diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index 1b81165f1c..5dd3b5366c 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -301,7 +301,7 @@ class MirabufParser { const mat = partInstance.transform ? MirabufTransform_ThreeMatrix4(partInstance.transform) - : def.baseTransform + : def?.baseTransform ? MirabufTransform_ThreeMatrix4(def.baseTransform) : new THREE.Matrix4().identity() diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 1988708c05..48ea15c370 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -1554,7 +1554,7 @@ function filterNonPhysicsNodes(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembl for (const part of x.parts) { const inst = mira.data!.parts!.partInstances![part]! const def = mira.data!.parts!.partDefinitions![inst.partDefinitionReference!]! - if (def.bodies && def.bodies.length > 0) { + if (def.bodies && def?.bodies?.length > 0) { return true } } From a331f88d1b4467023331de0eaec5fbd386b501af Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 2 Jul 2025 09:22:41 -0700 Subject: [PATCH 46/68] fix: catch and recover mesh calculation --- .../src/Parser/SynthesisParser/Components.py | 12 ++++++------ .../src/Parser/SynthesisParser/JointHierarchy.py | 8 ++++---- .../src/Parser/SynthesisParser/Joints.py | 2 +- .../src/Parser/SynthesisParser/Materials.py | 2 +- .../src/Parser/SynthesisParser/Utilities.py | 4 +--- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 01d4667a47..3dcd335d88 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -19,9 +19,6 @@ from src.Proto import assembly_pb2, joint_pb2, material_pb2, types_pb2 from src.Types import ExportMode -logger = getLogger() - - # TODO: Impelement Material overrides def MapAllComponents( design: adsk.fusion.Design, @@ -31,7 +28,6 @@ def MapAllComponents( materials: material_pb2.Materials, ) -> Result[None]: - logger.log(10, f"HELLO") for component in design.allComponents: adsk.doEvents() if progressDialog.wasCancelled(): @@ -233,6 +229,7 @@ def ParseBRep( options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, ) -> Result[None]: + calc = body.meshManager.createMeshCalculator() # Disabling for now. We need the user to be able to adjust this, otherwise it gets locked # into whatever the default was at the time it first creates the export options. @@ -240,7 +237,10 @@ def ParseBRep( _ = calc.setQuality(adsk.fusion.TriangleMeshQualityOptions.LowQualityTriangleMesh) # calc.maxNormalDeviation = 3.14159 * (1.0 / 6.0) # calc.surfaceTolerance = 0.5 - mesh = calc.calculate() + try: + mesh = calc.calculate() + except: + return Err(f"Failed to calculate mesh for {body.name}", ErrorSeverity.Error) fill_info_result = fill_info(trimesh, body) if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: @@ -253,7 +253,7 @@ def ParseBRep( plainmesh_out.normals.extend(mesh.normalVectorsAsFloat) plainmesh_out.indices.extend(mesh.nodeIndices) plainmesh_out.uv.extend(mesh.textureCoordinatesAsFloat) - + return Ok(None) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index b20a8f9d58..ef0de35718 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -271,16 +271,16 @@ def __getAllJoints(self) -> Result[None]: occurrenceTwo = joint.occurrenceTwo else: # Non-fatal since it's recovered in the next two statements - _: Err[None] = Err("Found joint without two occurences", ErrorSeverity.Warning) + _: Err[None] = Err("Found joint without two occurrences", ErrorSeverity.Warning) if occurrenceOne is None: if joint.geometryOrOriginOne.entityOne.assemblyContext is None: - return Err("occurrenceOne and entityOne's assembly context are None", ErrorSeverity.Fatal) + _ = Err("occurrenceOne and entityOne's assembly context are None", ErrorSeverity.Fatal) occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext if occurrenceTwo is None: if joint.geometryOrOriginTwo.entityTwo.assemblyContext is None: - return Err("occurrenceOne and entityTwo's assembly context are None", ErrorSeverity.Fatal) + __ = Err("occurrenceOne and entityTwo's assembly context are None", ErrorSeverity.Fatal) occurrenceTwo = joint.geometryOrOriginTwo.entityTwo.assemblyContext oneEntityToken = "" @@ -305,7 +305,7 @@ def __getAllJoints(self) -> Result[None]: # TODO: Check if this is fatal or not if occurrenceTwo is None and occurrenceOne is None: - return Err( + ___ = Err( f"Occurrences that connect joints could not be found\n\t1: {occurrenceOne}\n\t2: {occurrenceTwo}", ErrorSeverity.Fatal, ) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index 21274fb4d2..422855299f 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -310,7 +310,7 @@ def fillRevoluteJointMotion(revoluteMotion: adsk.fusion.RevoluteJointMotion, pro dof = proto_joint.rotational.rotational_freedom - # name + #name # axis # pivot # dynamics diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 3b2f9b7a1b..79e49568cc 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -140,7 +140,7 @@ def getPhysicalMaterialData( mechanicalProperties.density = materialProperties.itemById("structural_Density").value mechanicalProperties.damping_coefficient = materialProperties.itemById("structural_Damping_coefficient").value - missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None] # ignore: type + missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None and not k.startswith("__")] # ignore: type if missingProperties.__len__() > 0: _: Err[None] = Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index aada12650c..50f8f0a27a 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -57,9 +57,7 @@ def construct_info( proto_obj.info.name = fus_object.name elif name != "": proto_obj.info.name = name - else: - _: Err[None] = Err("Attempted to set proto_obj.info.name to None", ErrorSeverity.Warning) - + if GUID is not None: proto_obj.info.GUID = str(GUID) elif fus_object is not None and hasattr(fus_object, "entityToken"): From 456a853d430ffc2246acfad51ec972d7809bc7d0 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 2 Jul 2025 09:43:34 -0700 Subject: [PATCH 47/68] chore: typing + formatting --- .../src/Parser/SynthesisParser/Components.py | 3 ++- .../src/Parser/SynthesisParser/JointHierarchy.py | 8 +++++--- .../src/Parser/SynthesisParser/Joints.py | 2 +- .../src/Parser/SynthesisParser/Materials.py | 4 +++- .../src/Parser/SynthesisParser/PhysicalProperties.py | 4 +++- .../src/Parser/SynthesisParser/Utilities.py | 2 +- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 3dcd335d88..40845c175b 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -19,6 +19,7 @@ from src.Proto import assembly_pb2, joint_pb2, material_pb2, types_pb2 from src.Types import ExportMode + # TODO: Impelement Material overrides def MapAllComponents( design: adsk.fusion.Design, @@ -253,7 +254,7 @@ def ParseBRep( plainmesh_out.normals.extend(mesh.normalVectorsAsFloat) plainmesh_out.indices.extend(mesh.nodeIndices) plainmesh_out.uv.extend(mesh.textureCoordinatesAsFloat) - + return Ok(None) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index ef0de35718..7f0963e829 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -275,12 +275,14 @@ def __getAllJoints(self) -> Result[None]: if occurrenceOne is None: if joint.geometryOrOriginOne.entityOne.assemblyContext is None: - _ = Err("occurrenceOne and entityOne's assembly context are None", ErrorSeverity.Fatal) + ____: Err[None] = Err( + "occurrenceOne and entityOne's assembly context are None", ErrorSeverity.Fatal + ) occurrenceOne = joint.geometryOrOriginOne.entityOne.assemblyContext if occurrenceTwo is None: if joint.geometryOrOriginTwo.entityTwo.assemblyContext is None: - __ = Err("occurrenceOne and entityTwo's assembly context are None", ErrorSeverity.Fatal) + __: Err[None] = Err("occurrenceOne and entityTwo's assembly context are None", ErrorSeverity.Fatal) occurrenceTwo = joint.geometryOrOriginTwo.entityTwo.assemblyContext oneEntityToken = "" @@ -305,7 +307,7 @@ def __getAllJoints(self) -> Result[None]: # TODO: Check if this is fatal or not if occurrenceTwo is None and occurrenceOne is None: - ___ = Err( + ___: Err[None] = Err( f"Occurrences that connect joints could not be found\n\t1: {occurrenceOne}\n\t2: {occurrenceTwo}", ErrorSeverity.Fatal, ) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index 422855299f..21274fb4d2 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -310,7 +310,7 @@ def fillRevoluteJointMotion(revoluteMotion: adsk.fusion.RevoluteJointMotion, pro dof = proto_joint.rotational.rotational_freedom - #name + # name # axis # pivot # dynamics diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 79e49568cc..89c4164f90 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -140,7 +140,9 @@ def getPhysicalMaterialData( mechanicalProperties.density = materialProperties.itemById("structural_Density").value mechanicalProperties.damping_coefficient = materialProperties.itemById("structural_Damping_coefficient").value - missingProperties: list[str] = [k for k, v in vars(mechanicalProperties).items() if v is None and not k.startswith("__")] # ignore: type + missingProperties: list[str] = [ + k for k, v in vars(mechanicalProperties).items() if v is None and not k.startswith("__") + ] # ignore: type if missingProperties.__len__() > 0: _: Err[None] = Err(f"Missing Mechanical Properties {missingProperties}", ErrorSeverity.Warning) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 25ca56f9a1..7a18d1d3e8 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -41,7 +41,9 @@ def GetPhysicalProperties( if physical is None: return Err("Physical properties object is None", ErrorSeverity.Error) - missing_properties_bools: list[bool] = [value is None for prop, value in vars(physical).items() if not prop.startswith('__')] + missing_properties_bools: list[bool] = [ + value is None for prop, value in vars(physical).items() if not prop.startswith("__") + ] if any(prop for prop in missing_properties_bools): # missing_properties: list[Any] = [physical[i] for i, prop in enumerate(missing_properties_bools) if prop] _: Err[None] = Err("Missing some physical properties", ErrorSeverity.Warning) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index 50f8f0a27a..6eb56a7f44 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -57,7 +57,7 @@ def construct_info( proto_obj.info.name = fus_object.name elif name != "": proto_obj.info.name = name - + if GUID is not None: proto_obj.info.GUID = str(GUID) elif fus_object is not None and hasattr(fus_object, "entityToken"): From df520abefbc595979c55a09d84c75df659a03193 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 3 Jul 2025 14:31:20 -0700 Subject: [PATCH 48/68] chore: changed all files to camelCase in our src files --- exporter/SynthesisFusionAddin/Synthesis.py | 2 -- exporter/SynthesisFusionAddin/src/ErrorHandling.py | 12 ++++++++++-- .../src/Parser/SynthesisParser/Components.py | 14 +++++++------- .../src/Parser/SynthesisParser/JointHierarchy.py | 2 +- .../src/Parser/SynthesisParser/Materials.py | 4 ++-- .../src/Parser/SynthesisParser/Parser.py | 12 ++++++------ .../Parser/SynthesisParser/PhysicalProperties.py | 2 +- .../src/Parser/SynthesisParser/RigidGroup.py | 2 +- exporter/SynthesisFusionAddin/src/Types.py | 14 +++++++------- exporter/SynthesisFusionAddin/src/UI/Camera.py | 2 +- .../src/UI/FileDialogConfig.py | 4 ++-- 11 files changed, 38 insertions(+), 32 deletions(-) diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py index 9729b49a89..2dfce1da61 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -50,7 +50,6 @@ def run(_context: dict[str, Any]) -> None: Arguments: **context** *context* -- Fusion context to derive app and UI. """ - # Remove all items prior to start just to make sure unregister_all() @@ -70,7 +69,6 @@ def stop(_context: dict[str, Any]) -> None: Arguments: **context** *context* -- Fusion Data. """ - unregister_all() app = adsk.core.Application.get() diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 4f7944672f..64fe7cf30e 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -20,7 +20,10 @@ class ErrorSeverity(Enum): class Result(Generic[T]): """ - Result class for error handling, similar to the Result enum in Rust. The `Err` and `Ok` variants are child types, rather than enum variants though. Another difference is that the error variant is necessarily packaged with a message and a severity, rather than being arbitrary. + Result class for error handling, similar to the Result enum in Rust. + + The `Err` and `Ok` variants are child types, rather than enum variants though. Another difference is that the error variant is necessarily packaged with a message and a severity, rather than being arbitrary. + Since python3 has no match statements, use the `is_ok()` or `is_err()` function to check the variant, then `unwrap()` or `unwrap_err()` to get the value or error message and severity. ## Example @@ -67,8 +70,13 @@ def __repr__(self) -> str: class Err(Result[T]): """ - The error variant of the Result class. Contains an error message and severity. Severity is the `ErrorSeverity` enum and is either Fatal, Error, or Warning, each corresponding to a logger severity level, Critical Error (50) and Warning (30) respectively. When an `Err` is instantiated, it is automatically logged in the current synthesis logfile. + The error variant of the Result class. + + It contains an error message and severity, which is either Fatal, Error, or Warning, each corresponding to a logger severity level, Critical Error (50) and Warning (30) respectively. + + When an `Err` is instantiated, it is automatically logged in the current synthesis logfile. + ## Examples If an error is fatal to the entire program (or the parent function), it should be returned and marked as Fatal: ```python return Err("Foo not found", ErrorSeverity.Fatal) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 40845c175b..28c23c63f9 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -21,7 +21,7 @@ # TODO: Impelement Material overrides -def MapAllComponents( +def mapAllComponents( design: adsk.fusion.Design, options: ExporterOptions, progressDialog: PDMessage, @@ -47,7 +47,7 @@ def MapAllComponents( if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: return fill_info_result - physical_properties_result = PhysicalProperties.GetPhysicalProperties(component, partDefinition.physical_data) + physical_properties_result = PhysicalProperties.getPhysicalProperties(component, partDefinition.physical_data) if physical_properties_result.is_err() and physical_properties_result.unwrap_err()[1] == ErrorSeverity.Fatal: return physical_properties_result @@ -95,7 +95,7 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non return Ok(None) -def ParseComponentRoot( +def parseComponentRoot( component: adsk.fusion.Component, progressDialog: PDMessage, options: ExporterOptions, @@ -217,7 +217,7 @@ def parseChildOccurrence( # saw online someone used this to get the correct context but oh boy does it look pricey # I think if I can make all parts relative to a parent it should return that parents transform maybe # TESTED AND VERIFIED - but unoptimized -def GetMatrixWorld(occurrence: adsk.fusion.Occurrence) -> adsk.core.Matrix3D: +def getMatrixWorld(occurrence: adsk.fusion.Occurrence) -> adsk.core.Matrix3D: matrix = occurrence.transform2 while occurrence.assemblyContext: matrix.transformBy(occurrence.assemblyContext.transform2) @@ -225,7 +225,7 @@ def GetMatrixWorld(occurrence: adsk.fusion.Occurrence) -> adsk.core.Matrix3D: return matrix -def ParseBRep( +def parseBRep( body: adsk.fusion.BRepBody, options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, @@ -258,7 +258,7 @@ def ParseBRep( return Ok(None) -def ParseMesh( +def parseMesh( meshBody: adsk.fusion.MeshBody, options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, @@ -283,7 +283,7 @@ def ParseMesh( return Ok(None) -def MapRigidGroups(rootComponent: adsk.fusion.Component, joints: joint_pb2.Joints) -> None: +def mapRigidGroups(rootComponent: adsk.fusion.Component, joints: joint_pb2.Joints) -> None: groups = rootComponent.allRigidGroups for group in groups: mira_group = joint_pb2.RigidGroup() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 7f0963e829..2326169ccc 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -481,7 +481,7 @@ def searchForGrounded( # ________________________ Build implementation ______________________ # -def BuildJointPartHierarchy( +def buildJointPartHierarchy( design: adsk.fusion.Design, joints: joint_pb2.Joints, options: ExporterOptions, diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 89c4164f90..c0824c43bb 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -26,7 +26,7 @@ } -def MapAllPhysicalMaterials( +def mapAllPhysicalMaterials( physicalMaterials: list[material_pb2.PhysicalMaterial], materials: material_pb2.Materials, options: ExporterOptions, @@ -165,7 +165,7 @@ def getPhysicalMaterialData( return Ok(None) -def MapAllAppearances( +def mapAllAppearances( appearances: list[material_pb2.Appearance], materials: material_pb2.Materials, options: ExporterOptions, diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 01f5f335d0..38b7bf612b 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -80,7 +80,7 @@ def export(self) -> None: ) handle_err_top( - Materials.MapAllAppearances( + Materials.mapAllAppearances( design.appearances, assembly_out.data.materials, self.exporterOptions, @@ -89,7 +89,7 @@ def export(self) -> None: ) handle_err_top( - Materials.MapAllPhysicalMaterials( + Materials.mapAllPhysicalMaterials( design.materials, assembly_out.data.materials, self.exporterOptions, @@ -98,7 +98,7 @@ def export(self) -> None: ) handle_err_top( - Components.MapAllComponents( + Components.mapAllComponents( design, self.exporterOptions, self.pdMessage, @@ -110,7 +110,7 @@ def export(self) -> None: rootNode = types_pb2.Node() handle_err_top( - Components.ParseComponentRoot( + Components.parseComponentRoot( design.rootComponent, self.pdMessage, self.exporterOptions, @@ -120,7 +120,7 @@ def export(self) -> None: ) ) - Components.MapRigidGroups(design.rootComponent, assembly_out.data.joints) + Components.mapRigidGroups(design.rootComponent, assembly_out.data.joints) assembly_out.design_hierarchy.nodes.append(rootNode) @@ -150,7 +150,7 @@ def export(self) -> None: ) handle_err_top( - JointHierarchy.BuildJointPartHierarchy( + JointHierarchy.buildJointPartHierarchy( design, assembly_out.data.joints, self.exporterOptions, self.pdMessage ) ) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py index 7a18d1d3e8..42f933a951 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -25,7 +25,7 @@ from src.Proto import types_pb2 -def GetPhysicalProperties( +def getPhysicalProperties( fusionObject: adsk.fusion.BRepBody | adsk.fusion.Occurrence | adsk.fusion.Component, physicalProperties: types_pb2.PhysicalProperties, level: int = 1, diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py index f49992affc..afcba2114a 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py @@ -23,7 +23,7 @@ # According to the type errors I'm getting here this code would have never compiled. # Should be removed later @logFailure -def ExportRigidGroups( +def exportRigidGroups( fus_occ: adsk.fusion.Occurrence | adsk.fusion.Component, hel_occ: assembly_pb2.Occurrence, # type: ignore[name-defined] ) -> None: diff --git a/exporter/SynthesisFusionAddin/src/Types.py b/exporter/SynthesisFusionAddin/src/Types.py index 5de436fd6e..173f8a84ef 100644 --- a/exporter/SynthesisFusionAddin/src/Types.py +++ b/exporter/SynthesisFusionAddin/src/Types.py @@ -182,7 +182,7 @@ def _os() -> str: else: raise OSError(2, "No Operating System Recognized", f"{osName}") - def AssertEquals(self, comparing: object) -> bool: + def assertEquals(self, comparing: object) -> bool: """Compares the two OString objects Args: @@ -236,7 +236,7 @@ def deserialize(cls, serialized: str | os.PathLike[str]) -> object: return cls(path, file) @classmethod - def LocalPath(cls, fileName: str) -> object: + def localPath(cls, fileName: str) -> object: """Gets the local path in the absolute form for this file Args: @@ -249,7 +249,7 @@ def LocalPath(cls, fileName: str) -> object: return cls(path.split(os.sep), fileName) @classmethod - def AddinPath(cls, fileName: str) -> object: + def addinPath(cls, fileName: str) -> object: """Gets the local path in the absolute form for this file Args: @@ -262,7 +262,7 @@ def AddinPath(cls, fileName: str) -> object: return cls(path, fileName) @classmethod - def AppDataPath(cls, fileName: str) -> object: + def appDataPath(cls, fileName: str) -> object: """Attempts to generate a file path in the Appdata Directory listed below Used by TempPath in the windows environment @@ -281,7 +281,7 @@ def AppDataPath(cls, fileName: str) -> object: return None @classmethod - def ThumbnailPath(cls, fileName: str) -> object: + def thumbnailPath(cls, fileName: str) -> object: # this is src src = pathlib.Path(__file__).parent.parent res = os.path.join(src, "Resources", "Icons") @@ -289,7 +289,7 @@ def ThumbnailPath(cls, fileName: str) -> object: return cls(res, fileName) @classmethod - def TempPath(cls, fileName: str) -> object: + def tempPath(cls, fileName: str) -> object: """Find a temporary path that will work on any OS to write a file to and read from Args: @@ -301,7 +301,7 @@ def TempPath(cls, fileName: str) -> object: _os = cls._os() if _os == "Windows": - return cls.AppDataPath(fileName) + return cls.appDataPath(fileName) elif _os == "Darwin": baseFile = pathlib.Path(__file__).parent.parent.parent path = os.path.join(baseFile, "TemporaryOutput") diff --git a/exporter/SynthesisFusionAddin/src/UI/Camera.py b/exporter/SynthesisFusionAddin/src/UI/Camera.py index 53168fd24d..79c14a8dc9 100644 --- a/exporter/SynthesisFusionAddin/src/UI/Camera.py +++ b/exporter/SynthesisFusionAddin/src/UI/Camera.py @@ -47,7 +47,7 @@ def clearIconCache() -> None: This is useful for now but should be cached in the event the app is closed and re-opened. """ - path = OString.ThumbnailPath("Whatever.png").getDirectory() # type: ignore[attr-defined] + path = OString.thumbnailPath("Whatever.png").getDirectory() # type: ignore[attr-defined] for _r, _d, f in os.walk(path): for file in f: diff --git a/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py b/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py index 48be6df4f4..05f03acdc4 100644 --- a/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py +++ b/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py @@ -77,7 +77,7 @@ def generateFilePath() -> str: """ # Transition: AARD-1765 # Ignoring the type for now, will revisit in the OString refactor - tempPath = OString.TempPath("").getPath() # type: ignore + tempPath = OString.tempPath("").getPath() # type: ignore return str(tempPath) @@ -100,4 +100,4 @@ def generateFileName() -> str: return "{0}_{1}.mira".format(name, version) -def OpenFileDialog() -> None: ... +def openFileDialog() -> None: ... From af57015ecefb4285ed6430a9dd2d84b0aadf8278 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 3 Jul 2025 14:33:32 -0700 Subject: [PATCH 49/68] chore: fix unrelated changes --- .github/workflows/FusionTyping.yml | 4 ++-- fission/src/systems/physics/PhysicsSystem.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/FusionTyping.yml b/.github/workflows/FusionTyping.yml index 16a87da037..e156d71677 100644 --- a/.github/workflows/FusionTyping.yml +++ b/.github/workflows/FusionTyping.yml @@ -3,9 +3,9 @@ name: Fusion - mypy Typing Validation on: workflow_dispatch: {} push: - branches: [prod, dev] + branches: [ prod, dev ] pull_request: - branches: [prod, dev] + branches: [ prod, dev ] jobs: mypy: diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index fb9b8ff7c1..9a351fbaa6 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -1559,7 +1559,7 @@ function filterNonPhysicsNodes(nodes: RigidNodeReadOnly[], mira: mirabuf.Assembl for (const part of x.parts) { const inst = mira.data!.parts!.partInstances![part]! const def = mira.data!.parts!.partDefinitions![inst.partDefinitionReference!]! - if (def.bodies && def?.bodies?.length > 0) { + if (def.bodies && def.bodies.length > 0) { return true } } From bfab139ff44af4d90a7f13c80f11e1b36248382c Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 3 Jul 2025 14:44:00 -0700 Subject: [PATCH 50/68] fix: return fatal error --- .../src/Parser/SynthesisParser/Components.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 28c23c63f9..5e9bf1b806 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -66,11 +66,11 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non part_body.part = comp_ref if isinstance(body, adsk.fusion.BRepBody): - parse_result = ParseBRep(body, options, part_body.triangle_mesh) + parse_result = parseBRep(body, options, part_body.triangle_mesh) if parse_result.is_err() and parse_result.unwrap_err()[1] == ErrorSeverity.Fatal: return parse_result else: - parse_result = ParseMesh(body, options, part_body.triangle_mesh) + parse_result = parseMesh(body, options, part_body.triangle_mesh) if parse_result.is_err() and parse_result.unwrap_err()[1] == ErrorSeverity.Fatal: return parse_result @@ -176,7 +176,7 @@ def parseChildOccurrence( if occurrence.component.material: part.physical_material = occurrence.component.material.id else: - __: Err[None] = Err(f"Component Material is None", ErrorSeverity.Warning) + return Err(f"Component Material is None", ErrorSeverity.Fatal) def_map = partsData.part_definitions @@ -192,7 +192,7 @@ def parseChildOccurrence( part.transform.spatial_matrix.extend(occurrence.transform.asArray()) - worldTransform = GetMatrixWorld(occurrence) + worldTransform = getMatrixWorld(occurrence) if worldTransform: part.global_transform.spatial_matrix.extend(worldTransform.asArray()) From 4e81973debf7ab88664e16fd6e3add00bf3acdf1 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 3 Jul 2025 14:45:49 -0700 Subject: [PATCH 51/68] chore: add back gitkeep --- exporter/SynthesisFusionAddin/logs/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 exporter/SynthesisFusionAddin/logs/.gitkeep diff --git a/exporter/SynthesisFusionAddin/logs/.gitkeep b/exporter/SynthesisFusionAddin/logs/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 From ff8b9db3ab8160c1156b3b200cdc65c4211524ce Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 7 Jul 2025 11:52:10 -0700 Subject: [PATCH 52/68] refactor: extract error checking to is_fatal --- .../SynthesisFusionAddin/src/ErrorHandling.py | 5 +++- .../src/Parser/SynthesisParser/Components.py | 24 +++++++++---------- .../Parser/SynthesisParser/JointHierarchy.py | 10 ++++---- .../src/Parser/SynthesisParser/Joints.py | 18 +++++++------- .../src/Parser/SynthesisParser/Materials.py | 8 +++---- 5 files changed, 34 insertions(+), 31 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 64fe7cf30e..5e80ff36d1 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -29,7 +29,7 @@ class Result(Generic[T]): ## Example ```py foo_result = foo() - if foo_result.is_err() and foo_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if foo_result.is_fatal(): return foo_result ``` @@ -42,6 +42,9 @@ def is_ok(self) -> bool: def is_err(self) -> bool: return isinstance(self, Err) + def is_fatal(self) -> bool: + return self.is_err() and self.unwrap_err()[1] == ErrorSeverity.Fatal + def unwrap(self) -> T: if self.is_ok(): return self.value # type: ignore diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 5e9bf1b806..0537618b88 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -38,17 +38,17 @@ def mapAllComponents( comp_ref = guid_component(component) fill_info_result = fill_info(partsData, None) - if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if fill_info_result.is_fatal(): return fill_info_result partDefinition = partsData.part_definitions[comp_ref] fill_info_result = fill_info(partDefinition, component, comp_ref) - if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if fill_info_result.is_fatal(): return fill_info_result physical_properties_result = PhysicalProperties.getPhysicalProperties(component, partDefinition.physical_data) - if physical_properties_result.is_err() and physical_properties_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if physical_properties_result.is_fatal(): return physical_properties_result partDefinition.dynamic = options.exportMode != ExportMode.FIELD @@ -60,18 +60,18 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non part_body = partDefinition.bodies.add() fill_info_result = fill_info(part_body, body) - if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if fill_info_result.is_fatal(): return fill_info_result part_body.part = comp_ref if isinstance(body, adsk.fusion.BRepBody): parse_result = parseBRep(body, options, part_body.triangle_mesh) - if parse_result.is_err() and parse_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if parse_result.is_fatal(): return parse_result else: parse_result = parseMesh(body, options, part_body.triangle_mesh) - if parse_result.is_err() and parse_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if parse_result.is_fatal(): return parse_result appearance_key = "{}_{}".format(body.appearance.name, body.appearance.id) @@ -85,11 +85,11 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non for body in component.bRepBodies: process_result = processBody(body) - if process_result.is_err() and process_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if process_result.is_fatal(): return process_result for body in component.meshBodies: process_result = processBody(body) - if process_result.is_err() and process_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if process_result.is_fatal(): return process_result return Ok(None) @@ -110,7 +110,7 @@ def parseComponentRoot( node.value = mapConstant fill_info_result = fill_info(part, component, mapConstant) - if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if fill_info_result.is_fatal(): return fill_info_result def_map = partsData.part_definitions @@ -157,7 +157,7 @@ def parseChildOccurrence( node.value = mapConstant fill_info_result = fill_info(part, occurrence, mapConstant) - if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if fill_info_result.is_fatal(): return fill_info_result collision_attr = occurrence.attributes.itemByName("synthesis", "collision_off") @@ -244,7 +244,7 @@ def parseBRep( return Err(f"Failed to calculate mesh for {body.name}", ErrorSeverity.Error) fill_info_result = fill_info(trimesh, body) - if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if fill_info_result.is_fatal(): return fill_info_result trimesh.has_volume = True @@ -268,7 +268,7 @@ def parseMesh( return Err("Component Mesh was None", ErrorSeverity.Fatal) fill_info_result = fill_info(trimesh, meshBody) - if fill_info_result.is_err() and fill_info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if fill_info_result.is_fatal(): return fill_info_result trimesh.has_volume = True diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 2326169ccc..7bfc55d59d 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -337,7 +337,7 @@ def _recurseLink(self, simNode: SimulationNode) -> Result[None]: simNode.edges.append(edge) recurse_result = self._recurseLink(connectedAxis) - if recurse_result.is_err() and recurse_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if recurse_result.is_fatal(): return recurse_result return Ok(None) @@ -404,7 +404,7 @@ def _populateNode( populate_result = self._populateNode( occurrence, node, OccurrenceRelationship.TRANSFORM, is_ground=is_ground ) - if populate_result.is_err() and populate_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if populate_result.is_fatal(): return populate_result # if not is_ground: # THIS IS A BUG - OCCURRENCE ACCESS VIOLATION @@ -433,7 +433,7 @@ def _populateNode( (OccurrenceRelationship.CONNECTION if rigid else OccurrenceRelationship.NEXT), is_ground=is_ground, ) - if populate_result.is_err() and populate_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if populate_result.is_fatal(): return populate_result else: # Check if this joint occurance violation is really a fatal error or just something we should filter on @@ -496,7 +496,7 @@ def buildJointPartHierarchy( rootSimNode = jointParser.groundSimNode populate_joint_result = populateJoint(rootSimNode, joints, progressDialog) - if populate_joint_result.is_err() and populate_joint_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if populate_joint_result.is_fatal(): return populate_joint_result # 1. Get Node @@ -547,7 +547,7 @@ def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDia # next in line to be populated for edge in simNode.edges: populate_joint_result = populateJoint(cast(SimulationNode, edge.node), joints, progressDialog) - if populate_joint_result.is_err() and populate_joint_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if populate_joint_result.is_fatal(): return populate_joint_result return Ok(None) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index 21274fb4d2..1399a3e673 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -79,7 +79,7 @@ def populateJoints( assembly: assembly_pb2.Assembly, ) -> Result[None]: info_result = fill_info(joints, None) - if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if info_result.is_fatal(): return info_result # This is for creating all of the Joint Definition objects @@ -90,12 +90,12 @@ def populateJoints( # Add the grounded joints object - TODO: rename some of the protobuf stuff for the love of god joint_definition_ground = joints.joint_definitions["grounded"] info_result = construct_info("grounded", joint_definition_ground) - if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if info_result.is_fatal(): return info_result joint_instance_ground = joints.joint_instances["grounded"] info_result = construct_info("grounded", joint_instance_ground) - if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if info_result.is_fatal(): return info_result joint_instance_ground.joint_reference = joint_definition_ground.info.GUID @@ -132,7 +132,7 @@ def populateJoints( signal = signals.signal_map[guid] info_result = construct_info(joint.name, signal, GUID=guid) - if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if info_result.is_fatal(): return info_result signal.io = signal_pb2.IOType.OUTPUT @@ -147,7 +147,7 @@ def populateJoints( motor = joints.motor_definitions[joint.entityToken] info_result = fill_info(motor, joint) - if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if info_result.is_fatal(): return info_result simple_motor = motor.simple_motor @@ -161,7 +161,7 @@ def populateJoints( # signals.signal_map.remove(guid) joint_result = _addJointInstance(joint, joint_instance, joint_definition, signals, options) - if joint_result.is_err() and joint_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if joint_result.is_fatal(): return joint_result # adds information for joint motion and limits @@ -176,7 +176,7 @@ def populateJoints( def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> Result[None]: info_result = fill_info(joint_definition, joint) - if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if info_result.is_fatal(): return info_result jointPivotTranslation = _jointOrigin(joint) @@ -205,7 +205,7 @@ def _addJointInstance( options: ExporterOptions, ) -> Result[None]: info_result = fill_info(joint_instance, joint) - if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if info_result.is_fatal(): return info_result # because there is only one and we are using the token - should be the same @@ -250,7 +250,7 @@ def _addJointInstance( signal = signals.signal_map[guid] info_result = construct_info("joint_signal", signal, GUID=guid) - if info_result.is_err() and info_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if info_result.is_fatal(): return info_result signal.io = signal_pb2.IOType.OUTPUT diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index c0824c43bb..ec2deaac90 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -33,7 +33,7 @@ def mapAllPhysicalMaterials( progressDialog: PDMessage, ) -> Result[None]: set_result = setDefaultMaterial(materials.physicalMaterials["default"], options) - if set_result.is_err() and set_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if set_result.is_fatal(): return set_result for material in physicalMaterials: @@ -46,7 +46,7 @@ def mapAllPhysicalMaterials( newmaterial = materials.physicalMaterials[material.id] material_result = getPhysicalMaterialData(material, newmaterial, options) - if material_result.is_err() and material_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if material_result.is_fatal(): return material_result return Ok(None) @@ -174,7 +174,7 @@ def mapAllAppearances( # in case there are no appearances on a body # this is just a color tho set_default_result = setDefaultAppearance(materials.appearances["default"]) - if set_default_result.is_err() and set_default_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if set_default_result.is_fatal(): return set_default_result fill_info_result = fill_info(materials, None) @@ -191,7 +191,7 @@ def mapAllAppearances( material = materials.appearances["{}_{}".format(appearance.name, appearance.id)] material_result = getMaterialAppearance(appearance, options, material) - if material_result.is_err() and material_result.unwrap_err()[1] == ErrorSeverity.Fatal: + if material_result.is_fatal(): return material_result return Ok(None) From 598ba21d14e0dbedbb454c71714063b26f8e8d9d Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 7 Jul 2025 11:59:34 -0700 Subject: [PATCH 53/68] refactor: User canceling export is handled by the error handling system --- .../src/Parser/SynthesisParser/Components.py | 8 ++++---- .../src/Parser/SynthesisParser/JointHierarchy.py | 8 ++++---- .../src/Parser/SynthesisParser/Materials.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 0537618b88..0bab16e431 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -32,7 +32,7 @@ def mapAllComponents( for component in design.allComponents: adsk.doEvents() if progressDialog.wasCancelled(): - raise RuntimeError("User canceled export") + return Err("User canceled export", ErrorSeverity.Fatal) progressDialog.addComponent(component.name) comp_ref = guid_component(component) @@ -55,7 +55,7 @@ def mapAllComponents( def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[None]: if progressDialog.wasCancelled(): - raise RuntimeError("User canceled export") + return Err("User canceled export", ErrorSeverity.Fatal) if body.isLightBulbOn: part_body = partDefinition.bodies.add() @@ -120,7 +120,7 @@ def parseComponentRoot( for occur in component.occurrences: if progressDialog.wasCancelled(): - raise RuntimeError("User canceled export") + return Err("User canceled export", ErrorSeverity.Fatal) if occur.isLightBulbOn: child_node = types_pb2.Node() @@ -199,7 +199,7 @@ def parseChildOccurrence( for occur in occurrence.childOccurrences: if progressDialog.wasCancelled(): - raise RuntimeError("User canceled export") + return Err("User canceled export", ErrorSeverity.Fatal) if occur.isLightBulbOn: child_node = types_pb2.Node() diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 7bfc55d59d..3b33a3ae38 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -510,7 +510,7 @@ def buildJointPartHierarchy( # now add each wheel to the root I believe if progressDialog.wasCancelled(): - raise RuntimeError("User canceled export") + return Err("User canceled export", ErrorSeverity.Fatal) return Ok(None) @@ -524,7 +524,7 @@ def buildJointPartHierarchy( def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDialog: PDMessage) -> Result[None]: if progressDialog.wasCancelled(): - raise RuntimeError("User canceled export") + return Err("User canceled export", ErrorSeverity.Fatal) if not simNode.joint: proto_joint = joints.joint_instances["grounded"] @@ -557,9 +557,9 @@ def createTreeParts( relationship: RelationshipBase | None, node: types_pb2.Node, progressDialog: PDMessage, -) -> None: +) -> Result[None]: if progressDialog.wasCancelled(): - raise RuntimeError("User canceled export") + return Err("User canceled export", ErrorSeverity.Fatal) # if it's the next part just exit early for our own sanity # This shouldn't be fatal nor even an error diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index ec2deaac90..027794f874 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -42,7 +42,7 @@ def mapAllPhysicalMaterials( progressDialog.addMaterial(material.name) if progressDialog.wasCancelled(): - raise RuntimeError("User canceled export") + return Err("User canceled export", ErrorSeverity.Fatal) newmaterial = materials.physicalMaterials[material.id] material_result = getPhysicalMaterialData(material, newmaterial, options) @@ -187,7 +187,7 @@ def mapAllAppearances( # NOTE I'm not sure if this should be integrated with the error handling system or not, since it's fully intentional and immediantly aborts, which is the desired behavior # TODO Talk to Brandon about this if progressDialog.wasCancelled(): - raise RuntimeError("User canceled export") + return Err("User canceled export", ErrorSeverity.Fatal) material = materials.appearances["{}_{}".format(appearance.name, appearance.id)] material_result = getMaterialAppearance(appearance, options, material) From 6ca14f39e278a1fabbef3570da3b765537646c3d Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Mon, 7 Jul 2025 12:02:58 -0700 Subject: [PATCH 54/68] chore: remove leaked optional chaining --- fission/src/mirabuf/MirabufParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index 5dd3b5366c..1b81165f1c 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -301,7 +301,7 @@ class MirabufParser { const mat = partInstance.transform ? MirabufTransform_ThreeMatrix4(partInstance.transform) - : def?.baseTransform + : def.baseTransform ? MirabufTransform_ThreeMatrix4(def.baseTransform) : new THREE.Matrix4().identity() From 86c78740a0fa90c7e45d1f6f047b14aa37ce2624 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 9 Jul 2025 14:40:38 -0700 Subject: [PATCH 55/68] refactor: function decorator handles top-level errors --- .../SynthesisFusionAddin/src/ErrorHandling.py | 13 +++ .../src/Parser/SynthesisParser/Components.py | 4 +- .../Parser/SynthesisParser/JointHierarchy.py | 3 +- .../src/Parser/SynthesisParser/Joints.py | 4 +- .../src/Parser/SynthesisParser/Materials.py | 4 +- .../src/Parser/SynthesisParser/Parser.py | 98 +++++++++---------- 6 files changed, 68 insertions(+), 58 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 5e80ff36d1..238061bfb6 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -1,3 +1,4 @@ +from collections.abc import Callable from enum import Enum from typing import Generic, TypeVar @@ -111,3 +112,15 @@ def __repr__(self) -> str: def write_error(self) -> None: logger.log(self.severity.value, self.message) + + +def handle_err_top(func: Callable[..., Result[None]]) -> Callable[[], None]: + + def wrapper(): + result = func() + if result.is_err(): + message, severity = result.unwrap_err() + if severity == ErrorSeverity.Fatal: + app = adsk.core.Application.get() + app.userInterface.messageBox(f"Fatal Error Encountered: {message}") + return wrapper diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 0bab16e431..1028869d40 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -6,7 +6,7 @@ from google.protobuf.message import Error from requests.models import parse_header_links -from src.ErrorHandling import Err, ErrorSeverity, Ok, Result +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result, handle_err_top from src.Logging import getLogger, logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser import PhysicalProperties @@ -21,6 +21,7 @@ # TODO: Impelement Material overrides +@handle_err_top def mapAllComponents( design: adsk.fusion.Design, options: ExporterOptions, @@ -95,6 +96,7 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> Result[Non return Ok(None) +@handle_err_top def parseComponentRoot( component: adsk.fusion.Component, progressDialog: PDMessage, diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 3b33a3ae38..474f790144 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -7,7 +7,7 @@ from google.protobuf.message import Error from src import gm -from src.ErrorHandling import Err, ErrorSeverity, Ok, Result +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result, handle_err_top from src.Logging import getLogger, logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage @@ -481,6 +481,7 @@ def searchForGrounded( # ________________________ Build implementation ______________________ # +@handle_err_top def buildJointPartHierarchy( design: adsk.fusion.Design, joints: joint_pb2.Joints, diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py index 1399a3e673..e7b7a1e239 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -29,7 +29,7 @@ import adsk.core import adsk.fusion -from src.ErrorHandling import Err, ErrorSeverity, Ok, Result +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result, handle_err_top from src.Logging import getLogger from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage @@ -70,6 +70,7 @@ # 3. connect all instances with graphcontainer +@handle_err_top def populateJoints( design: adsk.fusion.Design, joints: joint_pb2.Joints, @@ -539,6 +540,7 @@ def _jointOrigin(fusionJoint: Union[adsk.fusion.Joint, adsk.fusion.AsBuiltJoint] return adsk.core.Point3D.create(origin.x + offsetX, origin.y + offsetY, origin.z + offsetZ) +@handle_err_top def createJointGraph( suppliedJoints: list[Joint], _wheels: list[Wheel], diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 027794f874..9f2179dc90 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -1,6 +1,6 @@ import adsk.core -from src.ErrorHandling import Err, ErrorSeverity, Ok, Result +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result, handle_err_top from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import construct_info, fill_info @@ -26,6 +26,7 @@ } +@handle_err_top def mapAllPhysicalMaterials( physicalMaterials: list[material_pb2.PhysicalMaterial], materials: material_pb2.Materials, @@ -165,6 +166,7 @@ def getPhysicalMaterialData( return Ok(None) +@handle_err_top def mapAllAppearances( appearances: list[material_pb2.Appearance], materials: material_pb2.Materials, diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 38b7bf612b..de87939462 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -44,6 +44,7 @@ def export(self) -> None: return assembly_out = assembly_pb2.Assembly() + # This can't use the wrapper because there are lower level calls of this utility function handle_err_top( fill_info( assembly_out, @@ -79,45 +80,38 @@ def export(self) -> None: progressDialog, ) - handle_err_top( - Materials.mapAllAppearances( - design.appearances, - assembly_out.data.materials, - self.exporterOptions, - self.pdMessage, - ) + Materials.mapAllAppearances( + design.appearances, + assembly_out.data.materials, + self.exporterOptions, + self.pdMessage, ) + - handle_err_top( - Materials.mapAllPhysicalMaterials( - design.materials, - assembly_out.data.materials, - self.exporterOptions, - self.pdMessage, - ) + Materials.mapAllPhysicalMaterials( + design.materials, + assembly_out.data.materials, + self.exporterOptions, + self.pdMessage, ) - handle_err_top( - Components.mapAllComponents( - design, - self.exporterOptions, - self.pdMessage, - assembly_out.data.parts, - assembly_out.data.materials, - ) + Components.mapAllComponents( + design, + self.exporterOptions, + self.pdMessage, + assembly_out.data.parts, + assembly_out.data.materials, ) rootNode = types_pb2.Node() - handle_err_top( - Components.parseComponentRoot( - design.rootComponent, - self.pdMessage, - self.exporterOptions, - assembly_out.data.parts, - assembly_out.data.materials.appearances, - rootNode, - ) + Components.parseComponentRoot( + design.rootComponent, + self.pdMessage, + self.exporterOptions, + assembly_out.data.parts, + assembly_out.data.materials.appearances, + rootNode, ) Components.mapRigidGroups(design.rootComponent, assembly_out.data.joints) @@ -125,35 +119,30 @@ def export(self) -> None: assembly_out.design_hierarchy.nodes.append(rootNode) # Problem Child - handle_err_top( - Joints.populateJoints( - design, - assembly_out.data.joints, - assembly_out.data.signals, - self.pdMessage, - self.exporterOptions, - assembly_out, - ) + Joints.populateJoints( + design, + assembly_out.data.joints, + assembly_out.data.signals, + self.pdMessage, + self.exporterOptions, + assembly_out, ) # add condition in here for advanced joints maybe idk # should pre-process to find if there are any grounded joints at all # that or add code to existing parser to determine leftovers - handle_err_top( - Joints.createJointGraph( - self.exporterOptions.joints, - self.exporterOptions.wheels, - assembly_out.joint_hierarchy, - self.pdMessage, - ) + Joints.createJointGraph( + self.exporterOptions.joints, + self.exporterOptions.wheels, + assembly_out.joint_hierarchy, + self.pdMessage, ) - handle_err_top( - JointHierarchy.buildJointPartHierarchy( - design, assembly_out.data.joints, self.exporterOptions, self.pdMessage - ) + JointHierarchy.buildJointPartHierarchy( + design, assembly_out.data.joints, self.exporterOptions, self.pdMessage ) + # These don't have an effect, I forgot how this is suppose to work # progressDialog.message = "Taking Photo for thumbnail..." @@ -269,9 +258,10 @@ def export(self) -> None: logger.debug(debug_output.strip()) -def handle_err_top[T](err: Result[T]) -> None: - if err.is_err(): - message, severity = err.unwrap_err() +def handle_err_top[T](result: Result[T]): + if result.is_err(): + message, severity = result.unwrap_err() if severity == ErrorSeverity.Fatal: app = adsk.core.Application.get() app.userInterface.messageBox(f"Fatal Error Encountered: {message}") + From e92c5f09b76191ccb1f70c7251117a93710f51db Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 9 Jul 2025 14:57:35 -0700 Subject: [PATCH 56/68] refactor: replace runtime exceptions with result --- .../src/Parser/SynthesisParser/Utilities.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index 6eb56a7f44..6c25164967 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -1,4 +1,5 @@ import math +from typing import Never import uuid import adsk.core @@ -68,7 +69,7 @@ def construct_info( return Ok(None) -def rad_to_deg(rad): # type: ignore +def rad_to_deg(rad: float) -> float: """Converts radians to degrees Args: @@ -80,16 +81,16 @@ def rad_to_deg(rad): # type: ignore return (rad * 180) / math.pi -def throwZero(): # type: ignore +def throwZero() -> Err[tuple[float, float, float, float]]: # type: ignore """Errors on incorrect quat values Raises: RuntimeError: Error describing the issue """ - raise RuntimeError("While computing the quaternion the trace was reported as 0 which is invalid") + return Err("While computing the quaternion the trace was reported as 0 which is invalid", ErrorSeverity.Fatal) -def spatial_to_quaternion(mat): # type: ignore +def spatial_to_quaternion(mat: list[float]) -> Result[tuple[float, float, float, float]]: """Takes a 1D Spatial Transform Matrix and derives rotational quaternion I wrote this however it is difficult to extensibly test so use with caution @@ -107,7 +108,7 @@ def spatial_to_quaternion(mat): # type: ignore if trace > 0: s = math.sqrt(trace + 1.0) * 2 if s == 0: - throwZero() + return throwZero() qw = 0.25 * s qx = (mat[9] - mat[6]) / s qy = (mat[2] - mat[8]) / s @@ -115,7 +116,7 @@ def spatial_to_quaternion(mat): # type: ignore elif (mat[0] > mat[5]) and (mat[0] > mat[8]): s = math.sqrt(1.0 + mat[0] - mat[5] - mat[10]) * 2.0 if s == 0: - throwZero() + return throwZero() qw = (mat[9] - mat[6]) / s qx = 0.25 * s qy = (mat[1] + mat[4]) / s @@ -123,7 +124,7 @@ def spatial_to_quaternion(mat): # type: ignore elif mat[5] > mat[10]: s = math.sqrt(1.0 + mat[5] - mat[0] - mat[10]) * 2.0 if s == 0: - throwZero() + return throwZero() qw = (mat[2] - mat[8]) / s qx = (mat[1] + mat[4]) / s qy = 0.25 * s @@ -131,7 +132,7 @@ def spatial_to_quaternion(mat): # type: ignore else: s = math.sqrt(1.0 + mat[10] - mat[0] - mat[5]) * 2.0 if s == 0: - throwZero() + return throwZero() qw = (mat[4] - mat[1]) / s qx = (mat[2] + mat[8]) / s qy = (mat[6] + mat[9]) / s @@ -141,13 +142,13 @@ def spatial_to_quaternion(mat): # type: ignore qx, qy, qz, qw = normalize_quaternion(qx, qy, qz, qw) # So these quat values need to be reversed? I have no idea why at the moment - return round(qx, 13), round(-qy, 13), round(-qz, 13), round(qw, 13) + return Ok((round(qx, 13), round(-qy, 13), round(-qz, 13), round(qw, 13))) else: - raise RuntimeError("Supplied matrix to spatial_to_quaternion is not a 1D spatial matrix in size.") + return Err("Supplied matrix to spatial_to_quaternion is not a 1D spatial matrix in size.", ErrorSeverity.Fatal) -def normalize_quaternion(x, y, z, w): # type: ignore +def normalize_quaternion(x: float, y: float, z: float, w: float) -> tuple[float, float, float, float]: f = 1.0 / math.sqrt((x * x) + (y * y) + (z * z) + (w * w)) return x * f, y * f, z * f, w * f From d1987d82cef1905148c8ba74ac43156f25485478 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 9 Jul 2025 14:59:27 -0700 Subject: [PATCH 57/68] chore: remove unused utility functions --- .../src/Parser/SynthesisParser/Utilities.py | 90 ------------------- 1 file changed, 90 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index 6c25164967..8cf7fe26d4 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -68,93 +68,3 @@ def construct_info( return Ok(None) - -def rad_to_deg(rad: float) -> float: - """Converts radians to degrees - - Args: - rad (float): radians unit - - Returns: - float: degrees - """ - return (rad * 180) / math.pi - - -def throwZero() -> Err[tuple[float, float, float, float]]: # type: ignore - """Errors on incorrect quat values - - Raises: - RuntimeError: Error describing the issue - """ - return Err("While computing the quaternion the trace was reported as 0 which is invalid", ErrorSeverity.Fatal) - - -def spatial_to_quaternion(mat: list[float]) -> Result[tuple[float, float, float, float]]: - """Takes a 1D Spatial Transform Matrix and derives rotational quaternion - - I wrote this however it is difficult to extensibly test so use with caution - Args: - mat (list): spatial transform matrix - - Raises: - RuntimeError: matrix is not of the correct size - - Returns: - x, y, z, w: float representation of quaternions - """ - if len(mat) > 15: - trace = mat[0] + mat[5] + mat[10] - if trace > 0: - s = math.sqrt(trace + 1.0) * 2 - if s == 0: - return throwZero() - qw = 0.25 * s - qx = (mat[9] - mat[6]) / s - qy = (mat[2] - mat[8]) / s - qz = (mat[4] - mat[1]) / s - elif (mat[0] > mat[5]) and (mat[0] > mat[8]): - s = math.sqrt(1.0 + mat[0] - mat[5] - mat[10]) * 2.0 - if s == 0: - return throwZero() - qw = (mat[9] - mat[6]) / s - qx = 0.25 * s - qy = (mat[1] + mat[4]) / s - qz = (mat[2] + mat[8]) / s - elif mat[5] > mat[10]: - s = math.sqrt(1.0 + mat[5] - mat[0] - mat[10]) * 2.0 - if s == 0: - return throwZero() - qw = (mat[2] - mat[8]) / s - qx = (mat[1] + mat[4]) / s - qy = 0.25 * s - qz = (mat[6] + mat[9]) / s - else: - s = math.sqrt(1.0 + mat[10] - mat[0] - mat[5]) * 2.0 - if s == 0: - return throwZero() - qw = (mat[4] - mat[1]) / s - qx = (mat[2] + mat[8]) / s - qy = (mat[6] + mat[9]) / s - qz = 0.25 * s - - # normalizes the value - as demanded by unity - qx, qy, qz, qw = normalize_quaternion(qx, qy, qz, qw) - - # So these quat values need to be reversed? I have no idea why at the moment - return Ok((round(qx, 13), round(-qy, 13), round(-qz, 13), round(qw, 13))) - - else: - return Err("Supplied matrix to spatial_to_quaternion is not a 1D spatial matrix in size.", ErrorSeverity.Fatal) - - -def normalize_quaternion(x: float, y: float, z: float, w: float) -> tuple[float, float, float, float]: - f = 1.0 / math.sqrt((x * x) + (y * y) + (z * z) + (w * w)) - return x * f, y * f, z * f, w * f - - -def _getAngleTo(vec_origin: list, vec_current: adsk.core.Vector3D) -> int: # type: ignore - origin = adsk.core.Vector3D.create(vec_origin[0], vec_origin[1], vec_origin[2]) - val = origin.angleTo(vec_current) - deg = val * (180 / math.pi) - return val # type: ignore From 2f5b7ab9c5e7f2724f2c7cce0c82157843e2159c Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 9 Jul 2025 15:25:31 -0700 Subject: [PATCH 58/68] feat: caller function and line number printed in error message --- .../SynthesisFusionAddin/src/ErrorHandling.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 238061bfb6..cd361e8bcf 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -1,3 +1,4 @@ +import inspect from collections.abc import Callable from enum import Enum from typing import Generic, TypeVar @@ -100,24 +101,32 @@ class Err(Result[T]): message: str severity: ErrorSeverity + function: str + line: int def __init__(self, message: str, severity: ErrorSeverity): self.message = message self.severity = severity + frame = inspect.currentframe() + caller_frame = inspect.getouterframes(frame)[2] + + self.function = caller_frame.function + self.line = caller_frame.lineno + self.write_error() def __repr__(self) -> str: return f"Err({self.message})" def write_error(self) -> None: - logger.log(self.severity.value, self.message) + logger.log(self.severity.value, f"In `{self.function}` on line {self.line}: {self.message}") -def handle_err_top(func: Callable[..., Result[None]]) -> Callable[[], None]: +def handle_err_top(func: Callable[..., Result[None]]) -> Callable[..., None]: - def wrapper(): - result = func() + def wrapper(*args, **kwargs): # type: ignore + result = func(*args, **kwargs) if result.is_err(): message, severity = result.unwrap_err() if severity == ErrorSeverity.Fatal: From e4762b3daef6665c9e26d32aa6306652616e9d95 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 9 Jul 2025 15:30:59 -0700 Subject: [PATCH 59/68] fix: correct call stack --- exporter/SynthesisFusionAddin/src/ErrorHandling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index cd361e8bcf..84ea3e9beb 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -109,7 +109,7 @@ def __init__(self, message: str, severity: ErrorSeverity): self.severity = severity frame = inspect.currentframe() - caller_frame = inspect.getouterframes(frame)[2] + caller_frame = inspect.getouterframes(frame)[1] self.function = caller_frame.function self.line = caller_frame.lineno From ccd72c627f6fa68ce593bf11c64cac74a01beadf Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 9 Jul 2025 15:48:46 -0700 Subject: [PATCH 60/68] fix: message logging for field exports and reloading --- exporter/SynthesisFusionAddin/src/ErrorHandling.py | 13 ++++++++----- .../src/Parser/SynthesisParser/Components.py | 2 +- .../SynthesisFusionAddin/src/UI/ConfigCommand.py | 3 +++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index 84ea3e9beb..a0e7b18e5e 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -1,4 +1,6 @@ +import adsk.core import inspect + from collections.abc import Callable from enum import Enum from typing import Generic, TypeVar @@ -105,22 +107,23 @@ class Err(Result[T]): line: int def __init__(self, message: str, severity: ErrorSeverity): - self.message = message - self.severity = severity - frame = inspect.currentframe() caller_frame = inspect.getouterframes(frame)[1] self.function = caller_frame.function self.line = caller_frame.lineno + self.severity = severity + self.message = f"In `{self.function}` on line {self.line}: {message}" + + self.write_error() def __repr__(self) -> str: return f"Err({self.message})" def write_error(self) -> None: - logger.log(self.severity.value, f"In `{self.function}` on line {self.line}: {self.message}") + logger.log(self.severity.value, self.message) def handle_err_top(func: Callable[..., Result[None]]) -> Callable[..., None]: @@ -131,5 +134,5 @@ def wrapper(*args, **kwargs): # type: ignore message, severity = result.unwrap_err() if severity == ErrorSeverity.Fatal: app = adsk.core.Application.get() - app.userInterface.messageBox(f"Fatal Error Encountered: {message}") + app.userInterface.messageBox(f"Fatal Error Encountered {message}") return wrapper diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 1028869d40..a37d67cd79 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -178,7 +178,7 @@ def parseChildOccurrence( if occurrence.component.material: part.physical_material = occurrence.component.material.id else: - return Err(f"Component Material is None", ErrorSeverity.Fatal) + __: Err[None] = Err(f"Component Material is None", ErrorSeverity.Warning) def_map = partsData.part_definitions diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 3ecefa53e2..3bafa28236 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -11,6 +11,7 @@ import adsk.core import adsk.fusion +from src import Logging import src.Parser.SynthesisParser.Parser as Parser import src.UI.GamepieceConfigTab as GamepieceConfigTab import src.UI.GeneralConfigTab as GeneralConfigTab @@ -30,6 +31,8 @@ INPUTS_ROOT: adsk.core.CommandInputs +logger = Logging.getLogger() + def reload() -> None: """Reloads the sub modules to reflect any changes made during development.""" From 1121ac676952e947702a10c227277b2941d77cc5 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Wed, 9 Jul 2025 15:53:07 -0700 Subject: [PATCH 61/68] chore: format and fix mypy --- exporter/SynthesisFusionAddin/src/ErrorHandling.py | 10 +++++----- .../src/Parser/SynthesisParser/JointHierarchy.py | 14 +++++++++++--- .../src/Parser/SynthesisParser/Materials.py | 1 - .../src/Parser/SynthesisParser/Parser.py | 9 ++------- .../src/Parser/SynthesisParser/Utilities.py | 3 +-- .../SynthesisFusionAddin/src/UI/ConfigCommand.py | 3 +-- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/ErrorHandling.py b/exporter/SynthesisFusionAddin/src/ErrorHandling.py index a0e7b18e5e..f529fc2d71 100644 --- a/exporter/SynthesisFusionAddin/src/ErrorHandling.py +++ b/exporter/SynthesisFusionAddin/src/ErrorHandling.py @@ -1,10 +1,10 @@ -import adsk.core import inspect - from collections.abc import Callable from enum import Enum from typing import Generic, TypeVar +import adsk.core + from .Logging import getLogger logger = getLogger() @@ -116,7 +116,6 @@ def __init__(self, message: str, severity: ErrorSeverity): self.severity = severity self.message = f"In `{self.function}` on line {self.line}: {message}" - self.write_error() def __repr__(self) -> str: @@ -127,12 +126,13 @@ def write_error(self) -> None: def handle_err_top(func: Callable[..., Result[None]]) -> Callable[..., None]: - - def wrapper(*args, **kwargs): # type: ignore + + def wrapper(*args, **kwargs): # type: ignore result = func(*args, **kwargs) if result.is_err(): message, severity = result.unwrap_err() if severity == ErrorSeverity.Fatal: app = adsk.core.Application.get() app.userInterface.messageBox(f"Fatal Error Encountered {message}") + return wrapper diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 474f790144..d2f7f940c4 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -541,7 +541,9 @@ def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDia root = types_pb2.Node() # construct body tree if possible - createTreeParts(simNode.data, OccurrenceRelationship.CONNECTION, root, progressDialog) + tree_parts_result = createTreeParts(simNode.data, OccurrenceRelationship.CONNECTION, root, progressDialog) + if tree_parts_result.is_fatal(): + return tree_parts_result proto_joint.parts.nodes.append(root) @@ -565,7 +567,7 @@ def createTreeParts( # if it's the next part just exit early for our own sanity # This shouldn't be fatal nor even an error if relationship == OccurrenceRelationship.NEXT or dynNode.data.isLightBulbOn == False: - return + return Ok(None) # set the occurrence / component id to reference the part @@ -591,5 +593,11 @@ def createTreeParts( # recurse and add all children connections for edge in dynNode.edges: child_node = types_pb2.Node() - createTreeParts(cast(DynamicOccurrenceNode, edge.node), edge.relationship, child_node, progressDialog) + tree_parts_result = createTreeParts( + cast(DynamicOccurrenceNode, edge.node), edge.relationship, child_node, progressDialog + ) + if tree_parts_result.is_fatal(): + return tree_parts_result node.children.append(child_node) + + return Ok(None) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py index 9f2179dc90..2db13a3dae 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -259,7 +259,6 @@ def getMaterialAppearance( appearance.roughness = roughnessProp.value # Thank Liam for this. - # TODO Test if this is should be an error that we're just ignoring, or if it's actually just something we can skip over modelItem = properties.itemById("interior_model") if modelItem: matModelType = modelItem.value diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index de87939462..510116d2d7 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -86,7 +86,6 @@ def export(self) -> None: self.exporterOptions, self.pdMessage, ) - Materials.mapAllPhysicalMaterials( design.materials, @@ -139,10 +138,7 @@ def export(self) -> None: self.pdMessage, ) - JointHierarchy.buildJointPartHierarchy( - design, assembly_out.data.joints, self.exporterOptions, self.pdMessage - ) - + JointHierarchy.buildJointPartHierarchy(design, assembly_out.data.joints, self.exporterOptions, self.pdMessage) # These don't have an effect, I forgot how this is suppose to work # progressDialog.message = "Taking Photo for thumbnail..." @@ -258,10 +254,9 @@ def export(self) -> None: logger.debug(debug_output.strip()) -def handle_err_top[T](result: Result[T]): +def handle_err_top[T](result: Result[T]) -> None: if result.is_err(): message, severity = result.unwrap_err() if severity == ErrorSeverity.Fatal: app = adsk.core.Application.get() app.userInterface.messageBox(f"Fatal Error Encountered: {message}") - diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py index 8cf7fe26d4..b3b23db4bd 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -1,6 +1,6 @@ import math -from typing import Never import uuid +from typing import Never import adsk.core import adsk.fusion @@ -67,4 +67,3 @@ def construct_info( proto_obj.info.GUID = str(uuid.uuid4()) return Ok(None) - diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 3bafa28236..2f9bcb1eee 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -11,12 +11,11 @@ import adsk.core import adsk.fusion -from src import Logging import src.Parser.SynthesisParser.Parser as Parser import src.UI.GamepieceConfigTab as GamepieceConfigTab import src.UI.GeneralConfigTab as GeneralConfigTab import src.UI.JointConfigTab as JointConfigTab -from src import APP_WEBSITE_URL, gm +from src import APP_WEBSITE_URL, Logging, gm from src.APS.APS import getAuth, getUserInfo from src.Logging import logFailure from src.Parser.ExporterOptions import ExporterOptions From a471cd62951f2508c44e0433e26ede5a79059355 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 10 Jul 2025 10:55:52 -0700 Subject: [PATCH 62/68] refactor: updated error handling in APS system --- exporter/SynthesisFusionAddin/src/APS/APS.py | 149 ++++++++---------- .../Parser/SynthesisParser/JointHierarchy.py | 10 +- .../src/Parser/SynthesisParser/Parser.py | 15 +- 3 files changed, 81 insertions(+), 93 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index 65ab01e4f7..c4f4d0a776 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -12,6 +12,7 @@ import requests from src import ADDIN_PATH, gm +from src.ErrorHandling import Err, ErrorSeverity, Ok, Result from src.Logging import getLogger logger = getLogger() @@ -149,7 +150,7 @@ def refreshAuthToken() -> None: gm.ui.messageBox("Please sign in again.") -def loadUserInfo() -> APSUserInfo | None: +def loadUserInfo() -> Result[APSUserInfo]: global APS_AUTH if not APS_AUTH: return None @@ -174,22 +175,18 @@ def loadUserInfo() -> APSUserInfo | None: company=data["company"], picture=data["picture"], ) - return APS_USER_INFO + return Ok(APS_USER_INFO) except urllib.request.HTTPError as e: removeAuth() - logger.error(f"User Info Error:\n{e.code} - {e.reason}") - gm.ui.messageBox("Please sign in again.") - finally: - return None - + return Err(f"User Info Error:\n{e.code} - {e.reason}\nPlease sign in again", ErrorSeverity.Fatal) -def getUserInfo() -> APSUserInfo | None: +def getUserInfo() -> Result[APSUserInfo]: if APS_USER_INFO is not None: - return APS_USER_INFO + return Ok(APS_USER_INFO) return loadUserInfo() -def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_display_name: str) -> str | None: +def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_display_name: str) -> Result[str]: """ creates a folder on an APS project @@ -219,18 +216,17 @@ def create_folder(auth: str, project_id: str, parent_folder_id: str, folder_disp f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/folders", headers=headers, json=data ) if not res.ok: - gm.ui.messageBox(f"Failed to create new folder: {res.text}", "ERROR") - return None + return Err(f"Failed to create new folder: {res.text}", ErrorSeverity.Fatal) json: dict[str, Any] = res.json() id: str = json["data"]["id"] - return id + return Ok(id) def file_path_to_file_name(file_path: str) -> str: return file_path.split("/").pop() -def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_contents: str) -> str | None: +def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_contents: str) -> Result[str]: """ uploads mirabuf file to a specific folder in an APS project the folder and project must be created and valid @@ -261,34 +257,37 @@ def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_content # data:create global APS_AUTH if APS_AUTH is None: - gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR") - return None + return Err("You must login to upload designs to APS (USER ERROR)", ErrorSeverity.Fatal) auth = APS_AUTH.access_token # Get token from APS API later new_folder_id = get_item_id(auth, project_id, folder_id, "MirabufDir", "folders") if new_folder_id is None: - created_folder_id = create_folder(auth, project_id, folder_id, "MirabufDir") + created_folder_result = create_folder(auth, project_id, folder_id, "MirabufDir") + if created_folder_result.is_fatal(): + return created_folder_result + else: + created_folder_id = created_folder_result.unwrap() else: created_folder_id = new_folder_id - if created_folder_id is None: - return None - - file_id_data = get_file_id(auth, project_id, created_folder_id, file_name) - if file_id_data is None: - return None + file_id_result = get_file_id(auth, project_id, created_folder_id, file_name) + if file_id_result.is_fatal(): + # Hack to get around different return types + return Err(file_id_result.unwrap_err()[0], ErrorSeverity.Fatal) + file_id_data = file_id_result.unwrap() (lineage_id, file_id, file_version) = file_id_data """ Create APS Storage Location """ - object_id = create_storage_location(auth, project_id, created_folder_id, file_name) - if object_id is None: - gm.ui.messageBox("UPLOAD ERROR", "Object id is none; check create storage location") - return None + object_id_result = create_storage_location(auth, project_id, created_folder_id, file_name) + if object_id_result.is_fatal(): + return object_id_result + object_id = object_id_result.unwrap() + (prefix, object_key) = str(object_id).split("/", 1) bucket_key = prefix.split(":", 3)[3] # gets the last element smth like: wip.dm.prod @@ -296,25 +295,32 @@ def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_content Create Signed URL For APS Upload """ generate_signed_url_result = generate_signed_url(auth, bucket_key, object_key) - if generate_signed_url_result is None: - return None + if generate_signed_url_result.is_fatal(): + # Hack to get around different Result success types in the err case + return Err(generate_signed_url_result.unwrap_err()[0], ErrorSeverity.Fatal) - (upload_key, signed_url) = generate_signed_url_result - if upload_file(signed_url, file_contents) is None: - return None + (upload_key, signed_url) = generate_signed_url_result.unwrap() + upload_file_result = upload_file(signed_url, file_contents) + if upload_file_result.is_fatal(): + return upload_file_result + upload_file_result = upload_file_result.unwrap() """ Finish Upload and Initialize File Version """ - if complete_upload(auth, upload_key, object_key, bucket_key) is None: - return None + complete_upload_result = complete_upload(auth, upload_key, object_key, bucket_key) + if complete_upload_result.is_fatal(): + return complete_upload_result + if file_id != "": - update_file_version( + update_file_result = update_file_version( auth, project_id, created_folder_id, lineage_id, file_id, file_name, file_contents, file_version, object_id ) + if update_file_result.is_fatal(): + return update_file_result else: _lineage_info = create_first_file_version(auth, str(object_id), project_id, str(created_folder_id), file_name) - return "" + return Ok("") def get_hub_id(auth: str, hub_name: str) -> str | None: @@ -403,7 +409,7 @@ def update_file_version( file_contents: str, curr_file_version: str, object_id: str, -) -> str | None: +) -> Result[str]: """ updates an existing file in an APS folder @@ -423,22 +429,6 @@ def update_file_version( - file doesn't exist in that position / with that id / name ; fix: get_file_id() or smth - version one of the file hasn't been created ; fix: create_first_file_version() """ - - # object_id = create_storage_location(auth, project_id, folder_id, file_name) - # if object_id is None: - # return None - # - # (prefix, object_key) = str(object_id).split("/", 1) - # bucket_key = prefix.split(":", 3)[3] # gets the last element smth like: wip.dm.prod - # (upload_key, signed_url) = generate_signed_url(auth, bucket_key, object_key) - # - # if upload_file(signed_url, file_contents) is None: - # return None - - # if complete_upload(auth, upload_key, object_key, bucket_key) is None: - # return None - - # gm.ui.messageBox(f"file_name:{file_name}\nlineage_id:{lineage_id}\nfile_id:{file_id}\ncurr_file_version:{curr_file_version}\nobject_id:{object_id}", "REUPLOAD ARGS") headers = { "Authorization": f"Bearer {auth}", "Content-Type": "application/vnd.api+json", @@ -469,16 +459,15 @@ def update_file_version( f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/versions", headers=headers, json=data ) if not update_res.ok: - gm.ui.messageBox(f"UPLOAD ERROR:\n{update_res.text}", "Updating file to new version failed") - return None + return Err(f"Updating file to new version failed\nUPLOAD ERROR:\n{update_res.text}", ErrorSeverity.Fatal) gm.ui.messageBox( f"Successfully updated file {file_name} to version {int(curr_file_version) + 1} on APS", "UPLOAD SUCCESS" ) new_id: str = update_res.json()["data"]["id"] - return new_id + return Ok(new_id) -def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> tuple[str, str, str] | None: +def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> Result[tuple[str, str, str]]: """ gets the file id given a file name @@ -509,20 +498,19 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> t params=params, ) if file_res.status_code == 404: - return ("", "", "") + return Ok(("", "", "")) elif not file_res.ok: - gm.ui.messageBox(f"UPLOAD ERROR: {file_res.text}", "Failed to get file") - return None + return Err(f"UPLOAD ERROR: {file_res.text} (Failed to get file)", ErrorSeverity.Fatal) file_json: dict[str, Any] = file_res.json() if len(file_json["data"]) == 0: - return ("", "", "") + return Ok(("", "", "")) id: str = str(file_json["data"][0]["id"]) lineage: str = str(file_json["data"][0]["relationships"]["item"]["data"]["id"]) version: str = str(file_json["data"][0]["attributes"]["versionNumber"]) - return (lineage, id, version) + return Ok((lineage, id, version)) -def create_storage_location(auth: str, project_id: str, folder_id: str, file_name: str) -> str | None: +def create_storage_location(auth: str, project_id: str, folder_id: str, file_name: str) -> Result[str]: """ creates a storage location (a bucket) the bucket can be used to upload a file to @@ -560,14 +548,13 @@ def create_storage_location(auth: str, project_id: str, folder_id: str, file_nam f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/storage", json=data, headers=headers ) if not storage_location_res.ok: - gm.ui.messageBox(f"UPLOAD ERROR: {storage_location_res.text}", f"Failed to create storage location") - return None + return Err(f"UPLOAD ERROR: {storage_location_res.text} (Failed to create storage location)", ErrorSeverity.Fatal) storage_location_json: dict[str, Any] = storage_location_res.json() object_id: str = storage_location_json["data"]["id"] - return object_id + return Ok(object_id) -def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> tuple[str, str] | None: +def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> Result[tuple[str, str]]: """ generates a signed_url for a bucket, given a bucket_key and object_key @@ -593,13 +580,12 @@ def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> tuple[st headers=headers, ) if not signed_url_res.ok: - gm.ui.messageBox(f"UPLOAD ERROR: {signed_url_res.text}", "Failed to get signed url") - return None + return Err(f"Failed to get signed URL:\nUPLOAD ERROR: {signed_url_res.text}", ErrorSeverity.Fatal) signed_url_json: dict[str, str] = signed_url_res.json() - return (signed_url_json["uploadKey"], signed_url_json["urls"][0]) + return Ok((signed_url_json["uploadKey"], signed_url_json["urls"][0])) -def upload_file(signed_url: str, file_contents: str) -> str | None: +def upload_file(signed_url: str, file_contents: str) -> Result[str]: """ uploads a file to APS given a signed_url a path to the file on your machine @@ -616,12 +602,11 @@ def upload_file(signed_url: str, file_contents: str) -> str | None: """ upload_response = requests.put(url=signed_url, data=file_contents) if not upload_response.ok: - gm.ui.messageBox("UPLOAD ERROR", f"Failed to upload to signed url: {upload_response.text}") - return None - return "" + return Err(f"Failed to upload to signed url\nUPLOAD ERROR: {upload_response.text}", ErrorSeverity.Fatal) + return Ok("") -def complete_upload(auth: str, upload_key: str, object_key: str, bucket_key: str) -> str | None: +def complete_upload(auth: str, upload_key: str, object_key: str, bucket_key: str) -> Result[str]: """ completes and verifies the APS file upload given the upload_key @@ -647,16 +632,15 @@ def complete_upload(auth: str, upload_key: str, object_key: str, bucket_key: str headers=headers, ) if not completed_res.ok: - gm.ui.messageBox( - f"UPLOAD ERROR: {completed_res.text}\n{completed_res.status_code}", "Failed to complete upload" + return Err( + f"Failed to complete upload\n UPLOAD ERROR: {completed_res.text}\n{completed_res.status_code}", ErrorSeverity.Fatal ) - return None - return "" + return Ok("") def create_first_file_version( auth: str, object_id: str, project_id: str, folder_id: str, file_name: str -) -> tuple[str, str] | None: +) -> Result[tuple[str, str]]: """ initializes versioning for a file @@ -720,8 +704,7 @@ def create_first_file_version( f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/items", json=data, headers=headers ) if not first_version_res.ok: - gm.ui.messageBox(f"Failed to create first file version: {first_version_res.text}", "UPLOAD ERROR") - return None + return Err(f"Failed to create first file version:\nUPLOAD ERROR: {first_version_res.text}", ErrorSeverity.Fatal) first_version_json: dict[str, Any] = first_version_res.json() lineage_id: str = first_version_json["data"]["id"] @@ -729,4 +712,4 @@ def create_first_file_version( gm.ui.messageBox(f"Successful Upload of {file_name} to APS", "UPLOAD SUCCESS") - return (lineage_id, href) + return Ok((lineage_id, href)) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index d2f7f940c4..739e7c5ca8 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -4,11 +4,10 @@ import adsk.core import adsk.fusion -from google.protobuf.message import Error from src import gm from src.ErrorHandling import Err, ErrorSeverity, Ok, Result, handle_err_top -from src.Logging import getLogger, logFailure +from src.Logging import getLogger from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser.PDMessage import PDMessage from src.Parser.SynthesisParser.Utilities import guid_component, guid_occurrence @@ -192,7 +191,6 @@ class JointParser: grounded: adsk.fusion.Occurrence # NOTE This function cannot under the value-based error handling system, since it's an __init__ function - @logFailure def __init__(self, design: adsk.fusion.Design) -> None: """Create hierarchy with just joint assembly - Assembly @@ -222,8 +220,10 @@ def __init__(self, design: adsk.fusion.Design) -> None: self.grounded = searchForGrounded(design.rootComponent) if self.grounded is None: - gm.ui.messageBox("There is not currently a Grounded Component in the assembly, stopping kinematic export.") - raise RuntimeWarning("There is no grounded component") + message = "These is no grounded component in this assembly, aborting kinematic export." + gm.ui.messageBox(message) + ___: Err[None] = Err(message, ErrorSeverity.Fatal) + raise RuntimeError() self.currentTraversal: dict[str, DynamicOccurrenceNode | bool] = dict() self.groundedConnections: list[adsk.fusion.Occurrence] = [] diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index 510116d2d7..633e55ed77 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -6,7 +6,7 @@ from src import gm from src.APS.APS import getAuth, upload_mirabuf -from src.ErrorHandling import ErrorSeverity, Result +from src.ErrorHandling import Err, ErrorSeverity, Result from src.Logging import getLogger, logFailure, timed from src.Parser.ExporterOptions import ExporterOptions from src.Parser.SynthesisParser import ( @@ -33,7 +33,6 @@ def __init__(self, options: ExporterOptions): """ self.exporterOptions = options - @logFailure(messageBox=True) @timed def export(self) -> None: app = adsk.core.Application.get() @@ -182,12 +181,18 @@ def export(self) -> None: logger.debug("Uploading file to APS") project = app.data.activeProject if not project.isValid: - raise RuntimeError("Project is invalid") + app.userInterface.messageBox(f"Project is invalid") + return project_id = project.id folder_id = project.rootFolder.id file_name = f"{self.exporterOptions.fileLocation}.mira" - if upload_mirabuf(project_id, folder_id, file_name, assembly_out.SerializeToString()) is None: - raise RuntimeError("Could not upload to APS") + + # Can't use decorator because it returns a value + upload_result = upload_mirabuf(project_id, folder_id, file_name, assembly_out.SerializeToString()) + if upload_result.is_err(): + message = upload_result.unwrap_err()[0] + app.userInterface.messageBox(f"Fatal Error Encountered: {message}") + return else: assert self.exporterOptions.exportLocation == ExportLocation.DOWNLOAD # check if entire path exists and create if not since gzip doesn't do that. From a6e93fa5b8f1e0a61ccee4a677aedc669889f1ff Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 10 Jul 2025 11:16:55 -0700 Subject: [PATCH 63/68] refactor: update old handling in jointhierarchy --- .../src/Parser/SynthesisParser/JointHierarchy.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 739e7c5ca8..837324187c 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -243,7 +243,10 @@ def __init__(self, design: adsk.fusion.Design) -> None: # dynamic joint node for grounded components and static components populate_node_result = self._populateNode(self.grounded, None, None, is_ground=True) if populate_node_result.is_err(): # We need the value to proceed - raise RuntimeWarning(populate_node_result.unwrap_err()[0]) + message = populate_node_result.unwrap_err()[0] + gm.ui.messageBox(message) + ___: Err[None] = Err(message, ErrorSeverity.Fatal) + raise RuntimeError() rootNode = populate_node_result.unwrap() self.groundSimNode = SimulationNode(rootNode, None, grounded=True) @@ -257,7 +260,10 @@ def __init__(self, design: adsk.fusion.Design) -> None: for key, value in self.dynamicJoints.items(): populate_axis_result = self._populateAxis(key, value) if populate_axis_result.is_err(): - raise RuntimeError(populate_axis_result.unwrap_err()[0]) + message = populate_axis_result.unwrap_err()[0] + gm.ui.messageBox(message) + ___: Err[None] = Err(message, ErrorSeverity.Fatal) + raise RuntimeError() __ = self._linkAllAxis() From 090837812870f1658c728ccaf1cd8a7a9bc64323 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Thu, 10 Jul 2025 11:46:00 -0700 Subject: [PATCH 64/68] fix: mypy + formatting --- exporter/SynthesisFusionAddin/src/APS/APS.py | 15 +++++++++------ .../src/Parser/SynthesisParser/JointHierarchy.py | 6 +++--- .../SynthesisFusionAddin/src/UI/ConfigCommand.py | 13 ++++++++----- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py index c4f4d0a776..16c18df849 100644 --- a/exporter/SynthesisFusionAddin/src/APS/APS.py +++ b/exporter/SynthesisFusionAddin/src/APS/APS.py @@ -153,7 +153,7 @@ def refreshAuthToken() -> None: def loadUserInfo() -> Result[APSUserInfo]: global APS_AUTH if not APS_AUTH: - return None + return Err("Aps Authentication is undefined", ErrorSeverity.Fatal) global APS_USER_INFO req = urllib.request.Request("https://api.userprofile.autodesk.com/userinfo") req.add_header(key="Authorization", val=APS_AUTH.access_token) @@ -180,6 +180,7 @@ def loadUserInfo() -> Result[APSUserInfo]: removeAuth() return Err(f"User Info Error:\n{e.code} - {e.reason}\nPlease sign in again", ErrorSeverity.Fatal) + def getUserInfo() -> Result[APSUserInfo]: if APS_USER_INFO is not None: return Ok(APS_USER_INFO) @@ -303,7 +304,6 @@ def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_content upload_file_result = upload_file(signed_url, file_contents) if upload_file_result.is_fatal(): return upload_file_result - upload_file_result = upload_file_result.unwrap() """ Finish Upload and Initialize File Version @@ -311,7 +311,7 @@ def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_content complete_upload_result = complete_upload(auth, upload_key, object_key, bucket_key) if complete_upload_result.is_fatal(): return complete_upload_result - + if file_id != "": update_file_result = update_file_version( auth, project_id, created_folder_id, lineage_id, file_id, file_name, file_contents, file_version, object_id @@ -548,7 +548,9 @@ def create_storage_location(auth: str, project_id: str, folder_id: str, file_nam f"https://developer.api.autodesk.com/data/v1/projects/{project_id}/storage", json=data, headers=headers ) if not storage_location_res.ok: - return Err(f"UPLOAD ERROR: {storage_location_res.text} (Failed to create storage location)", ErrorSeverity.Fatal) + return Err( + f"UPLOAD ERROR: {storage_location_res.text} (Failed to create storage location)", ErrorSeverity.Fatal + ) storage_location_json: dict[str, Any] = storage_location_res.json() object_id: str = storage_location_json["data"]["id"] return Ok(object_id) @@ -580,7 +582,7 @@ def generate_signed_url(auth: str, bucket_key: str, object_key: str) -> Result[t headers=headers, ) if not signed_url_res.ok: - return Err(f"Failed to get signed URL:\nUPLOAD ERROR: {signed_url_res.text}", ErrorSeverity.Fatal) + return Err(f"Failed to get signed URL:\nUPLOAD ERROR: {signed_url_res.text}", ErrorSeverity.Fatal) signed_url_json: dict[str, str] = signed_url_res.json() return Ok((signed_url_json["uploadKey"], signed_url_json["urls"][0])) @@ -633,7 +635,8 @@ def complete_upload(auth: str, upload_key: str, object_key: str, bucket_key: str ) if not completed_res.ok: return Err( - f"Failed to complete upload\n UPLOAD ERROR: {completed_res.text}\n{completed_res.status_code}", ErrorSeverity.Fatal + f"Failed to complete upload\n UPLOAD ERROR: {completed_res.text}\n{completed_res.status_code}", + ErrorSeverity.Fatal, ) return Ok("") diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 837324187c..57297d5b13 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -222,7 +222,7 @@ def __init__(self, design: adsk.fusion.Design) -> None: if self.grounded is None: message = "These is no grounded component in this assembly, aborting kinematic export." gm.ui.messageBox(message) - ___: Err[None] = Err(message, ErrorSeverity.Fatal) + _____: Err[None] = Err(message, ErrorSeverity.Fatal) raise RuntimeError() self.currentTraversal: dict[str, DynamicOccurrenceNode | bool] = dict() @@ -245,7 +245,7 @@ def __init__(self, design: adsk.fusion.Design) -> None: if populate_node_result.is_err(): # We need the value to proceed message = populate_node_result.unwrap_err()[0] gm.ui.messageBox(message) - ___: Err[None] = Err(message, ErrorSeverity.Fatal) + ____: Err[None] = Err(message, ErrorSeverity.Fatal) raise RuntimeError() rootNode = populate_node_result.unwrap() @@ -262,7 +262,7 @@ def __init__(self, design: adsk.fusion.Design) -> None: if populate_axis_result.is_err(): message = populate_axis_result.unwrap_err()[0] gm.ui.messageBox(message) - ___: Err[None] = Err(message, ErrorSeverity.Fatal) + ___: Err[None] = Err(message, ErrorSeverity.Fatal) raise RuntimeError() __ = self._linkAllAxis() diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 2f9bcb1eee..f0d152784d 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -121,10 +121,13 @@ def notify(self, args: adsk.core.CommandCreatedEventArgs) -> None: jointConfigTab.addWheel(fusionJoints[0], wheel) getAuth() - user_info = getUserInfo() - apsSettings = INPUTS_ROOT.addTabCommandInput( - "aps_settings", f"APS Settings ({user_info.given_name if user_info else 'Not Signed In'})" - ) + user_info_result = getUserInfo() + if user_info_result.is_err(): + user_name = "Not Signed In" + else: + user_name = user_info_result.unwrap().given_name + + apsSettings = INPUTS_ROOT.addTabCommandInput("aps_settings", f"APS Settings ({user_name})") apsSettings.tooltip = "Configuration settings for Autodesk Platform Services." @@ -138,7 +141,7 @@ def notify(self, _: adsk.core.CommandEventArgs) -> None: fullName = design.rootComponent.name versionMatch = re.search(r"v\d+", fullName) - docName = (fullName[: versionMatch.start()].strip() if versionMatch else fullName).replace(" ", "_") + docName = (fullName[versionMatch.start()].strip() if versionMatch else fullName).replace(" ", "_") docVersion = versionMatch.group() if versionMatch else "v0" processedFileName = gm.app.activeDocument.name.replace(" ", "_") From e44df504f3f468d29e8ad2e23457500709a8fa31 Mon Sep 17 00:00:00 2001 From: BrandonPacewic <92102436+BrandonPacewic@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:02:31 -0700 Subject: [PATCH 65/68] fix(exporter): revert image file update --- .../src/Resources/PWM_icon/16x16-normal.png | Bin 494 -> 782 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/16x16-normal.png b/exporter/SynthesisFusionAddin/src/Resources/PWM_icon/16x16-normal.png index 2e3175e8a880c7ec51f90939eb9bf03ebfefb40c..07cd465f72d6f27012f2fd9dad871c162e02105a 100644 GIT binary patch literal 782 zcmV+p1M&QcP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0;5SpK~zXf&6Z0~ zQ&AL!KZ*!azy}HxBZ3kYh{QL>$Up}gNem&5oH#Xc$@R=Ay zd`26PhbT}ew6svbwR>(;6S?h>uH@2l&ug!<_B#7ggom{ota;51MbSgjoYH>^g;WMc zuBgyRWssZL<{(tnYF0q9uEOFR#3N9u{<11qn1YpMDBq^ZnzN{}K#A7WK|*skl|o6m z*34*ZR_!vat$@@LEX83p<>IV^;@*?=dhLI)q9;IO2V8joZI_@`PkXmE(@^;i=(_C! zrV?=Z9vnCaBX3~OF}QHc9pXq2oWH3kB0KyV_ML^RkD$3z?{6JhALy9t!iYIlb~VHJ zAPhcn0XnZsNvW_k1ba_GWeqeOheoxl>P76R8#6_!8zI!HvD0p^P@A5^P;*e~ZApjl zgCDW1#>-Mi&X6) z_@j3todA=mC$hwy6-EcT@Zc+b0RYF0fGwHBsHeO3Me9$dh_Ju=WOukr27rE z$O7ASpkevdB_tQ}D`o~Y1;Q_(|1R|15wTx-f8mVWa6-hsyMRq+T%XO&z%I+26mGew zOQh;FGa_Q%SPu9R%l0RA@}WL>F4GP?7D?M!C!Ur{$px1tQ_RQ(bt&wY+E@llF_*sm zGQ8Ph-cv9S{D?VcCgz=2?9_JqtYlqwnN3 zn=_!)N^Q%)ib_TfH>6VaE2(*Lm4QI96s&eu zFr{ptMcYH+tUlTWGV(qs*vjvh$_+;#^EKZ<{FkxxN8D={*uOpS7qM>jiniOs2mk;8 M07*qoM6N<$f{5y5xc~qF literal 494 zcmV1OJNdEfy`{l7s>o1?&vH25G2EAtvm!;B{{ z-duPmpd9*>MaZ3jg>C8UOUJ$v<1J*DF_}y`u-$O_mA|UdHKyo_S+E#@;zTu5OU`c} znN>f$e*W(7y_+wIF#ry<4Gow$JStnCz4|Zv@7}TUcV{*{CE5T@D+fky+q8^VKZMf% z-Q833_1KK*zkmP!OSAzjYHkJ}nH0Oe-#R+&)4_=ozJGrI8>>2827F>wi2C~S8#mL9 zbqjE5#%TZ(Fc$uN{4DVI5A%D5ukSwN3nxZg65Jwkf((qzT)#d(fBE~@k3YE7G2xPs zvdDE(v#-t+6p`n~=K`z-0Fx)vyHD(acb>6T{9|Mi!e;<8E(vxvw!42n-{1P@!_}+5 ket!GSKn7sM=S3I*0Dxtiu2_e#i2wiq07*qoM6N<$g7N|3S^xk5 From 6e8fa2c5e8f8cd1fd70599a76c6f253a1c95a3db Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 15 Jul 2025 09:09:59 -0700 Subject: [PATCH 66/68] fix: runtime exception is no longer raised by a lack of rigid components --- .../src/Parser/SynthesisParser/JointHierarchy.py | 1 + exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py index 57297d5b13..9ea63d6967 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -1,4 +1,5 @@ import enum +import sys from logging import ERROR from typing import Any, Iterator, cast diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 19ac4f80f8..3d84854aa5 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -193,7 +193,10 @@ def notify(self, _: adsk.core.CommandEventArgs) -> None: openSynthesisUponExport=generalConfigTab.openSynthesisUponExport, ) - Parser.Parser(exporterOptions).export() + try: + Parser.Parser(exporterOptions).export() + except: + pass exporterOptions.writeToDesign() jointConfigTab.reset() gamepieceConfigTab.reset() From 76ed2b087b0c7c3920b990d34bb6e2c1e829ca4f Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 15 Jul 2025 10:26:32 -0700 Subject: [PATCH 67/68] fix: file dialog names --- exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 3d84854aa5..9d88c9ee3c 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -151,9 +151,10 @@ def notify(self, _: adsk.core.CommandEventArgs) -> None: design = adsk.fusion.Design.cast(adsk.core.Application.get().activeProduct) exporterOptions = ExporterOptions().readFromDesign() or ExporterOptions() - fullName = design.rootComponent.name + fullName: str = design.rootComponent.name versionMatch = re.search(r"v\d+", fullName) - docName = (fullName[versionMatch.start()].strip() if versionMatch else fullName).replace(" ", "_") + strippedName = fullName[0 : versionMatch.start()].strip() + docName = (strippedName if versionMatch else fullName).replace(" ", "_") docVersion = versionMatch.group() if versionMatch else "v0" processedFileName = gm.app.activeDocument.name.replace(" ", "_") From d313b80565cbbf1a74ca7fa843843febc44b46e1 Mon Sep 17 00:00:00 2001 From: Azalea Colburn Date: Tue, 15 Jul 2025 11:57:14 -0700 Subject: [PATCH 68/68] chore: fix mypy lint errors --- exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 9d88c9ee3c..cf40fc557a 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -153,8 +153,11 @@ def notify(self, _: adsk.core.CommandEventArgs) -> None: fullName: str = design.rootComponent.name versionMatch = re.search(r"v\d+", fullName) - strippedName = fullName[0 : versionMatch.start()].strip() - docName = (strippedName if versionMatch else fullName).replace(" ", "_") + if versionMatch: + strippedName: str = fullName[0 : versionMatch.start()].strip() + else: + strippedName = fullName + docName = strippedName.replace(" ", "_") docVersion = versionMatch.group() if versionMatch else "v0" processedFileName = gm.app.activeDocument.name.replace(" ", "_")