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 = "
Configure the unit of mass for the weight calculation." @@ -306,211 +283,34 @@ def notify(self, args): weightTableInput.addCommandInput(weight_input, 0, 2) # add command inputs to table weightTableInput.addCommandInput(weight_unit, 0, 3) # add command inputs to table - # ~~~~~~~~~~~~~~~~ WHEEL CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Wheel configuration command input group - - Container for wheel selection Table - """ - wheelConfig = inputs.addGroupCommandInput("wheel_config", "Wheel Configuration") - wheelConfig.isExpanded = True - wheelConfig.isEnabled = True - wheelConfig.tooltip = "Select and define the drive-train wheels in your assembly." - - wheel_inputs = wheelConfig.children - - # WHEEL SELECTION TABLE - """ - All selected wheel occurrences appear here. - """ - wheelTableInput = self.createTableInput( - "wheel_table", - "Wheel Table", - wheel_inputs, - 4, - "1:4:2:2", - 50, - ) - - addWheelInput = wheel_inputs.addBoolValueInput("wheel_add", "Add", False) # add button - - removeWheelInput = wheel_inputs.addBoolValueInput("wheel_delete", "Remove", False) # remove button - - addWheelInput.tooltip = "Add a wheel joint" # tooltips - removeWheelInput.tooltip = "Remove a wheel joint" - - wheelSelectInput = wheel_inputs.addSelectionInput( - "wheel_select", - "Selection", - "Select the wheels joints in your drive-train assembly.", - ) - wheelSelectInput.addSelectionFilter("Joints") # filter selection to only occurrences - - wheelSelectInput.setSelectionLimits(0) # no selection count limit - wheelSelectInput.isEnabled = False - wheelSelectInput.isVisible = False - - wheelTableInput.addToolbarCommandInput(addWheelInput) # add buttons to the toolbar - wheelTableInput.addToolbarCommandInput(removeWheelInput) # add buttons to the toolbar - - wheelTableInput.addCommandInput( # create textbox input using helper (component name) - self.createTextBoxInput("name_header", "Name", wheel_inputs, "Joint name", bold=False), - 0, - 1, - ) - - wheelTableInput.addCommandInput( - self.createTextBoxInput( # wheel type header - "parent_header", - "Parent", - wheel_inputs, - "Wheel type", - background="#d9d9d9", # textbox header background color - ), - 0, - 2, - ) - - wheelTableInput.addCommandInput( - self.createTextBoxInput( # Signal type header - "signal_header", - "Signal", - wheel_inputs, - "Signal type", - background="#d9d9d9", # textbox header background color - ), - 0, - 3, - ) + global jointConfigTab + jointConfigTab = JointConfigTab(args) + # Transition: AARD-1685 + # There remains some overlap between adding joints as wheels. + # Should investigate changes to improve performance. + if exporterOptions.joints: + for synJoint in exporterOptions.joints: + fusionJoint = gm.app.activeDocument.design.findEntityByToken(synJoint.jointToken)[0] + jointConfigTab.addJoint(fusionJoint, synJoint) + else: + for joint in [ + *gm.app.activeDocument.design.rootComponent.allJoints, + *gm.app.activeDocument.design.rootComponent.allAsBuiltJoints, + ]: + if ( + joint.jointMotion.jointType in (JointMotions.REVOLUTE.value, JointMotions.SLIDER.value) + and not joint.isSuppressed + ): + jointConfigTab.addJoint(joint) + + # Adding saved wheels must take place after joints are added as a result of how the two types are connected. + # Transition: AARD-1685 + # Should consider changing how the parser handles wheels and joints to avoid overlap if exporterOptions.wheels: for wheel in exporterOptions.wheels: - wheelEntity = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] - addWheelToTable(wheelEntity) - - # ~~~~~~~~~~~~~~~~ JOINT CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Joint configuration group. Container for joint selection table - """ - jointConfig = inputs.addGroupCommandInput("joint_config", "Joint Configuration") - jointConfig.isExpanded = False - jointConfig.isVisible = True - jointConfig.tooltip = "Select and define joint occurrences in your assembly." - - joint_inputs = jointConfig.children - - # JOINT SELECTION TABLE - """ - All selection joints appear here. - """ - jointTableInput = self.createTableInput( # create tablecommandinput using helper - "joint_table", - "Joint Table", - joint_inputs, - 6, - "1:2:2:2:2:2", - 50, - ) - - addJointInput = joint_inputs.addBoolValueInput("joint_add", "Add", False) # add button - - removeJointInput = joint_inputs.addBoolValueInput("joint_delete", "Remove", False) # remove button - - addJointInput.isEnabled = removeJointInput.isEnabled = True - - addJointInput.tooltip = "Add a joint selection" # tooltips - removeJointInput.tooltip = "Remove a joint selection" - - jointSelectInput = joint_inputs.addSelectionInput( - "joint_select", - "Selection", - "Select a joint in your drive-train assembly.", - ) - - jointSelectInput.addSelectionFilter("Joints") # only allow joint selection - jointSelectInput.setSelectionLimits(0) # set no selection count limits - jointSelectInput.isEnabled = False - jointSelectInput.isVisible = False # make selection box invisible - - jointTableInput.addToolbarCommandInput(addJointInput) # add bool inputs to the toolbar - jointTableInput.addToolbarCommandInput(removeJointInput) # add bool inputs to the toolbar - - jointTableInput.addCommandInput( - self.createTextBoxInput( # create a textBoxCommandInput for the table header (Joint Motion), using helper - "motion_header", - "Motion", - joint_inputs, - "Motion", - bold=False, - ), - 0, - 0, - ) - - jointTableInput.addCommandInput( - self.createTextBoxInput( # textBoxCommandInput for table header (Joint Name), using helper - "name_header", "Name", joint_inputs, "Joint name", bold=False - ), - 0, - 1, - ) - - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "parent_header", - "Parent", - joint_inputs, - "Parent joint", - background="#d9d9d9", # background color - ), - 0, - 2, - ) - - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "signal_header", - "Signal", - joint_inputs, - "Signal type", - background="#d9d9d9", # back color - ), - 0, - 3, - ) - - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "speed_header", - "Speed", - joint_inputs, - "Joint Speed", - background="#d9d9d9", # back color - ), - 0, - 4, - ) - - jointTableInput.addCommandInput( - self.createTextBoxInput( # another header using helper - "force_header", - "Force", - joint_inputs, - "Joint Force", - background="#d9d9d9", # back color - ), - 0, - 5, - ) - - # Fill the table with all joints in current design - for joint in list(gm.app.activeDocument.design.rootComponent.allJoints) + list( - gm.app.activeDocument.design.rootComponent.allAsBuiltJoints - ): - if ( - joint.jointMotion.jointType == JointMotions.REVOLUTE.value - or joint.jointMotion.jointType == JointMotions.SLIDER.value - ) and not joint.isSuppressed: - addJointToTable(joint) + fusionJoint = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] + jointConfigTab.addWheel(fusionJoint, wheel) # ~~~~~~~~~~~~~~~~ GAMEPIECE CONFIGURATION ~~~~~~~~~~~~~~~~ """ @@ -833,6 +633,8 @@ def notify(self, args): "Failed:\n{}".format(traceback.format_exc()) ) + # Transition: AARD-1685 + # Functionality will be fully moved to `CreateCommandInputsHelper` in AARD-1683 def createBooleanInput( self, _id: str, @@ -869,6 +671,8 @@ def createBooleanInput( "Failed:\n{}".format(traceback.format_exc()) ) + # Transition: AARD-1685 + # Functionality will be fully moved to `CreateCommandInputsHelper` in AARD-1683 def createTableInput( self, _id: str, @@ -909,6 +713,8 @@ def createTableInput( "Failed:\n{}".format(traceback.format_exc()) ) + # Transition: AARD-1685 + # Functionality will be fully moved to `CreateCommandInputsHelper` in AARD-1683 def createTextBoxInput( self, _id: str, @@ -999,7 +805,6 @@ def __init__(self): super().__init__() self.log = logging.getLogger(f"{INTERNAL_ID}.UI.{self.__class__.__name__}") self.current = SerialCommand() - self.designAttrs = adsk.core.Application.get().activeProduct.attributes def notify(self, args): try: @@ -1010,6 +815,13 @@ def notify(self, args): self.log.error("Could not execute configuration due to failure") return + processedFileName = gm.app.activeDocument.name.replace(" ", "_") + dropdownExportMode = INPUTS_ROOT.itemById("mode") + if dropdownExportMode.selectedItem.index == 0: + isRobot = True + elif dropdownExportMode.selectedItem.index == 1: + isRobot = False + processedFileName = gm.app.activeDocument.name.replace(" ", "_") dropdownExportMode = INPUTS_ROOT.itemById("mode") if dropdownExportMode.selectedItem.index == 0: @@ -1018,13 +830,7 @@ def notify(self, args): isRobot = False dropdownExportLocation = INPUTS_ROOT.itemById("location") if dropdownExportLocation.selectedItem.index == 1: # Download - if isRobot: - savepath = FileDialogConfig.SaveFileDialog( - defaultPath=exporterOptions.fileLocation, - ext="Synthesis File (*.synth)", - ) - else: - savepath = FileDialogConfig.SaveFileDialog(defaultPath=exporterOptions.fileLocation) + savepath = FileDialogConfig.saveFileDialog(defaultPath=exporterOptions.fileLocation) if savepath == False: # save was canceled @@ -1035,100 +841,18 @@ def notify(self, args): self.current.filePath = str(updatedPath) else: savepath = processedFileName + adsk.doEvents() # get active document design = gm.app.activeDocument.design name = design.rootComponent.name.rsplit(" ", 1)[0] version = design.rootComponent.name.rsplit(" ", 1)[1] - _exportWheels = [] # all selected wheels, formatted for parseOptions - _exportJoints = [] # all selected joints, formatted for parseOptions _exportGamepieces = [] # TODO work on the code to populate Gamepiece _robotWeight = float _mode: ExportMode _location: ExportLocation - """ - Loops through all rows in the wheel table to extract all the input values - """ - onSelect = gm.handlers[3] - wheelTableInput = wheelTable() - for row in range(wheelTableInput.rowCount): - if row == 0: - continue - - wheelTypeIndex = wheelTableInput.getInputAtPosition( - row, 2 - ).selectedItem.index # This must be either 0 or 1 for standard or omni - - signalTypeIndex = wheelTableInput.getInputAtPosition(row, 3).selectedItem.index - - _exportWheels.append( - Wheel( - WheelListGlobal[row - 1].entityToken, - WheelType(wheelTypeIndex + 1), - SignalType(signalTypeIndex + 1), - # onSelect.wheelJointList[row-1][0] # GUID of wheel joint. if no joint found, default to None - ) - ) - - """ - Loops through all rows in the joint table to extract the input values - """ - jointTableInput = jointTable() - for row in range(jointTableInput.rowCount): - if row == 0: - continue - - parentJointIndex = jointTableInput.getInputAtPosition( - row, 2 - ).selectedItem.index # parent joint index, int - - signalTypeIndex = jointTableInput.getInputAtPosition( - row, 3 - ).selectedItem.index # signal type index, int - - # typeString = jointTableInput.getInputAtPosition( - # row, 0 - # ).name - - jointSpeed = jointTableInput.getInputAtPosition(row, 4).value - - jointForce = jointTableInput.getInputAtPosition(row, 5).value - - parentJointToken = "" - - if parentJointIndex == 0: - _exportJoints.append( - Joint( - JointListGlobal[row - 1].entityToken, - JointParentType.ROOT, - SignalType(signalTypeIndex + 1), - jointSpeed, - jointForce / 100.0, - ) # parent joint GUID - ) - continue - elif parentJointIndex < row: - parentJointToken = JointListGlobal[parentJointIndex - 1].entityToken # parent joint GUID, str - else: - parentJointToken = JointListGlobal[parentJointIndex + 1].entityToken # parent joint GUID, str - - # for wheel in _exportWheels: - # find some way to get joint - # 1. Compare Joint occurrence1 to wheel.occurrenceToken - # 2. if true set the parent to Root - - _exportJoints.append( - Joint( - JointListGlobal[row - 1].entityToken, - parentJointToken, - SignalType(signalTypeIndex + 1), - jointSpeed, - jointForce, - ) - ) - """ Loops through all rows in the gamepiece table to extract the input values """ @@ -1194,6 +918,8 @@ def notify(self, args): .children.itemById("compress") ).value + selectedJoints, selectedWheels = jointConfigTab.getSelectedJointsAndWheels() + export_as_part_boolean = ( eventArgs.command.commandInputs.itemById("advanced_settings") .children.itemById("exporter_settings") @@ -1205,8 +931,8 @@ def notify(self, args): name, version, materials=0, - joints=_exportJoints, - wheels=_exportWheels, + joints=selectedJoints, + wheels=selectedWheels, gamepieces=_exportGamepieces, preferredUnits=selectedUnits, robotWeight=_robotWeight, @@ -1218,6 +944,11 @@ def notify(self, args): _: bool = Parser(exporterOptions).export() exporterOptions.writeToDesign() + + # All selections should be reset AFTER a successful export and save. + # If we run into an exporting error we should return back to the panel with all current options + # still in tact. Even if they did not save. + jointConfigTab.reset() except: if gm.ui: gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) @@ -1246,55 +977,19 @@ def notify(self, args): auto_calc_weight_f = INPUTS_ROOT.itemById("auto_calc_weight_f") - removeWheelInput = INPUTS_ROOT.itemById("wheel_delete") - removeJointInput = INPUTS_ROOT.itemById("joint_delete") + addFieldInput = INPUTS_ROOT.itemById("field_add") removeFieldInput = INPUTS_ROOT.itemById("field_delete") - addWheelInput = INPUTS_ROOT.itemById("wheel_add") - addJointInput = INPUTS_ROOT.itemById("joint_add") - addFieldInput = INPUTS_ROOT.itemById("field_add") + # Transition: AARD-1685 + # This is how all preview handles should be done in the future + jointConfigTab.handlePreviewEvent(args) - wheelTableInput = wheelTable() - jointTableInput = jointTable() gamepieceTableInput = gamepieceTable() - - if wheelTableInput.rowCount <= 1: - removeWheelInput.isEnabled = False - else: - removeWheelInput.isEnabled = True - - if jointTableInput.rowCount <= 1: - removeJointInput.isEnabled = False - else: - removeJointInput.isEnabled = True - if gamepieceTableInput.rowCount <= 1: removeFieldInput.isEnabled = auto_calc_weight_f.isEnabled = False else: removeFieldInput.isEnabled = auto_calc_weight_f.isEnabled = True - if not addWheelInput.isEnabled or not removeWheelInput: - # for wheel in WheelListGlobal: - # wheel.component.opacity = 0.25 - # CustomGraphics.createTextGraphics(wheel, WheelListGlobal) - - gm.app.activeViewport.refresh() - else: - gm.app.activeDocument.design.rootComponent.opacity = 1 - for group in gm.app.activeDocument.design.rootComponent.customGraphicsGroups: - group.deleteMe() - - if not addJointInput.isEnabled or not removeJointInput: # TODO: improve joint highlighting - # for joint in JointListGlobal: - # CustomGraphics.highlightJointedOccurrences(joint) - - # gm.app.activeViewport.refresh() - gm.app.activeDocument.design.rootComponent.opacity = 0.15 - # else: - # for group in gm.app.activeDocument.design.rootComponent.customGraphicsGroups: - # group.deleteMe() - # gm.app.activeDocument.design.rootComponent.opacity = 1 - if not addFieldInput.isEnabled or not removeFieldInput: for gamepiece in GamepieceListGlobal: gamepiece.component.opacity = 0.25 @@ -1470,23 +1165,12 @@ def notify(self, args: adsk.core.SelectionEventArgs): selectionInput.isEnabled = False selectionInput.isVisible = False + # Transition: AARD-1685 + # This is how all handle selection events should be done in the future although it will look + # slightly differently for each type of handle. elif self.selectedJoint: - self.cmd.setCursor("", 0, 0) - jointType = self.selectedJoint.jointMotion.jointType - if jointType == JointMotions.REVOLUTE.value or jointType == JointMotions.SLIDER.value: - if jointType == JointMotions.REVOLUTE.value and MySelectHandler.lastInputCmd.id == "wheel_select": - addWheelToTable(self.selectedJoint) - elif jointType == JointMotions.REVOLUTE.value and MySelectHandler.lastInputCmd.id == "wheel_remove": - if self.selectedJoint in WheelListGlobal: - removeWheelFromTable(WheelListGlobal.index(self.selectedJoint)) - else: - if self.selectedJoint not in JointListGlobal: - addJointToTable(self.selectedJoint) - else: - removeJointFromTable(self.selectedJoint) - - selectionInput.isEnabled = False - selectionInput.isVisible = False + self.cmd.setCursor("", 0, 0) # Reset select cursor back to normal cursor. + jointConfigTab.handleSelectionEvent(args, self.selectedJoint) except: if gm.ui: gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) @@ -1651,29 +1335,23 @@ def notify(self, args): try: eventArgs = adsk.core.InputChangedEventArgs.cast(args) cmdInput = eventArgs.input + + # Transition: AARD-1685 + # Should be how all input changed handles are done in the future + jointConfigTab.handleInputChanged(args, INPUTS_ROOT) + MySelectHandler.lastInputCmd = cmdInput inputs = cmdInput.commandInputs onSelect = gm.handlers[3] frictionCoeff = INPUTS_ROOT.itemById("friction_override_coeff") - wheelSelect = inputs.itemById("wheel_select") - jointSelect = inputs.itemById("joint_select") gamepieceSelect = inputs.itemById("gamepiece_select") - - wheelTableInput = wheelTable() - jointTableInput = jointTable() gamepieceTableInput = gamepieceTable() weightTableInput = inputs.itemById("weight_table") weight_input = INPUTS_ROOT.itemById("weight_input") - - wheelConfig = inputs.itemById("wheel_config") - jointConfig = inputs.itemById("joint_config") gamepieceConfig = inputs.itemById("gamepiece_config") - - addWheelInput = INPUTS_ROOT.itemById("wheel_add") - addJointInput = INPUTS_ROOT.itemById("joint_add") addFieldInput = INPUTS_ROOT.itemById("field_add") indicator = INPUTS_ROOT.itemById("algorithmic_indicator") @@ -1693,47 +1371,13 @@ def notify(self, args): gamepieceConfig.isVisible = False weightTableInput.isVisible = True - addFieldInput.isEnabled = wheelConfig.isVisible = jointConfig.isVisible = True + addFieldInput.isEnabled = True elif modeDropdown.selectedItem.index == 1: if gamepieceConfig: gm.ui.activeSelections.clear() gm.app.activeDocument.design.rootComponent.opacity = 1 - addWheelInput.isEnabled = addJointInput.isEnabled = gamepieceConfig.isVisible = True - - jointConfig.isVisible = wheelConfig.isVisible = weightTableInput.isVisible = False - - elif cmdInput.id == "joint_config": - gm.app.activeDocument.design.rootComponent.opacity = 1 - - elif cmdInput.id == "placeholder_w" or cmdInput.id == "name_w" or cmdInput.id == "signal_type_w": - self.reset() - - wheelSelect.isEnabled = False - addWheelInput.isEnabled = True - - cmdInput_str = cmdInput.id - - if cmdInput_str == "placeholder_w": - position = wheelTableInput.getPosition(adsk.core.ImageCommandInput.cast(cmdInput))[1] - 1 - elif cmdInput_str == "name_w": - position = wheelTableInput.getPosition(adsk.core.TextBoxCommandInput.cast(cmdInput))[1] - 1 - elif cmdInput_str == "signal_type_w": - position = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput))[1] - 1 - - gm.ui.activeSelections.add(WheelListGlobal[position]) - - elif ( - cmdInput.id == "placeholder" - or cmdInput.id == "name_j" - or cmdInput.id == "joint_parent" - or cmdInput.id == "signal_type" - ): - self.reset() - jointSelect.isEnabled = False - addJointInput.isEnabled = True - elif cmdInput.id == "blank_gp" or cmdInput.id == "name_gp" or cmdInput.id == "weight_gp": self.reset() @@ -1753,54 +1397,6 @@ def notify(self, args): gm.ui.activeSelections.add(GamepieceListGlobal[position]) - elif cmdInput.id == "wheel_type_w": - self.reset() - - wheelSelect.isEnabled = False - addWheelInput.isEnabled = True - - cmdInput_str = cmdInput.id - position = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput))[1] - 1 - wheelDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - - if wheelDropdown.selectedItem.index == 0: - getPosition = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput)) - iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - iconInput.imageFile = IconPaths.wheelIcons["standard"] - iconInput.tooltip = "Standard wheel" - - elif wheelDropdown.selectedItem.index == 1: - getPosition = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput)) - iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - iconInput.imageFile = IconPaths.wheelIcons["omni"] - iconInput.tooltip = "Omni wheel" - - elif wheelDropdown.selectedItem.index == 2: - getPosition = wheelTableInput.getPosition(adsk.core.DropDownCommandInput.cast(cmdInput)) - iconInput = wheelTableInput.getInputAtPosition(getPosition[1], 0) - iconInput.imageFile = IconPaths.wheelIcons["mecanum"] - iconInput.tooltip = "Mecanum wheel" - - gm.ui.activeSelections.add(WheelListGlobal[position]) - - elif cmdInput.id == "wheel_add": - self.reset() - - wheelSelect.isVisible = True - wheelSelect.isEnabled = True - wheelSelect.clearSelection() - addJointInput.isEnabled = True - addWheelInput.isEnabled = False - - elif cmdInput.id == "joint_add": - self.reset() - - addWheelInput.isEnabled = True - jointSelect.isVisible = True - jointSelect.isEnabled = True - jointSelect.clearSelection() - addJointInput.isEnabled = False - elif cmdInput.id == "field_add": self.reset() @@ -1809,31 +1405,6 @@ def notify(self, args): gamepieceSelect.clearSelection() addFieldInput.isEnabled = False - elif cmdInput.id == "wheel_delete": - # Currently causes Internal Autodesk Error - # gm.ui.activeSelections.clear() - - addWheelInput.isEnabled = True - if wheelTableInput.selectedRow == -1 or wheelTableInput.selectedRow == 0: - wheelTableInput.selectedRow = wheelTableInput.rowCount - 1 - gm.ui.messageBox("Select a row to delete.") - else: - index = wheelTableInput.selectedRow - 1 - removeWheelFromTable(index) - - elif cmdInput.id == "joint_delete": - gm.ui.activeSelections.clear() - - addJointInput.isEnabled = True - addWheelInput.isEnabled = True - - if jointTableInput.selectedRow == -1 or jointTableInput.selectedRow == 0: - jointTableInput.selectedRow = jointTableInput.rowCount - 1 - gm.ui.messageBox("Select a row to delete.") - else: - joint = JointListGlobal[jointTableInput.selectedRow - 1] - removeJointFromTable(joint) - elif cmdInput.id == "field_delete": gm.ui.activeSelections.clear() @@ -1846,19 +1417,13 @@ def notify(self, args): index = gamepieceTableInput.selectedRow - 1 removeGamePieceFromTable(index) - elif cmdInput.id == "wheel_select": - addWheelInput.isEnabled = True - - elif cmdInput.id == "joint_select": - addJointInput.isEnabled = True - elif cmdInput.id == "gamepiece_select": addFieldInput.isEnabled = True elif cmdInput.id == "friction_override": boolValue = adsk.core.BoolValueCommandInput.cast(cmdInput) - if boolValue.value == True: + if boolValue.value: frictionCoeff.isVisible = True else: frictionCoeff.isVisible = False @@ -1972,8 +1537,7 @@ def notify(self, args): try: onSelect = gm.handlers[3] - WheelListGlobal.clear() - JointListGlobal.clear() + jointConfigTab.reset() GamepieceListGlobal.clear() onSelect.allWheelPreselections.clear() onSelect.wheelJointList.clear() @@ -1992,197 +1556,6 @@ def notify(self, args): ) -def addJointToTable(joint: adsk.fusion.Joint) -> None: - """### Adds a Joint object to its global list and joint table. - - Args: - joint (adsk.fusion.Joint): Joint object to be added - """ - try: - JointListGlobal.append(joint) - jointTableInput = jointTable() - cmdInputs = adsk.core.CommandInputs.cast(jointTableInput.commandInputs) - - # joint type icons - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Rigid", IconPaths.jointIcons["rigid"]) - icon.tooltip = "Rigid joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Revolute", IconPaths.jointIcons["revolute"]) - icon.tooltip = "Revolute joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Slider", IconPaths.jointIcons["slider"]) - icon.tooltip = "Slider joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Planar", IconPaths.jointIcons["planar"]) - icon.tooltip = "Planar joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"]) - icon.tooltip = "Pin slot joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"]) - icon.tooltip = "Cylindrical joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: - icon = cmdInputs.addImageCommandInput("placeholder", "Ball", IconPaths.jointIcons["ball"]) - icon.tooltip = "Ball joint" - - # joint name - name = cmdInputs.addTextBoxCommandInput("name_j", "Occurrence name", "", 1, True) - name.tooltip = joint.name - name.formattedText = "

