diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index df5d21c9c1..203c20e888 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -18,7 +18,7 @@ # Not 100% sure what this is for - Brandon JointParentType = Enum("JointParentType", ["ROOT", "END"]) -WheelType = Enum("WheelType", ["STANDARD", "OMNI"]) +WheelType = Enum("WheelType", ["STANDARD", "OMNI", "MECANUM"]) SignalType = Enum("SignalType", ["PWM", "CAN", "PASSIVE"]) ExportMode = Enum("ExportMode", ["ROBOT", "FIELD"]) # Dynamic / Static export PreferredUnits = Enum("PreferredUnits", ["METRIC", "IMPERIAL"]) @@ -40,6 +40,12 @@ class Joint: speed: float = field(default=None) force: float = field(default=None) + # Transition: AARD-1865 + # Should consider changing how the parser handles wheels and joints as there is overlap between + # `Joint` and `Wheel` that should be avoided + # This overlap also presents itself in 'ConfigCommand.py' and 'JointConfigTab.py' + isWheel: bool = field(default=False) + @dataclass class Gamepiece: @@ -78,7 +84,10 @@ class ModelHierarchy(Enum): @dataclass class ExporterOptions: - fileLocation: str = field( + # Python's `os` module can return `None` when attempting to find the home directory if the + # user's computer has conflicting configs of some sort. This has happened and should be accounted + # for accordingly. + fileLocation: str | None = field( default=(os.getenv("HOME") if platform.system() == "Windows" else os.path.expanduser("~")) ) name: str = field(default=None) @@ -103,18 +112,21 @@ class ExporterOptions: physicalDepth: PhysicalDepth = field(default=PhysicalDepth.AllOccurrence) physicalCalculationLevel: CalculationAccuracy = field(default=CalculationAccuracy.LowCalculationAccuracy) - def readFromDesign(self) -> None: - designAttributes = adsk.core.Application.get().activeProduct.attributes - for field in fields(self): - attribute = designAttributes.itemByName(INTERNAL_ID, field.name) - if attribute: - setattr( - self, - field.name, - self._makeObjectFromJson(field.type, json.loads(attribute.value)), - ) + def readFromDesign(self) -> "ExporterOptions": + try: + designAttributes = adsk.core.Application.get().activeProduct.attributes + for field in fields(self): + attribute = designAttributes.itemByName(INTERNAL_ID, field.name) + if attribute: + setattr( + self, + field.name, + self._makeObjectFromJson(field.type, json.loads(attribute.value)), + ) - return self + return self + except: + return ExporterOptions() def writeToDesign(self) -> None: designAttributes = adsk.core.Application.get().activeProduct.attributes diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index 806cd65bff..51564dada9 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -4,8 +4,6 @@ import logging import os - -# import platform import traceback from enum import Enum @@ -21,20 +19,22 @@ ExportLocation, ExportMode, Gamepiece, - Joint, - JointParentType, PreferredUnits, - SignalType, - Wheel, - WheelType, ) from ..Parser.SynthesisParser.Parser import Parser from ..Parser.SynthesisParser.Utilities import guid_occurrence -from . import CustomGraphics, FileDialogConfig, Helper, IconPaths, OsHelper +from . import CustomGraphics, FileDialogConfig, Helper, IconPaths from .Configuration.SerialCommand import SerialCommand +# Transition: AARD-1685 +# In the future all components should be handled in this way. +# This import broke everything when attempting to use absolute imports??? Investigate? +from .JointConfigTab import JointConfigTab + # ====================================== CONFIG COMMAND ====================================== +jointConfigTab: JointConfigTab + """ INPUTS_ROOT (adsk.fusion.CommandInputs): - Provides access to the set of all commandInput UI elements in the panel @@ -43,12 +43,8 @@ """ These lists are crucial, and contain all of the relevant object selections. -- WheelListGlobal: list of wheels (adsk.fusion.Occurrence) -- JointListGlobal: list of joints (adsk.fusion.Joint) - GamepieceListGlobal: list of gamepieces (adsk.fusion.Occurrence) """ -WheelListGlobal = [] -JointListGlobal = [] GamepieceListGlobal = [] # Default to compressed files @@ -71,24 +67,6 @@ def GUID(arg): return arg.entityToken -def wheelTable(): - """### Returns the wheel table command input - - Returns: - adsk.fusion.TableCommandInput - """ - return INPUTS_ROOT.itemById("wheel_table") - - -def jointTable(): - """### Returns the joint table command input - - Returns: - adsk.fusion.TableCommandInput - """ - return INPUTS_ROOT.itemById("joint_table") - - def gamepieceTable(): """### Returns the gamepiece table command input @@ -167,7 +145,6 @@ def __init__(self, configure): def notify(self, args): try: exporterOptions = ExporterOptions().readFromDesign() - # exporterOptions = ExporterOptions() if not Helper.check_solid_open(): return @@ -242,7 +219,7 @@ def notify(self, args): # ~~~~~~~~~~~~~~~~ WEIGHT CONFIGURATION ~~~~~~~~~~~~~~~~ """ - Table for weight config. + Table for weight config. - Used this to align multiple commandInputs on the same row """ weightTableInput = self.createTableInput( @@ -296,8 +273,8 @@ def notify(self, args): adsk.core.DropDownStyles.LabeledIconDropDownStyle, ) - weight_unit.listItems.add("", imperialUnits, IconPaths.massIcons["LBS"]) # add listdropdown mass options - weight_unit.listItems.add("", not imperialUnits, IconPaths.massIcons["KG"]) # add listdropdown mass options + weight_unit.listItems.add("", imperialUnits, IconPaths.massIcons["LBS"]) + weight_unit.listItems.add("", not imperialUnits, IconPaths.massIcons["KG"]) weight_unit.tooltip = "Unit of mass" weight_unit.tooltipDescription = "
{}
".format(joint.name) - - jointType = cmdInputs.addDropDownCommandInput( - "joint_parent", - "Joint Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - jointType.isFullWidth = True - jointType.listItems.add("Root", True) - - # after each additional joint added, add joint to the dropdown of all preview rows/joints - for row in range(jointTableInput.rowCount): - if row != 0: - dropDown = jointTableInput.getInputAtPosition(row, 2) - dropDown.listItems.add(JointListGlobal[-1].name, False) - - # add all parent joint options to added joint dropdown - for j in range(len(JointListGlobal) - 1): - jointType.listItems.add(JointListGlobal[j].name, False) - - jointType.tooltip = "Possible parent joints" - jointType.tooltipDescription = "+ {text} +
+ + """ + + input = inputs.addTextBoxCommandInput(id, name, outputText, rowCount, read) + input.tooltip = tooltip + input.tooltipDescription = advanced_tooltip + + return input + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.CreateCommandInputsHelper").error( + "Failed:\n{}".format(traceback.format_exc()) + ) diff --git a/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py b/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py index e49cf240b7..7af7a1e73a 100644 --- a/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py +++ b/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py @@ -9,7 +9,7 @@ from ..Types.OString import OString -def SaveFileDialog(defaultPath="", defaultName="", ext="MiraBuf Package (*.mira)") -> Union[str, bool]: +def saveFileDialog(defaultPath: str | None = None, defaultName: str | None = None) -> str | bool: """Function to generate the Save File Dialog for the Hellion Data files Args: @@ -21,22 +21,18 @@ def SaveFileDialog(defaultPath="", defaultName="", ext="MiraBuf Package (*.mira) str: full file path """ - ext = "MiraBuf Package (*.mira)" - fileDialog: adsk.core.FileDialog = gm.ui.createFileDialog() fileDialog.isMultiSelectEnabled = False fileDialog.title = "Save Export Result" - fileDialog.filter = f"{ext}" + fileDialog.filter = "MiraBuf Package (*.mira)" - if defaultPath == "": + if not defaultPath: defaultPath = generateFilePath() fileDialog.initialDirectory = defaultPath - # print(defaultPath) - - if defaultName == "": + if not defaultName: defaultName = generateFileName() fileDialog.initialFilename = defaultName diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py new file mode 100644 index 0000000000..5f9be302b3 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -0,0 +1,544 @@ +import logging +import traceback + +import adsk.core +import adsk.fusion + +from ..general_imports import INTERNAL_ID +from ..Parser.ExporterOptions import ( + Joint, + JointParentType, + SignalType, + Wheel, + WheelType, +) +from . import IconPaths +from .CreateCommandInputsHelper import ( + createBooleanInput, + createTableInput, + createTextBoxInput, +) + + +class JointConfigTab: + selectedJointList: list[adsk.fusion.Joint] = [] + previousWheelCheckboxState: list[bool] = [] + jointWheelIndexMap: dict[str, int] = {} + jointConfigTab: adsk.core.TabCommandInput + jointConfigTable: adsk.core.TableCommandInput + wheelConfigTable: adsk.core.TableCommandInput + + def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None: + try: + inputs = args.command.commandInputs + self.jointConfigTab = inputs.addTabCommandInput("jointSettings", "Joint Settings") + self.jointConfigTab.tooltip = "Select and configure robot joints." + jointConfigTabInputs = self.jointConfigTab.children + self.jointConfigTable = createTableInput( + "jointTable", "Joint Table", jointConfigTabInputs, 7, "1:2:2:2:2:2:2" + ) + self.jointConfigTable.addCommandInput( + createTextBoxInput("jointMotionHeader", "Motion", jointConfigTabInputs, "Motion", bold=False), 0, 0 + ) + self.jointConfigTable.addCommandInput( + createTextBoxInput("nameHeader", "Name", jointConfigTabInputs, "Joint name", bold=False), 0, 1 + ) + self.jointConfigTable.addCommandInput( + createTextBoxInput( + "parentHeader", "Parent", jointConfigTabInputs, "Parent joint", background="#d9d9d9" + ), + 0, + 2, + ) + self.jointConfigTable.addCommandInput( + createTextBoxInput("signalHeader", "Signal", jointConfigTabInputs, "Signal type", background="#d9d9d9"), + 0, + 3, + ) + self.jointConfigTable.addCommandInput( + createTextBoxInput("speedHeader", "Speed", jointConfigTabInputs, "Joint Speed", background="#d9d9d9"), + 0, + 4, + ) + self.jointConfigTable.addCommandInput( + createTextBoxInput("forceHeader", "Force", jointConfigTabInputs, "Joint Force", background="#d9d9d9"), + 0, + 5, + ) + self.jointConfigTable.addCommandInput( + createTextBoxInput("wheelHeader", "Is Wheel", jointConfigTabInputs, "Is Wheel", background="#d9d9d9"), + 0, + 6, + ) + + jointSelect = jointConfigTabInputs.addSelectionInput( + "jointSelection", "Selection", "Select a joint in your assembly to add." + ) + jointSelect.addSelectionFilter("Joints") + jointSelect.setSelectionLimits(0) + jointSelect.isEnabled = jointSelect.isVisible = False # Visibility is triggered by `addJointInputButton` + + jointConfigTabInputs.addTextBoxCommandInput("jointTabBlankSpacer", "", "", 1, True) + + self.wheelConfigTable = createTableInput("wheelTable", "Wheel Table", jointConfigTabInputs, 4, "1:2:2:2") + self.wheelConfigTable.addCommandInput( + createTextBoxInput("wheelMotionHeader", "Motion", jointConfigTabInputs, "Motion", bold=False), 0, 0 + ) + self.wheelConfigTable.addCommandInput( + createTextBoxInput("name_header", "Name", jointConfigTabInputs, "Joint name", bold=False), 0, 1 + ) + self.wheelConfigTable.addCommandInput( + createTextBoxInput( + "wheelTypeHeader", "WheelType", jointConfigTabInputs, "Wheel type", background="#d9d9d9" + ), + 0, + 2, + ) + self.wheelConfigTable.addCommandInput( + createTextBoxInput( + "signalTypeHeader", "SignalType", jointConfigTabInputs, "Signal type", background="#d9d9d9" + ), + 0, + 3, + ) + + jointSelectCancelButton = jointConfigTabInputs.addBoolValueInput("jointSelectCancelButton", "Cancel", False) + jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + + addJointInputButton = jointConfigTabInputs.addBoolValueInput("jointAddButton", "Add", False) + removeJointInputButton = jointConfigTabInputs.addBoolValueInput("jointRemoveButton", "Remove", False) + addJointInputButton.isEnabled = removeJointInputButton.isEnabled = True + + self.jointConfigTable.addToolbarCommandInput(addJointInputButton) + self.jointConfigTable.addToolbarCommandInput(removeJointInputButton) + self.jointConfigTable.addToolbarCommandInput(jointSelectCancelButton) + + self.reset() + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) + + def addJoint(self, fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = None) -> bool: + try: + if fusionJoint in self.selectedJointList: + return False + + self.selectedJointList.append(fusionJoint) + commandInputs = self.jointConfigTable.commandInputs + + if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Rigid", IconPaths.jointIcons["rigid"]) + icon.tooltip = "Rigid joint" + + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Revolute", IconPaths.jointIcons["revolute"]) + icon.tooltip = "Revolute joint" + + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Slider", IconPaths.jointIcons["slider"]) + icon.tooltip = "Slider joint" + + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Planar", IconPaths.jointIcons["planar"]) + icon.tooltip = "Planar joint" + + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"]) + icon.tooltip = "Pin slot joint" + + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: + icon = commandInputs.addImageCommandInput( + "placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"] + ) + icon.tooltip = "Cylindrical joint" + + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: + icon = commandInputs.addImageCommandInput("placeholder", "Ball", IconPaths.jointIcons["ball"]) + icon.tooltip = "Ball joint" + + name = commandInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) + name.tooltip = fusionJoint.name + name.formattedText = f"{fusionJoint.name}
" + + jointType = commandInputs.addDropDownCommandInput( + "jointParent", + "Joint Type", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + + jointType.isFullWidth = True + + # Transition: AARD-1685 + # Implementation of joint parent system needs to be revisited. + jointType.listItems.add("Root", True) + + for row in range(1, self.jointConfigTable.rowCount): # Row is 1 indexed + dropDown = self.jointConfigTable.getInputAtPosition(row, 2) + dropDown.listItems.add(self.selectedJointList[-1].name, False) + + for fusionJoint in self.selectedJointList: + jointType.listItems.add(fusionJoint.name, False) + + jointType.tooltip = "Possible parent joints" + jointType.tooltipDescription = "