diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 0ebf37af93..e85b577397 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -42,6 +42,7 @@ class ExporterOptions: wheels: list[Wheel] = field(default_factory=list) joints: list[Joint] = field(default_factory=list) gamepieces: list[Gamepiece] = field(default_factory=list) + tags: dict[str, str] = field(default_factory=dict) robotWeight: KG = field(default=KG(0.0)) autoCalcRobotWeight: bool = field(default=False) autoCalcGamepieceWeight: bool = field(default=False) diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index 6a78ad7cdf..a52af496df 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -53,6 +53,9 @@ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> None: fill_info(part_body, body) part_body.part = comp_ref + if body.entityToken in options.tags: + partsData.user_data.data[f"tag_{body.entityToken}"] = options.tags[body.entityToken] + if isinstance(body, adsk.fusion.BRepBody): _ParseBRep(body, options, part_body.triangle_mesh) else: diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py index b085b48d32..274dfd6432 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -24,6 +24,16 @@ logger = getLogger() +def reload() -> None: + """Reloads the Parser module""" + import importlib + + importlib.reload(Components) + importlib.reload(Joints) + importlib.reload(Materials) + importlib.reload(PDMessage) + + 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 242773d227..06c30f3e47 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -15,9 +15,10 @@ import src.UI.GamepieceConfigTab as GamepieceConfigTab import src.UI.GeneralConfigTab as GeneralConfigTab import src.UI.JointConfigTab as JointConfigTab +import src.UI.TaggingConfigTab as TaggingConfigTab from src import APP_WEBSITE_URL, gm from src.APS.APS import getAuth, getUserInfo -from src.Logging import getLogger, logFailure +from src.Logging import logFailure from src.Parser.ExporterOptions import ExporterOptions from src.Types import SELECTABLE_JOINT_TYPES, ExportLocation, ExportMode from src.UI import FileDialogConfig @@ -26,8 +27,7 @@ generalConfigTab: GeneralConfigTab.GeneralConfigTab jointConfigTab: JointConfigTab.JointConfigTab gamepieceConfigTab: GamepieceConfigTab.GamepieceConfigTab - -logger = getLogger() +taggingConfigTab: TaggingConfigTab.TaggingConfigTab INPUTS_ROOT: adsk.core.CommandInputs @@ -37,9 +37,10 @@ def reload() -> None: importlib.reload(GeneralConfigTab) importlib.reload(GamepieceConfigTab) importlib.reload(JointConfigTab) + importlib.reload(TaggingConfigTab) importlib.reload(Parser) - logger.info("UI modules reloaded successfully.") + Parser.reload() class ConfigureCommandCreatedHandler(adsk.core.CommandCreatedEventHandler): @@ -92,6 +93,10 @@ def notify(self, args: adsk.core.CommandCreatedEventArgs) -> None: jointConfigTab = JointConfigTab.JointConfigTab(args) generalConfigTab.jointConfigTab = jointConfigTab + global taggingConfigTab + taggingConfigTab = TaggingConfigTab.TaggingConfigTab(args) + generalConfigTab.taggingConfigTab = taggingConfigTab + if not exporterOptions.exportMode == ExportMode.FIELD: gamepieceConfigTab.isVisible = False @@ -119,6 +124,12 @@ def notify(self, args: adsk.core.CommandCreatedEventArgs) -> None: if len(fusionJoints): jointConfigTab.addWheel(fusionJoints[0], wheel) + if len(exporterOptions.tags): + for token, tag in exporterOptions.tags.items(): + fusionBody = design.findEntityByToken(token) + if len(fusionBody): + taggingConfigTab.addTag(fusionBody[0], tag) + getAuth() user_info = getUserInfo() apsSettings = INPUTS_ROOT.addTabCommandInput( @@ -154,6 +165,7 @@ def notify(self, _: adsk.core.CommandEventArgs) -> None: selectedJoints, selectedWheels = jointConfigTab.getSelectedJointsAndWheels() selectedGamepieces = gamepieceConfigTab.getGamepieces() + selectedTags = taggingConfigTab.getTags() exporterOptions = ExporterOptions( savepath, @@ -163,6 +175,7 @@ def notify(self, _: adsk.core.CommandEventArgs) -> None: joints=selectedJoints, wheels=selectedWheels, gamepieces=selectedGamepieces, + tags=selectedTags, robotWeight=generalConfigTab.robotWeight, autoCalcRobotWeight=generalConfigTab.autoCalculateWeight, autoCalcGamepieceWeight=gamepieceConfigTab.autoCalculateWeight, @@ -233,6 +246,9 @@ def notify(self, args: adsk.core.InputChangedEventArgs) -> None: if gamepieceConfigTab.isVisible and gamepieceConfigTab.isActive: gamepieceConfigTab.handleInputChanged(args, INPUTS_ROOT) + if taggingConfigTab.isVisible and taggingConfigTab.isActive: + taggingConfigTab.handleInputChanged(args, INPUTS_ROOT) + class MyCommandDestroyHandler(PersistentEventHandler, adsk.core.CommandEventHandler): """Called when the configuration panel is destroyed.""" diff --git a/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py index f734113975..bc75ea7398 100644 --- a/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py @@ -7,6 +7,7 @@ from src.UI.CreateCommandInputsHelper import createBooleanInput from src.UI.GamepieceConfigTab import GamepieceConfigTab from src.UI.JointConfigTab import JointConfigTab +from src.UI.TaggingConfigTab import TaggingConfigTab from src.Util import ( convertMassUnitsFrom, convertMassUnitsTo, @@ -23,6 +24,7 @@ class GeneralConfigTab: previousSelectedModeDropdownIndex: int jointConfigTab: JointConfigTab gamepieceConfigTab: GamepieceConfigTab + taggingConfigTab: TaggingConfigTab @logFailure def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: ExporterOptions) -> None: diff --git a/exporter/SynthesisFusionAddin/src/UI/IconPaths.py b/exporter/SynthesisFusionAddin/src/UI/IconPaths.py index 8b377eb659..0a92adf355 100644 --- a/exporter/SynthesisFusionAddin/src/UI/IconPaths.py +++ b/exporter/SynthesisFusionAddin/src/UI/IconPaths.py @@ -42,3 +42,7 @@ "calculate-enabled": resources + os.path.join("AutoCalcWeight_icon"), # resource folder "friction_override-enabled": resources + os.path.join("FrictionOverride_icon"), # resource folder } + +tagIcons = { + "blank": resources + "blank-preview16x16.png", +} diff --git a/exporter/SynthesisFusionAddin/src/UI/TaggingConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/TaggingConfigTab.py new file mode 100644 index 0000000000..a59f3050ed --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/UI/TaggingConfigTab.py @@ -0,0 +1,168 @@ +import adsk.core +import adsk.fusion + +from src.Logging import getLogger, logFailure +from src.UI import IconPaths +from src.UI.CreateCommandInputsHelper import createTableInput, createTextBoxInput + +logger = getLogger() + + +class TaggingConfigTab: + # stores the types of tags available for selection + tagTypes = ["Softbody", "Rigid", "Chain", "Spring", "Rope"] + tagMap: dict[str, str] = {} + tagList: list[str] = [] + + taggingConfigTab: adsk.core.TabCommandInput + taggingListTable: adsk.core.TableCommandInput + tagTypeDropdown: adsk.core.DropDownCommandInput + + @logFailure + def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None: + self.taggingConfigTab = args.command.commandInputs.addTabCommandInput("taggingOptionsTab", "Tagging Options") + self.taggingConfigTab.tooltip = "Configure tagging options for materials" + taggingConfigTabInputs = self.taggingConfigTab.children + + self.tagTypeDropdown = taggingConfigTabInputs.addDropDownCommandInput( + "tageTypeDropdown", "Tag Type", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle + ) + self.tagTypeDropdown.isFullWidth = False + for tag in self.tagTypes: + self.tagTypeDropdown.listItems.add(tag, False) + self.tagTypeDropdown.isEnabled = self.tagTypeDropdown.isVisible = False + + bodySelection = taggingConfigTabInputs.addSelectionInput( + "tagBodySelect", "Select Body", "Select a single body." + ) + bodySelection.addSelectionFilter("SolidBodies") + bodySelection.addSelectionFilter("SurfaceBodies") + + self.taggingListTable = createTableInput("tagListTable", "Tag List", taggingConfigTabInputs, 6, "1:4:4:4") + self.taggingListTable.addCommandInput( + createTextBoxInput("headerIcon", "", taggingConfigTabInputs, "", bold=False), 0, 0 + ) + self.taggingListTable.addCommandInput( + createTextBoxInput("headerBodyName", "Body", taggingConfigTabInputs, "Body Name", background="#d9d9d9"), + 0, + 1, + ) + self.taggingListTable.addCommandInput( + createTextBoxInput( + "headerComponentName", "Component", taggingConfigTabInputs, "Component Name", background="#d9d9d9" + ), + 0, + 2, + ) + self.taggingListTable.addCommandInput( + createTextBoxInput("headerTagType", "Type", taggingConfigTabInputs, "Tag Type", background="#d9d9d9"), 0, 3 + ) + + addTagInputButton = taggingConfigTabInputs.addBoolValueInput("tagAddButton", "Add", False) + removeTagInputButton = taggingConfigTabInputs.addBoolValueInput("tagRemoveButton", "Remove", False) + cancelInputButton = taggingConfigTabInputs.addBoolValueInput("tagCancelButton", "Cancel", False) + + self.taggingListTable.addToolbarCommandInput(addTagInputButton) + self.taggingListTable.addToolbarCommandInput(removeTagInputButton) + self.taggingListTable.addToolbarCommandInput(cancelInputButton) + + self.toggleSelecting(False) + + @property + def isVisible(self) -> bool: + return self.taggingConfigTab.isVisible or False + + @isVisible.setter + def isVisible(self, value: bool) -> None: + self.taggingConfigTab.isVisible = value + + @property + def isActive(self) -> bool: + return self.taggingConfigTab.isActive or False + + def toggleSelecting(self, value: bool) -> None: + tagAddButton: adsk.core.BoolValueCommandInput = self.taggingConfigTab.commandInputs.itemById("tagAddButton") + tagRemoveButton: adsk.core.BoolValueCommandInput = self.taggingConfigTab.commandInputs.itemById( + "tagRemoveButton" + ) + tagBodySelection: adsk.core.SelectionCommandInput = self.taggingConfigTab.commandInputs.itemById( + "tagBodySelect" + ) + tagCancelButton: adsk.core.BoolValueCommandInput = self.taggingConfigTab.commandInputs.itemById( + "tagCancelButton" + ) + + tagAddButton.isEnabled = tagRemoveButton.isEnabled = not value + tagCancelButton.isVisible = value + self.tagTypeDropdown.isEnabled = self.tagTypeDropdown.isVisible = value + + if not value: + tagBodySelection.clearSelection() + tagBodySelection.setSelectionLimits(0) + tagBodySelection.isEnabled = tagBodySelection.isVisible = False + + else: + tagBodySelection.isVisible = tagBodySelection.isEnabled = True + tagBodySelection.setSelectionLimits(0, 1) + + def addTag(self, body: adsk.fusion.BRepBody, tag: str) -> None: + commandInputs = self.taggingListTable.commandInputs + row = self.taggingListTable.rowCount + + icon = commandInputs.addImageCommandInput(f"tag_icon_{row}", "Ball", IconPaths.tagIcons["blank"]) + icon.tooltip = "Tag" + + bodyName = commandInputs.addTextBoxCommandInput(f"bodyName_{row}", "Body Name", "", 1, True) + bodyName.formattedText = f"

{body.name}

" + + componentName = commandInputs.addTextBoxCommandInput(f"componentName_{row}", "Component Name", "", 1, True) + componentName.formattedText = f"

{body.parentComponent.name}

" + + tagType = commandInputs.addTextBoxCommandInput(f"tagType_{row}", "Tag Type", "", 1, True) + tagType.formattedText = f"

{tag}

" + + self.taggingListTable.addCommandInput(icon, row, 0) + self.taggingListTable.addCommandInput(bodyName, row, 1) + self.taggingListTable.addCommandInput(componentName, row, 2) + self.taggingListTable.addCommandInput(tagType, row, 3) + + key_body = body.nativeObject or body + self.tagMap[key_body.entityToken] = tag + self.tagList.append(key_body.entityToken) + + @logFailure + def handleInputChanged( + self, args: adsk.core.InputChangedEventArgs, globalCommandInputs: adsk.core.CommandInputs + ) -> None: + commandInput = args.input + tagBodySelection: adsk.core.SelectionCommandInput = globalCommandInputs.itemById("tagBodySelect") + + if commandInput.id == "tagAddButton": + self.toggleSelecting(True) + + elif commandInput.id == "tagRemoveButton": + if self.taggingListTable.selectedRow == -1: + app = adsk.core.Application.get() + ui = app.userInterface + ui.messageBox("Please first select a tag to remove.") + return + + selectedRow = self.taggingListTable.selectedRow + if selectedRow == 0: + return + + token_to_remove = self.tagList[selectedRow - 1] + self.taggingListTable.deleteRow(selectedRow) + self.tagMap.pop(token_to_remove) + self.tagList.pop(selectedRow - 1) + + elif commandInput.id == "tagCancelButton": + self.toggleSelecting(False) + + elif tagBodySelection.selectionCount == 1 and self.tagTypeDropdown.selectedItem is not None: + self.addTag(tagBodySelection.selection(0).entity, self.tagTypeDropdown.selectedItem.name) + self.toggleSelecting(False) + + @logFailure + def getTags(self) -> dict[str, str]: + return self.tagMap