{}

".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 = "
The root component is usually the parent." - - signalType = cmdInputs.addDropDownCommandInput( - "signal_type", - "Signal Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) - signalType.tooltip = "Signal type" - - row = jointTableInput.rowCount - - jointTableInput.addCommandInput(icon, row, 0) - jointTableInput.addCommandInput(name, row, 1) - jointTableInput.addCommandInput(jointType, row, 2) - jointTableInput.addCommandInput(signalType, row, 3) - - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - jointSpeed = cmdInputs.addValueInput( - "joint_speed", - "Speed", - "deg", - adsk.core.ValueInput.createByReal(3.1415926), - ) - jointSpeed.tooltip = "Degrees per second" - jointTableInput.addCommandInput(jointSpeed, row, 4) - - jointForce = cmdInputs.addValueInput("joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000)) - jointForce.tooltip = "Newton-Meters***" - jointTableInput.addCommandInput(jointForce, row, 5) - - if joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: - jointSpeed = cmdInputs.addValueInput( - "joint_speed", - "Speed", - "m", - adsk.core.ValueInput.createByReal(100), - ) - jointSpeed.tooltip = "Meters per second" - jointTableInput.addCommandInput(jointSpeed, row, 4) - - jointForce = cmdInputs.addValueInput("joint_force", "Force", "N", adsk.core.ValueInput.createByReal(5000)) - jointForce.tooltip = "Newtons" - jointTableInput.addCommandInput(jointForce, row, 5) - - except: - gm.ui.messageBox("Failed:\n{}".format(traceback.format_exc())) - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addJointToTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - - -def addWheelToTable(wheel: adsk.fusion.Joint) -> None: - """### Adds a wheel occurrence to its global list and wheel table. - - Args: - wheel (adsk.fusion.Occurrence): wheel Occurrence object to be added. - """ - try: - try: - onSelect = gm.handlers[3] - onSelect.allWheelPreselections.append(wheel.entityToken) - except IndexError: - # Not 100% sure what we need the select handler here for however it should not run when - # first populating the saved wheel configs. This will naturally throw a IndexError as - # we do this before the initialization of gm.handlers[] - pass - - wheelTableInput = wheelTable() - # def addPreselections(child_occurrences): - # for occ in child_occurrences: - # onSelect.allWheelPreselections.append(occ.entityToken) - - # if occ.childOccurrences: - # addPreselections(occ.childOccurrences) - - # if wheel.childOccurrences: - # addPreselections(wheel.childOccurrences) - # else: - - WheelListGlobal.append(wheel) - cmdInputs = adsk.core.CommandInputs.cast(wheelTableInput.commandInputs) - - icon = cmdInputs.addImageCommandInput("placeholder_w", "Placeholder", IconPaths.wheelIcons["standard"]) - - name = cmdInputs.addTextBoxCommandInput("name_w", "Joint name", wheel.name, 1, True) - name.tooltip = wheel.name - - wheelType = cmdInputs.addDropDownCommandInput( - "wheel_type_w", - "Wheel Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - wheelType.listItems.add("Standard", True, "") - wheelType.listItems.add("Omni", False, "") - wheelType.tooltip = "Wheel type" - wheelType.tooltipDescription = "
Omni-directional wheels can be used just like regular drive wheels but they have the advantage of being able to roll freely perpendicular to the drive direction.
" - wheelType.toolClipFilename = OsHelper.getOSPath(".", "src", "Resources") + os.path.join( - "WheelIcons", "omni-wheel-preview.png" - ) - - signalType = cmdInputs.addDropDownCommandInput( - "signal_type_w", - "Signal Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - signalType.isFullWidth = True - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) - signalType.tooltip = "Signal type" - - row = wheelTableInput.rowCount - - wheelTableInput.addCommandInput(icon, row, 0) - wheelTableInput.addCommandInput(name, row, 1) - wheelTableInput.addCommandInput(wheelType, row, 2) - wheelTableInput.addCommandInput(signalType, row, 3) - - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addWheelToTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - - def addGamepieceToTable(gamepiece: adsk.fusion.Occurrence) -> None: """### Adds a gamepiece occurrence to its global list and gamepiece table. @@ -2258,79 +1631,6 @@ def addPreselections(child_occurrences): ) -def removeWheelFromTable(index: int) -> None: - """### Removes a wheel joint from its global list and wheel table. - - Args: - index (int): index of wheel item in its global list - """ - try: - onSelect = gm.handlers[3] - wheelTableInput = wheelTable() - wheel = WheelListGlobal[index] - - # def removePreselections(child_occurrences): - # for occ in child_occurrences: - # onSelect.allWheelPreselections.remove(occ.entityToken) - - # if occ.childOccurrences: - # removePreselections(occ.childOccurrences) - - # if wheel.childOccurrences: - # removePreselections(wheel.childOccurrences) - # else: - onSelect.allWheelPreselections.remove(wheel.entityToken) - - del WheelListGlobal[index] - wheelTableInput.deleteRow(index + 1) - - # updateJointTable(wheel) - except IndexError: - pass - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeWheelFromTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - - -def removeJointFromTable(joint: adsk.fusion.Joint) -> None: - """### Removes a joint occurrence from its global list and joint table. - - Args: - joint (adsk.fusion.Joint): Joint object to be removed - """ - try: - index = JointListGlobal.index(joint) - jointTableInput = jointTable() - JointListGlobal.remove(joint) - - jointTableInput.deleteRow(index + 1) - - for row in range(jointTableInput.rowCount): - if row == 0: - continue - - dropDown = jointTableInput.getInputAtPosition(row, 2) - listItems = dropDown.listItems - - if row > index: - if listItems.item(index + 1).isSelected: - listItems.item(index).isSelected = True - listItems.item(index + 1).deleteMe() - else: - listItems.item(index + 1).deleteMe() - else: - if listItems.item(index).isSelected: - listItems.item(index - 1).isSelected = True - listItems.item(index).deleteMe() - else: - listItems.item(index).deleteMe() - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeJointFromTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - - def removeGamePieceFromTable(index: int) -> None: """### Removes a gamepiece occurrence from its global list and gamepiece table. diff --git a/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py b/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py new file mode 100644 index 0000000000..ac3996b537 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py @@ -0,0 +1,96 @@ +import logging +import traceback + +import adsk.core + +from ..general_imports import INTERNAL_ID + + +def createTableInput( + id: str, + name: str, + inputs: adsk.core.CommandInputs, + columns: int, + ratio: str, + minRows: int = 1, + maxRows: int = 50, + columnSpacing: int = 0, + rowSpacing: int = 0, +) -> adsk.core.TableCommandInput: + try: + input = inputs.addTableCommandInput(id, name, columns, ratio) + input.minimumVisibleRows = minRows + input.maximumVisibleRows = maxRows + input.columnSpacing = columnSpacing + input.rowSpacing = rowSpacing + + return input + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.CreateCommandInputsHelper").error( + "Failed:\n{}".format(traceback.format_exc()) + ) + + +def createBooleanInput( + id: str, + name: str, + inputs: adsk.core.CommandInputs, + tooltip: str = "", + tooltipadvanced: str = "", + checked: bool = True, + enabled: bool = True, + isCheckBox: bool = True, +) -> adsk.core.BoolValueCommandInput: + try: + input = inputs.addBoolValueInput(id, name, isCheckBox) + input.value = checked + input.isEnabled = enabled + input.tooltip = tooltip + input.tooltipDescription = tooltipadvanced + + return input + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.CreateCommandInputsHelper").error( + "Failed:\n{}".format(traceback.format_exc()) + ) + + +def createTextBoxInput( + id: str, + name: str, + inputs: adsk.core.CommandInputs, + text: str, + italics: bool = True, + bold: bool = True, + fontSize: int = 10, + alignment: str = "center", + rowCount: int = 1, + read: bool = True, + background: str = "whitesmoke", + tooltip: str = "", + advanced_tooltip: str = "", +) -> adsk.core.TextBoxCommandInput: + try: + if bold: + text = f"{text}" + + if italics: + text = f"{text}" + + outputText = f""" +
+

+ {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 = "
The root component is usually the parent." + + signalType = commandInputs.addDropDownCommandInput( + "signalTypeJoint", + "Signal Type", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + + # Invisible white space characters are required in the list item name field to make this work. + # I have no idea why, Fusion API needs some special education help - Brandon + if synJoint: + signalType.listItems.add("‎", synJoint.signalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", synJoint.signalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) + signalType.listItems.add( + "‎", synJoint.signalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"] + ) + else: + signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) + + signalType.tooltip = "Signal type" + + row = self.jointConfigTable.rowCount + self.jointConfigTable.addCommandInput(icon, row, 0) + self.jointConfigTable.addCommandInput(name, row, 1) + self.jointConfigTable.addCommandInput(jointType, row, 2) + self.jointConfigTable.addCommandInput(signalType, row, 3) + + # Comparison by `==` over `is` because the Autodesk API does not use `Enum` for their enum classes + if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + if synJoint: + jointSpeedValue = synJoint.speed + else: + jointSpeedValue = 3.1415926 + + jointSpeed = commandInputs.addValueInput( + "jointSpeed", + "Speed", + "deg", + adsk.core.ValueInput.createByReal(jointSpeedValue), + ) + jointSpeed.tooltip = "Degrees per second" + self.jointConfigTable.addCommandInput(jointSpeed, row, 4) + + elif fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: + if synJoint: + jointSpeedValue = synJoint.speed + else: + jointSpeedValue = 100 + + jointSpeed = commandInputs.addValueInput( + "jointSpeed", + "Speed", + "m", + adsk.core.ValueInput.createByReal(jointSpeedValue), + ) + jointSpeed.tooltip = "Meters per second" + self.jointConfigTable.addCommandInput(jointSpeed, row, 4) + + if synJoint: + jointForceValue = synJoint.force * 100 # Currently a factor of 100 - Should be investigated + else: + jointForceValue = 5 + + jointForce = commandInputs.addValueInput( + "jointForce", "Force", "N", adsk.core.ValueInput.createByReal(jointForceValue) + ) + jointForce.tooltip = "Newtons" + self.jointConfigTable.addCommandInput(jointForce, row, 5) + + if fusionJoint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: + wheelCheckboxEnabled = True + wheelCheckboxTooltip = "Determines if this joint should be counted as a wheel." + else: + wheelCheckboxEnabled = False + wheelCheckboxTooltip = "Only Revolute joints can be treated as wheels." + + isWheel = synJoint.isWheel if synJoint else False + + # Transition: AARD-1685 + # All command inputs should be created using the helpers. + self.jointConfigTable.addCommandInput( + createBooleanInput( + "isWheel", + "Is Wheel", + commandInputs, + wheelCheckboxTooltip, + checked=isWheel, + enabled=wheelCheckboxEnabled, + ), + row, + 6, + ) + + self.previousWheelCheckboxState.append(isWheel) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) + + return True + + def addWheel(self, joint: adsk.fusion.Joint, wheel: Wheel | None = None) -> None: + try: + self.jointWheelIndexMap[joint.entityToken] = self.wheelConfigTable.rowCount + + commandInputs = self.wheelConfigTable.commandInputs + wheelIcon = commandInputs.addImageCommandInput( + "wheelPlaceholder", "Placeholder", IconPaths.wheelIcons["standard"] + ) + wheelIcon.tooltip = "Standard wheel" + wheelName = commandInputs.addTextBoxCommandInput("wheelName", "Joint Name", joint.name, 1, True) + wheelName.tooltip = joint.name # TODO: Should this be the same? + wheelType = commandInputs.addDropDownCommandInput( + "wheelType", "Wheel Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle + ) + + selectedWheelType = wheel.wheelType if wheel else WheelType.STANDARD + wheelType.listItems.add("Standard", selectedWheelType is WheelType.STANDARD, "") + wheelType.listItems.add("OMNI", selectedWheelType is WheelType.OMNI, "") + wheelType.listItems.add("Mecanum", selectedWheelType is WheelType.MECANUM, "") + wheelType.tooltip = "Wheel type" + wheelType.tooltipDescription = "".join( + [ + "
Omni-directional wheels can be used just like regular drive wheels", + "but they have the advantage of being able to roll freely perpendicular to", + "the drive direction.
", + ] + ) + + signalType = commandInputs.addDropDownCommandInput( + "wheelSignalType", "Signal Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle + ) + signalType.isFullWidth = True + signalType.isEnabled = False + signalType.tooltip = "Wheel signal type is linked with the respective joint signal type." + i = self.selectedJointList.index(joint) + jointSignalType = SignalType(self.jointConfigTable.getInputAtPosition(i + 1, 3).selectedItem.index + 1) + signalType.listItems.add("‎", jointSignalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) + signalType.listItems.add("‎", jointSignalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) + signalType.listItems.add("‎", jointSignalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) + + row = self.wheelConfigTable.rowCount + self.wheelConfigTable.addCommandInput(wheelIcon, row, 0) + self.wheelConfigTable.addCommandInput(wheelName, row, 1) + self.wheelConfigTable.addCommandInput(wheelType, row, 2) + self.wheelConfigTable.addCommandInput(signalType, row, 3) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) + + def removeIndexedJoint(self, index: int) -> None: + try: + self.removeJoint(self.selectedJointList[index]) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) + + def removeJoint(self, joint: adsk.fusion.Joint) -> None: + try: + if self.jointWheelIndexMap.get(joint.entityToken): + self.removeWheel(joint) + + i = self.selectedJointList.index(joint) + self.selectedJointList.remove(joint) + self.previousWheelCheckboxState.pop(i) + self.jointConfigTable.deleteRow(i + 1) + for row in range(1, self.jointConfigTable.rowCount): # Row is 1 indexed + # TODO: Step through this in the debugger and figure out if this is all necessary. + listItems = self.jointConfigTable.getInputAtPosition(row, 2).listItems + if row > i: + if listItems.item(i + 1).isSelected: + listItems.item(i).isSelected = True + listItems.item(i + 1).deleteMe() + else: + listItems.item(i + 1).deleteMe() + else: + if listItems.item(i).isSelected: + listItems.item(i - 1).isSelected = True + listItems.item(i).deleteMe() + else: + listItems.item(i).deleteMe() + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) + + def removeWheel(self, joint: adsk.fusion.Joint) -> None: + try: + row = self.jointWheelIndexMap[joint.entityToken] + self.wheelConfigTable.deleteRow(row) + del self.jointWheelIndexMap[joint.entityToken] + for key, value in self.jointWheelIndexMap.items(): + if value > row - 1: + self.jointWheelIndexMap[key] -= 1 + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) + + def getSelectedJointsAndWheels(self) -> tuple[list[Joint], list[Wheel]]: + try: + joints: list[Joint] = [] + wheels: list[Wheel] = [] + for row in range(1, self.jointConfigTable.rowCount): # Row is 1 indexed + jointEntityToken = self.selectedJointList[row - 1].entityToken + signalTypeIndex = self.jointConfigTable.getInputAtPosition(row, 3).selectedItem.index + signalType = SignalType(signalTypeIndex + 1) + jointSpeed: float = self.jointConfigTable.getInputAtPosition(row, 4).value + jointForce: float = self.jointConfigTable.getInputAtPosition(row, 5).value + isWheel: bool = self.jointConfigTable.getInputAtPosition(row, 6).value + + joints.append( + Joint( + jointEntityToken, + JointParentType.ROOT, + signalType, + jointSpeed, + jointForce / 100.0, + isWheel, + ) + ) + + if isWheel: + wheelRow = self.jointWheelIndexMap[jointEntityToken] + wheelTypeIndex = self.wheelConfigTable.getInputAtPosition(wheelRow, 2).selectedItem.index + wheels.append( + Wheel( + jointEntityToken, + WheelType(wheelTypeIndex + 1), + signalType, + ) + ) + + return (joints, wheels) + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) + + def reset(self) -> None: + self.selectedJointList.clear() + self.previousWheelCheckboxState.clear() + self.jointWheelIndexMap.clear() + + # Transition: AARD-1685 + # Find a way to not pass the global commandInputs into this function + # Perhaps get the joint tab from the args then get what we want? + # Idk the Fusion API seems to think that you would never need to change anything other than the effected + # commandInput in a input changed handle for some reason. + def handleInputChanged( + self, args: adsk.core.InputChangedEventArgs, globalCommandInputs: adsk.core.CommandInputs + ) -> None: + try: + commandInput = args.input + if commandInput.id == "wheelType": + wheelTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) + position = self.wheelConfigTable.getPosition(wheelTypeDropdown)[1] + iconInput: adsk.core.ImageCommandInput = self.wheelConfigTable.getInputAtPosition(position, 0) + + if wheelTypeDropdown.selectedItem.index == 0: + iconInput.imageFile = IconPaths.wheelIcons["standard"] + iconInput.tooltip = "Standard wheel" + elif wheelTypeDropdown.selectedItem.index == 1: + iconInput.imageFile = IconPaths.wheelIcons["omni"] + iconInput.tooltip = "Omni wheel" + elif wheelTypeDropdown.selectedItem.index == 2: + iconInput.imageFile = IconPaths.wheelIcons["mecanum"] + iconInput.tooltip = "Mecanum wheel" + + elif commandInput.id == "isWheel": + isWheelCheckbox = adsk.core.BoolValueCommandInput.cast(commandInput) + position = self.jointConfigTable.getPosition(isWheelCheckbox)[1] - 1 + isAlreadyWheel = bool(self.jointWheelIndexMap.get(self.selectedJointList[position].entityToken)) + + if isWheelCheckbox.value != self.previousWheelCheckboxState[position]: + if not isAlreadyWheel: + self.addWheel(self.selectedJointList[position]) + else: + self.removeWheel(self.selectedJointList[position]) + + self.previousWheelCheckboxState[position] = isWheelCheckbox.value + + elif commandInput.id == "signalTypeJoint": + signalTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) + jointTabPosition = self.jointConfigTable.getPosition(signalTypeDropdown)[1] # 1 indexed + wheelTabPosition = self.jointWheelIndexMap.get(self.selectedJointList[jointTabPosition - 1].entityToken) + + if wheelTabPosition: + wheelSignalItems: adsk.core.DropDownCommandInput = self.wheelConfigTable.getInputAtPosition( + wheelTabPosition, 3 + ) + wheelSignalItems.listItems.item(signalTypeDropdown.selectedItem.index).isSelected = True + + elif commandInput.id == "jointAddButton": + jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") + jointRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointRemoveButton") + jointSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById( + "jointSelectCancelButton" + ) + jointSelection: adsk.core.SelectionCommandInput = globalCommandInputs.itemById("jointSelection") + + jointSelection.isVisible = jointSelection.isEnabled = True + jointSelection.clearSelection() + jointAddButton.isEnabled = jointRemoveButton.isEnabled = False + jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = True + + elif commandInput.id == "jointRemoveButton": + jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") + jointTable: adsk.core.TableCommandInput = args.inputs.itemById("jointTable") + + jointAddButton.isEnabled = True + + if jointTable.selectedRow == -1 or jointTable.selectedRow == 0: + ui = adsk.core.Application.get().userInterface + ui.messageBox("Select a row to delete.") + else: + self.removeIndexedJoint(jointTable.selectedRow - 1) # selectedRow is 1 indexed + + elif commandInput.id == "jointSelectCancelButton": + jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton") + jointRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointRemoveButton") + jointSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById( + "jointSelectCancelButton" + ) + jointSelection: adsk.core.SelectionCommandInput = globalCommandInputs.itemById("jointSelection") + jointSelection.isEnabled = jointSelection.isVisible = False + jointSelectCancelButton.isEnabled = jointSelectCancelButton.isVisible = False + jointAddButton.isEnabled = jointRemoveButton.isEnabled = True + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) + + def handleSelectionEvent(self, args: adsk.core.SelectionEventArgs, selectedJoint: adsk.fusion.Joint) -> None: + try: + selectionInput = args.activeInput + jointType = selectedJoint.jointMotion.jointType + if ( + jointType == adsk.fusion.JointTypes.RevoluteJointType + or jointType == adsk.fusion.JointTypes.SliderJointType + ): + if not self.addJoint(selectedJoint): + ui = adsk.core.Application.get().userInterface + result = ui.messageBox( + "You have already selected this joint.\n" "Would you like to remove it?", + "Synthesis: Remove Joint Confirmation", + adsk.core.MessageBoxButtonTypes.YesNoButtonType, + adsk.core.MessageBoxIconTypes.QuestionIconType, + ) + + if result == adsk.core.DialogResults.DialogYes: + self.removeJoint(selectedJoint) + + selectionInput.isEnabled = selectionInput.isVisible = False + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc())) + + def handlePreviewEvent(self, args: adsk.core.CommandEventArgs) -> None: + try: + commandInputs = args.command.commandInputs + jointAddButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointAddButton") + jointRemoveButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointRemoveButton") + jointSelectCancelButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointSelectCancelButton") + jointSelection: adsk.core.SelectionCommandInput = commandInputs.itemById("jointSelection") + + if self.jointConfigTable.rowCount <= 1: + jointRemoveButton.isEnabled = False + + if not jointSelection.isEnabled: + jointAddButton.isEnabled = jointRemoveButton.isEnabled = True + jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = False + except BaseException: + logging.getLogger("{INTERNAL_ID}.UI.JointConfigTab").error("Failed:\n{}".format(traceback.format_exc()))