diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py index 92e739da77..1eb34e9e52 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -21,7 +21,6 @@ Joint, ModelHierarchy, PhysicalDepth, - PreferredUnits, Wheel, encodeNestedObjects, makeObjectFromJson, @@ -43,10 +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) - preferredUnits: PreferredUnits = field(default=PreferredUnits.IMPERIAL) - - # Always stored in kg regardless of 'preferredUnits' - robotWeight: KG = field(default=0.0) + robotWeight: KG = field(default=KG(0.0)) autoCalcRobotWeight: bool = field(default=False) autoCalcGamepieceWeight: bool = field(default=False) diff --git a/exporter/SynthesisFusionAddin/src/Resources/kg_icon/16x16-normal.png b/exporter/SynthesisFusionAddin/src/Resources/kg_icon/16x16-normal.png deleted file mode 100644 index 86e8fd3cf8..0000000000 Binary files a/exporter/SynthesisFusionAddin/src/Resources/kg_icon/16x16-normal.png and /dev/null differ diff --git a/exporter/SynthesisFusionAddin/src/Resources/lbs_icon/16x16-normal.png b/exporter/SynthesisFusionAddin/src/Resources/lbs_icon/16x16-normal.png deleted file mode 100644 index ced649bb03..0000000000 Binary files a/exporter/SynthesisFusionAddin/src/Resources/lbs_icon/16x16-normal.png and /dev/null differ diff --git a/exporter/SynthesisFusionAddin/src/Types.py b/exporter/SynthesisFusionAddin/src/Types.py index 5e2d7c967f..166bc43976 100644 --- a/exporter/SynthesisFusionAddin/src/Types.py +++ b/exporter/SynthesisFusionAddin/src/Types.py @@ -5,14 +5,24 @@ from enum import Enum, EnumType from typing import Any, TypeAlias, get_args, get_origin +import adsk.fusion + # Not 100% sure what this is for - Brandon JointParentType = Enum("JointParentType", ["ROOT", "END"]) 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"]) ExportLocation = Enum("ExportLocation", ["UPLOAD", "DOWNLOAD"]) +UnitSystem = Enum("UnitSystem", ["METRIC", "IMPERIAL"]) + +FUSION_UNIT_SYSTEM: dict[int, UnitSystem] = { + adsk.fusion.DistanceUnits.MillimeterDistanceUnits: UnitSystem.METRIC, + adsk.fusion.DistanceUnits.CentimeterDistanceUnits: UnitSystem.METRIC, + adsk.fusion.DistanceUnits.MeterDistanceUnits: UnitSystem.METRIC, + adsk.fusion.DistanceUnits.InchDistanceUnits: UnitSystem.IMPERIAL, + adsk.fusion.DistanceUnits.FootDistanceUnits: UnitSystem.IMPERIAL, +} @dataclass @@ -72,18 +82,8 @@ class ModelHierarchy(Enum): SingleMesh = 3 -LBS: TypeAlias = float KG: TypeAlias = float - - -def toLbs(kgs: KG) -> LBS: - return LBS(round(kgs * 2.2062, 2)) - - -def toKg(pounds: LBS) -> KG: - return KG(round(pounds / 2.2062, 2)) - - +LBS: TypeAlias = float PRIMITIVES = (bool, str, int, float, type(None)) diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py index b13fb7ccba..b463302c5b 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -331,12 +331,6 @@ def notify(self, args: adsk.core.CommandEventArgs) -> None: selectedJoints, selectedWheels = jointConfigTab.getSelectedJointsAndWheels() selectedGamepieces = gamepieceConfigTab.getGamepieces() - if generalConfigTab.exportMode == ExportMode.ROBOT: - units = generalConfigTab.selectedUnits - else: - assert generalConfigTab.exportMode == ExportMode.FIELD - units = gamepieceConfigTab.selectedUnits - exporterOptions = ExporterOptions( str(savepath), name, @@ -345,7 +339,6 @@ def notify(self, args: adsk.core.CommandEventArgs) -> None: joints=selectedJoints, wheels=selectedWheels, gamepieces=selectedGamepieces, - preferredUnits=units, robotWeight=generalConfigTab.robotWeight, autoCalcRobotWeight=generalConfigTab.autoCalculateWeight, autoCalcGamepieceWeight=gamepieceConfigTab.autoCalculateWeight, diff --git a/exporter/SynthesisFusionAddin/src/UI/GamepieceConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/GamepieceConfigTab.py index d3e26f1f99..c28f6c60f1 100644 --- a/exporter/SynthesisFusionAddin/src/UI/GamepieceConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/GamepieceConfigTab.py @@ -3,13 +3,13 @@ from src.Logging import logFailure from src.Parser.ExporterOptions import ExporterOptions -from src.Types import Gamepiece, PreferredUnits, toKg, toLbs -from src.UI import IconPaths +from src.Types import Gamepiece, UnitSystem from src.UI.CreateCommandInputsHelper import ( createBooleanInput, createTableInput, createTextBoxInput, ) +from src.Util import convertMassUnitsFrom, convertMassUnitsTo, getFusionUnitSystem class GamepieceConfigTab: @@ -19,7 +19,6 @@ class GamepieceConfigTab: gamepieceTable: adsk.core.TableCommandInput previousAutoCalcWeightCheckboxState: bool previousSelectedUnitDropdownIndex: int - currentUnits: PreferredUnits @logFailure def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: ExporterOptions) -> None: @@ -37,20 +36,6 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: Exp ) self.previousAutoCalcWeightCheckboxState = exporterOptions.autoCalcGamepieceWeight - self.currentUnits = exporterOptions.preferredUnits - imperialUnits = self.currentUnits == PreferredUnits.IMPERIAL - weightUnitTable = gamepieceTabInputs.addDropDownCommandInput( - "gamepieceWeightUnit", "Unit of Mass", 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 - weightUnitTable.listItems.add("‎", imperialUnits, IconPaths.massIcons["LBS"]) - weightUnitTable.listItems.add("‎", not imperialUnits, IconPaths.massIcons["KG"]) - weightUnitTable.tooltip = "Unit of mass" - weightUnitTable.tooltipDescription = "
Configure the unit of mass for for the weight calculation." - self.previousSelectedUnitDropdownIndex = int(not imperialUnits) - self.gamepieceTable = createTableInput( "gamepieceTable", "Gamepiece", @@ -62,8 +47,17 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: Exp self.gamepieceTable.addCommandInput( createTextBoxInput("gamepieceNameHeader", "Name", gamepieceTabInputs, "Name", bold=False), 0, 0 ) + fusUnitSystem = getFusionUnitSystem() self.gamepieceTable.addCommandInput( - createTextBoxInput("gamepieceWeightHeader", "Weight", gamepieceTabInputs, "Weight", bold=False), 0, 1 + createTextBoxInput( + "gamepieceWeightHeader", + "Weight", + gamepieceTabInputs, + f"Weight {'(lbs)' if fusUnitSystem is UnitSystem.IMPERIAL else '(kg)'}", + bold=False, + ), + 0, + 1, ) self.gamepieceTable.addCommandInput( createTextBoxInput( @@ -112,10 +106,6 @@ def isVisible(self, value: bool) -> None: def isActive(self) -> bool: return self.gamepieceConfigTab.isActive or False - @property - def selectedUnits(self) -> PreferredUnits: - return self.currentUnits - @property def autoCalculateWeight(self) -> bool: autoCalcWeightButton: adsk.core.BoolValueCommandInput = self.gamepieceConfigTab.children.itemById( @@ -168,26 +158,13 @@ def addChildOccurrences(childOccurrences: adsk.fusion.OccurrenceList) -> None: frictionCoefficient.valueOne = 0.5 physical = gamepiece.component.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) - if self.currentUnits == PreferredUnits.IMPERIAL: - gamepieceMass = toLbs(physical.mass) - else: - gamepieceMass = round(physical.mass, 2) - + gamepieceMass = round(convertMassUnitsFrom(physical.mass), 2) weight = commandInputs.addValueInput( "gamepieceWeight", "Weight Input", "", adsk.core.ValueInput.createByString(str(gamepieceMass)) ) weight.tooltip = "Weight of field element" weight.isEnabled = not self.previousAutoCalcWeightCheckboxState - weightUnitDropdown: adsk.core.DropDownCommandInput = self.gamepieceConfigTab.children.itemById( - "gamepieceWeightUnit" - ) - if weightUnitDropdown.selectedItem.index == 0: - weight.tooltipDescription = "(in pounds)" - else: - assert weightUnitDropdown.selectedItem.index == 1 - weight.tooltipDescription = "(in kilograms)" - row = self.gamepieceTable.rowCount self.gamepieceTable.addCommandInput(gamepieceName, row, 0) self.gamepieceTable.addCommandInput(weight, row, 1) @@ -222,7 +199,7 @@ def getGamepieces(self) -> list[Gamepiece]: gamepieces: list[Gamepiece] = [] for row in range(1, self.gamepieceTable.rowCount): # Row is 1 indexed gamepieceEntityToken = self.selectedGamepieceList[row - 1].entityToken - gamepieceWeight = self.gamepieceTable.getInputAtPosition(row, 1).value + gamepieceWeight = convertMassUnitsTo(self.gamepieceTable.getInputAtPosition(row, 1).value) gamepieceFrictionCoefficient = self.gamepieceTable.getInputAtPosition(row, 2).valueOne gamepieces.append(Gamepiece(gamepieceEntityToken, gamepieceWeight, gamepieceFrictionCoefficient)) @@ -232,14 +209,6 @@ def reset(self) -> None: self.selectedGamepieceEntityIDs.clear() self.selectedGamepieceList.clear() - @logFailure - def updateWeightTableToUnits(self, units: PreferredUnits) -> None: - assert units in {PreferredUnits.METRIC, PreferredUnits.IMPERIAL} - conversionFunc = toKg if units == PreferredUnits.METRIC else toLbs - for row in range(1, self.gamepieceTable.rowCount): # Row is 1 indexed - weightInput: adsk.core.ValueCommandInput = self.gamepieceTable.getInputAtPosition(row, 1) - weightInput.value = conversionFunc(weightInput.value) - @logFailure def calcGamepieceWeights(self) -> None: for row in range(1, self.gamepieceTable.rowCount): # Row is 1 indexed @@ -247,10 +216,7 @@ def calcGamepieceWeights(self) -> None: physical = self.selectedGamepieceList[row - 1].component.getPhysicalProperties( adsk.fusion.CalculationAccuracy.LowCalculationAccuracy ) - if self.currentUnits == PreferredUnits.IMPERIAL: - weightInput.value = toLbs(physical.mass) - else: - weightInput.value = round(physical.mass, 2) + weightInput.value = round(convertMassUnitsFrom(physical.mass), 2) @logFailure def handleInputChanged( @@ -283,20 +249,6 @@ def handleInputChanged( self.previousAutoCalcWeightCheckboxState = autoCalcWeightButton.value - elif commandInput.id == "gamepieceWeightUnit": - weightUnitDropdown = adsk.core.DropDownCommandInput.cast(commandInput) - if weightUnitDropdown.selectedItem.index == self.previousSelectedUnitDropdownIndex: - return - - if weightUnitDropdown.selectedItem.index == 0: - self.currentUnits = PreferredUnits.IMPERIAL - else: - assert weightUnitDropdown.selectedItem.index == 1 - self.currentUnits = PreferredUnits.METRIC - - self.updateWeightTableToUnits(self.currentUnits) - self.previousSelectedUnitDropdownIndex = weightUnitDropdown.selectedItem.index - elif commandInput.id == "gamepieceAddButton": gamepieceSelection.isVisible = gamepieceSelection.isEnabled = True gamepieceSelection.clearSelection() diff --git a/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py index 673e56054b..977f5d9298 100644 --- a/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py @@ -3,11 +3,16 @@ from src.Logging import logFailure from src.Parser.ExporterOptions import ExporterOptions -from src.Types import KG, ExportLocation, ExportMode, PreferredUnits, toKg, toLbs -from src.UI import IconPaths -from src.UI.CreateCommandInputsHelper import createBooleanInput, createTableInput +from src.Types import KG, ExportLocation, ExportMode, UnitSystem +from src.UI.CreateCommandInputsHelper import createBooleanInput from src.UI.GamepieceConfigTab import GamepieceConfigTab from src.UI.JointConfigTab import JointConfigTab +from src.Util import ( + convertMassUnitsFrom, + convertMassUnitsTo, + designMassCalculation, + getFusionUnitSystem, +) class GeneralConfigTab: @@ -16,7 +21,6 @@ class GeneralConfigTab: previousFrictionOverrideCheckboxState: bool previousSelectedUnitDropdownIndex: int previousSelectedModeDropdownIndex: int - currentUnits: PreferredUnits jointConfigTab: JointConfigTab gamepieceConfigTab: GamepieceConfigTab @@ -52,20 +56,6 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: Exp "
Do you want to upload this mirabuf file to APS, or download it to your local machine?" ) - weightTableInput = createTableInput( - "weightTable", - "Weight Table", - generalTabInputs, - 4, - "2:1:1", - 1, - ) - weightTableInput.tablePresentationStyle = 2 # Transparent background - - weightName = generalTabInputs.addStringValueInput("weightName", "Weight") - weightName.value = "Weight" - weightName.isReadOnly = True - autoCalcWeightButton = createBooleanInput( "autoCalcWeightButton", "Auto Calculate Robot Weight", @@ -75,44 +65,15 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: Exp ) self.previousAutoCalcWeightCheckboxState = exporterOptions.autoCalcRobotWeight - self.currentUnits = exporterOptions.preferredUnits - imperialUnits = self.currentUnits == PreferredUnits.IMPERIAL - if imperialUnits: - # ExporterOptions always contains the metric value - displayWeight = toLbs(exporterOptions.robotWeight) - else: - displayWeight = exporterOptions.robotWeight + displayWeight = convertMassUnitsFrom(exporterOptions.robotWeight) + fusUnitSystem = getFusionUnitSystem() weightInput = generalTabInputs.addValueInput( "weightInput", - "Weight Input", + f"Weight {'(lbs)' if fusUnitSystem is UnitSystem.IMPERIAL else '(kg)'}", "", adsk.core.ValueInput.createByReal(displayWeight), ) - weightInput.tooltip = "Robot weight" - weightInput.tooltipDescription = ( - f"(in {'pounds' if self.currentUnits == PreferredUnits.IMPERIAL else 'kilograms'})" - "
This is the weight of the entire robot assembly." - ) - weightInput.isEnabled = not exporterOptions.autoCalcRobotWeight - - weightUnitDropdown = generalTabInputs.addDropDownCommandInput( - "weightUnitDropdown", - "Weight Unit", - 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 - weightUnitDropdown.listItems.add("‎", imperialUnits, IconPaths.massIcons["LBS"]) - weightUnitDropdown.listItems.add("‎", not imperialUnits, IconPaths.massIcons["KG"]) - weightUnitDropdown.tooltip = "Unit of Mass" - weightUnitDropdown.tooltipDescription = "
Configure the unit of mass for the weight calculation." - self.previousSelectedUnitDropdownIndex = int(not imperialUnits) - - weightTableInput.addCommandInput(weightName, 0, 0) - weightTableInput.addCommandInput(weightInput, 0, 1) - weightTableInput.addCommandInput(weightUnitDropdown, 0, 2) createBooleanInput( "compressOutputButton", @@ -152,7 +113,7 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: Exp if exporterOptions.exportMode == ExportMode.FIELD: autoCalcWeightButton.isVisible = False exportAsPartButton.isVisible = False - weightInput.isVisible = weightTableInput.isVisible = False + weightInput.isVisible = False frictionOverrideButton.isVisible = frictionCoefficient.isVisible = False @property @@ -184,20 +145,10 @@ def exportAsPart(self) -> bool: ) return exportAsPartButton.value or False - @property - def selectedUnits(self) -> PreferredUnits: - return self.currentUnits - @property def robotWeight(self) -> KG: - weightInput: adsk.core.ValueCommandInput = self.generalOptionsTab.children.itemById( - "weightTable" - ).getInputAtPosition(0, 1) - if self.currentUnits == PreferredUnits.METRIC: - return KG(weightInput.value) - else: - assert self.currentUnits == PreferredUnits.IMPERIAL - return toKg(weightInput.value) + weightInput: adsk.core.ValueCommandInput = self.generalOptionsTab.children.itemById("weightInput") + return convertMassUnitsTo(weightInput.value) @property def autoCalculateWeight(self) -> bool: @@ -234,8 +185,7 @@ def frictionOverrideCoeff(self) -> float: @logFailure def handleInputChanged(self, args: adsk.core.InputChangedEventArgs) -> None: autoCalcWeightButton: adsk.core.BoolValueCommandInput = args.inputs.itemById("autoCalcWeightButton") - weightTable: adsk.core.TableCommandInput = args.inputs.itemById("weightTable") - weightInput: adsk.core.ValueCommandInput = weightTable.getInputAtPosition(0, 1) + weightInput: adsk.core.ValueCommandInput = args.inputs.itemById("weightInput") exportAsPartButton: adsk.core.BoolValueCommandInput = args.inputs.itemById("exportAsPartButton") overrideFrictionButton: adsk.core.BoolValueCommandInput = args.inputs.itemById("frictionOverride") frictionSlider: adsk.core.FloatSliderCommandInput = args.inputs.itemById("frictionCoefficient") @@ -251,7 +201,7 @@ def handleInputChanged(self, args: adsk.core.InputChangedEventArgs) -> None: self.gamepieceConfigTab.isVisible = False autoCalcWeightButton.isVisible = True - weightTable.isVisible = True + weightInput.isVisible = True exportAsPartButton.isVisible = True overrideFrictionButton.isVisible = True frictionSlider.isVisible = overrideFrictionButton.value @@ -261,41 +211,19 @@ def handleInputChanged(self, args: adsk.core.InputChangedEventArgs) -> None: self.gamepieceConfigTab.isVisible = True autoCalcWeightButton.isVisible = False - weightTable.isVisible = False + weightInput.isVisible = False exportAsPartButton.isVisible = False overrideFrictionButton.isVisible = frictionSlider.isVisible = False self.previousSelectedModeDropdownIndex = modeDropdown.selectedItem.index - elif commandInput.id == "weightUnitDropdown": - weightUnitDropdown = adsk.core.DropDownCommandInput.cast(commandInput) - if weightUnitDropdown.selectedItem.index == self.previousSelectedUnitDropdownIndex: - return - - if weightUnitDropdown.selectedItem.index == 0: - self.currentUnits = PreferredUnits.IMPERIAL - weightInput.value = toLbs(weightInput.value) - weightInput.tooltipDescription = ( - "(in pounds)
This is the weight of the entire robot assembly." - ) - else: - assert weightUnitDropdown.selectedItem.index == 1 - self.currentUnits = PreferredUnits.METRIC - weightInput.value = toKg(weightInput.value) - weightInput.tooltipDescription = ( - "(in kilograms)
This is the weight of the entire robot assembly." - ) - - self.previousSelectedUnitDropdownIndex = weightUnitDropdown.selectedItem.index - elif commandInput.id == "autoCalcWeightButton": autoCalcWeightButton = adsk.core.BoolValueCommandInput.cast(commandInput) if autoCalcWeightButton.value == self.previousAutoCalcWeightCheckboxState: return if autoCalcWeightButton.value: - robotMass = designMassCalculation() - weightInput.value = robotMass if self.currentUnits is PreferredUnits.METRIC else toLbs(robotMass) + weightInput.value = designMassCalculation() weightInput.isEnabled = False else: weightInput.isEnabled = True @@ -308,22 +236,4 @@ def handleInputChanged(self, args: adsk.core.InputChangedEventArgs) -> None: return frictionSlider.isVisible = frictionOverrideButton.value - self.previousFrictionOverrideCheckboxState = frictionOverrideButton.value - - -# TODO: Perhaps move this into a different module -@logFailure -def designMassCalculation() -> KG: - app = adsk.core.Application.get() - mass = 0.0 - for body in [x for x in app.activeDocument.design.rootComponent.bRepBodies if x.isLightBulbOn]: - physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) - mass += physical.mass - - for occ in [x for x in app.activeDocument.design.rootComponent.allOccurrences if x.isLightBulbOn]: - for body in [x for x in occ.component.bRepBodies if x.isLightBulbOn]: - physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) - mass += physical.mass - - return round(mass, 2) diff --git a/exporter/SynthesisFusionAddin/src/UI/IconPaths.py b/exporter/SynthesisFusionAddin/src/UI/IconPaths.py index 2804af221a..8b377eb659 100644 --- a/exporter/SynthesisFusionAddin/src/UI/IconPaths.py +++ b/exporter/SynthesisFusionAddin/src/UI/IconPaths.py @@ -32,11 +32,6 @@ "remove": resources + os.path.join("MousePreselectIcons", "mouse-remove-icon.png"), } -massIcons = { - "KG": resources + os.path.join("kg_icon"), # resource folder - "LBS": resources + os.path.join("lbs_icon"), # resource folder -} - signalIcons = { "PWM": resources + os.path.join("PWM_icon"), # resource folder "CAN": resources + os.path.join("CAN_icon"), # resource folder diff --git a/exporter/SynthesisFusionAddin/src/Util.py b/exporter/SynthesisFusionAddin/src/Util.py index 9916cbb853..c5f91ef635 100644 --- a/exporter/SynthesisFusionAddin/src/Util.py +++ b/exporter/SynthesisFusionAddin/src/Util.py @@ -1,5 +1,46 @@ import os +import adsk.core +import adsk.fusion + +from src.Types import FUSION_UNIT_SYSTEM, KG, LBS, UnitSystem + + +def getFusionUnitSystem() -> UnitSystem: + fusDesign = adsk.fusion.Design.cast(adsk.core.Application.get().activeProduct) + return FUSION_UNIT_SYSTEM.get(fusDesign.fusionUnitsManager.distanceDisplayUnits, UnitSystem.METRIC) + + +def convertMassUnitsFrom(input: KG | LBS) -> KG | LBS: + """Converts stored Synthesis mass units into user selected Fusion units.""" + unitManager = adsk.fusion.Design.cast(adsk.core.Application.get().activeProduct).fusionUnitsManager + toString = "kg" if getFusionUnitSystem() is UnitSystem.METRIC else "lbmass" + return unitManager.convert(input, "kg", toString) or 0.0 + + +def convertMassUnitsTo(input: KG | LBS) -> KG | LBS: + """Converts user selected Fusion mass units into Synthesis units.""" + unitManager = adsk.fusion.Design.cast(adsk.core.Application.get().activeProduct).fusionUnitsManager + fromString = "kg" if getFusionUnitSystem() is UnitSystem.METRIC else "lbmass" + return unitManager.convert(input, fromString, "kg") or 0.0 + + +def designMassCalculation() -> KG | LBS: + """Calculates and returns the total mass of the active design in Fusion units.""" + app = adsk.core.Application.get() + mass = 0.0 + for body in [x for x in app.activeDocument.design.rootComponent.bRepBodies if x.isLightBulbOn]: + physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) + mass += physical.mass + + for occ in [x for x in app.activeDocument.design.rootComponent.allOccurrences if x.isLightBulbOn]: + for body in [x for x in occ.component.bRepBodies if x.isLightBulbOn]: + physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) + mass += physical.mass + + # Internally, Fusion always uses metric units, same as Synthesis + return round(convertMassUnitsFrom(mass), 2) + def makeDirectories(directory: str) -> str: """Ensures than an input directory exists and attempts to create it if it doesn't.""" diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 2a289c6d12..52475c3f80 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -36,7 +36,6 @@ import ThemeEditorModal from "@/modals/configuring/theme-editor/ThemeEditorModal import MatchModeModal from "@/modals/spawning/MatchModeModal" import RobotSwitchPanel from "@/panels/RobotSwitchPanel" import SpawnLocationsPanel from "@/panels/SpawnLocationPanel" -import ConfigureSubsystemsPanel from "@/ui/panels/configuring/ConfigureSubsystemsPanel.tsx" import ScoreboardPanel from "@/panels/information/ScoreboardPanel" import DriverStationPanel from "@/panels/simulation/DriverStationPanel" import PokerPanel from "@/panels/PokerPanel.tsx" @@ -46,7 +45,6 @@ import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel.tsx" import Skybox from "./ui/components/Skybox.tsx" import ChooseInputSchemePanel from "./ui/panels/configuring/ChooseInputSchemePanel.tsx" import ProgressNotifications from "./ui/components/ProgressNotification.tsx" -import ConfigureRobotBrainPanel from "./ui/panels/configuring/ConfigureRobotBrainPanel.tsx" import SceneOverlay from "./ui/components/SceneOverlay.tsx" import WPILibWSWorker from "@/systems/simulation/wpilib_brain/WPILibWSWorker.ts?worker" @@ -228,17 +226,10 @@ const initialPanels: ReactElement[] = [ , , , - , , , , , - , ] export default Synthesis diff --git a/fission/src/mirabuf/MirabufLoader.ts b/fission/src/mirabuf/MirabufLoader.ts index 1355d9e38c..89b61c82e6 100644 --- a/fission/src/mirabuf/MirabufLoader.ts +++ b/fission/src/mirabuf/MirabufLoader.ts @@ -10,8 +10,9 @@ export type MirabufCacheID = string export interface MirabufCacheInfo { id: MirabufCacheID - cacheKey: string miraType: MiraType + cacheKey: string + buffer?: ArrayBuffer name?: string thumbnailStorageID?: string } @@ -29,10 +30,10 @@ const root = await navigator.storage.getDirectory() const robotFolderHandle = await root.getDirectoryHandle(robotsDirName, { create: true }) const fieldFolderHandle = await root.getDirectoryHandle(fieldsDirName, { create: true }) -export const backUpRobots: Map = new Map() -export const backUpFields: Map = new Map() +export let backUpRobots: MapCache = {} +export let backUpFields: MapCache = {} -const canOPFS = await (async () => { +export const canOPFS = await (async () => { try { if (robotFolderHandle.name == robotsDirName) { robotFolderHandle.entries @@ -52,6 +53,21 @@ const canOPFS = await (async () => { } } catch (e) { console.log(`No access to OPFS`) + + // Copy-pasted from RemoveAll() + for await (const key of robotFolderHandle.keys()) { + robotFolderHandle.removeEntry(key) + } + for await (const key of fieldFolderHandle.keys()) { + fieldFolderHandle.removeEntry(key) + } + + window.localStorage.setItem(robotsDirName, "{}") + window.localStorage.setItem(fieldsDirName, "{}") + + backUpRobots = {} + backUpFields = {} + return false } })() @@ -75,11 +91,10 @@ class MirabufCachingService { */ public static GetCacheMap(miraType: MiraType): MapCache { if ( - (window.localStorage.getItem(MIRABUF_LOCALSTORAGE_GENERATION_KEY) ?? "") == MIRABUF_LOCALSTORAGE_GENERATION + (window.localStorage.getItem(MIRABUF_LOCALSTORAGE_GENERATION_KEY) ?? "") != MIRABUF_LOCALSTORAGE_GENERATION ) { window.localStorage.setItem(MIRABUF_LOCALSTORAGE_GENERATION_KEY, MIRABUF_LOCALSTORAGE_GENERATION) - window.localStorage.setItem(robotsDirName, "{}") - window.localStorage.setItem(fieldsDirName, "{}") + this.RemoveAll() return {} } @@ -188,16 +203,19 @@ class MirabufCachingService { try { const map: MapCache = this.GetCacheMap(miraType) const id = map[key].id + const _buffer = miraType == MiraType.ROBOT ? backUpRobots[id].buffer : backUpFields[id].buffer const _name = map[key].name const _thumbnailStorageID = map[key].thumbnailStorageID const info: MirabufCacheInfo = { id: id, cacheKey: key, miraType: miraType, + buffer: _buffer, name: name ?? _name, thumbnailStorageID: thumbnailStorageID ?? _thumbnailStorageID, } map[key] = info + miraType == MiraType.ROBOT ? (backUpRobots[id] = info) : (backUpFields[id] = info) window.localStorage.setItem(miraType == MiraType.ROBOT ? robotsDirName : fieldsDirName, JSON.stringify(map)) return true } catch (e) { @@ -243,7 +261,7 @@ class MirabufCachingService { // Get buffer from hashMap. If not in hashMap, check OPFS. Otherwise, buff is undefined const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields const buff = - cache.get(id) ?? + cache[id]?.buffer ?? (await (async () => { const fileHandle = canOPFS ? await (miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle).getFileHandle(id, { @@ -299,7 +317,7 @@ class MirabufCachingService { const backUpCache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields if (backUpCache) { - backUpCache.delete(id) + delete backUpCache[id] } World.AnalyticsSystem?.Event("Cache Remove", { @@ -318,18 +336,20 @@ class MirabufCachingService { * Removes all Mirabuf files from the caching services. Mostly for debugging purposes. */ public static async RemoveAll() { - for await (const key of robotFolderHandle.keys()) { - robotFolderHandle.removeEntry(key) - } - for await (const key of fieldFolderHandle.keys()) { - fieldFolderHandle.removeEntry(key) + if (canOPFS) { + for await (const key of robotFolderHandle.keys()) { + robotFolderHandle.removeEntry(key) + } + for await (const key of fieldFolderHandle.keys()) { + fieldFolderHandle.removeEntry(key) + } } - window.localStorage.removeItem(robotsDirName) - window.localStorage.removeItem(fieldsDirName) + window.localStorage.setItem(robotsDirName, "{}") + window.localStorage.setItem(fieldsDirName, "{}") - backUpRobots.clear() - backUpFields.clear() + backUpRobots = {} + backUpFields = {} } // Optional name for when assembly is being decoded anyway like in CacheAndGetLocal() @@ -339,10 +359,10 @@ class MirabufCachingService { miraType?: MiraType, name?: string ): Promise { - const backupID = Date.now().toString() try { + const backupID = Date.now().toString() if (!miraType) { - console.log("Double loading") + console.debug("Double loading") miraType = this.AssemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD } @@ -350,8 +370,8 @@ class MirabufCachingService { const map: MapCache = this.GetCacheMap(miraType) const info: MirabufCacheInfo = { id: backupID, - cacheKey: key, miraType: miraType, + cacheKey: key, name: name, } map[key] = info @@ -377,7 +397,14 @@ class MirabufCachingService { // Store in hash const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields - cache.set(backupID, miraBuff) + const mapInfo: MirabufCacheInfo = { + id: backupID, + miraType: miraType, + cacheKey: key, + buffer: miraBuff, + name: name, + } + cache[backupID] = mapInfo return info } catch (e) { diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index b37283c705..c834e30cec 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -147,11 +147,6 @@ const MainHUD: React.FC = () => { icon={SynthesisIcons.MagnifyingGlass} onClick={() => openModal("view")} /> */} - openPanel("subsystem-config")} - /> = ({ value, index, onSelected, onDel {/* Indentation before the name */} {/* Label for joint index and type (grey if child) */} + = ({ value, index, onSelected, onDel sx={{ borderColor: "#888888" }} id={`select-button-${value.name}`} /> + {value.tooltipText && CustomTooltip(value.tooltipText)} {/** Delete button only if onDelete is defined */} {onDelete && includeDelete && ( <> diff --git a/fission/src/ui/components/StyledComponents.tsx b/fission/src/ui/components/StyledComponents.tsx index e5de2cfba3..66e404d71f 100644 --- a/fission/src/ui/components/StyledComponents.tsx +++ b/fission/src/ui/components/StyledComponents.tsx @@ -64,6 +64,7 @@ export class SynthesisIcons { public static EditLarge = () public static LeftArrowLarge = () public static BugLarge = () + public static XmarkLarge = () public static OpenHudIcon = ( = ({ modalId }) => { }} /> ("SubsystemGravity")} onClick={checked => { setSubsystemGravity(checked) }} + tooltipText="Allows you to set a target torque or force for subsystems and joints. If not properly configured, joints may not be able to resist gravity or may not behave as intended." /> { - /* Adds the joints that the wheels are associated with */ - if (behavior instanceof ArcadeDriveBehavior) { - behavior.wheels.forEach(wheel => { - const assoc = World.PhysicsSystem.GetBodyAssociation( - wheel.constraint.GetVehicleBody().GetID() - ) as RigidNodeAssociate - - if (!assoc || assoc.sceneObject !== robot) { - return - } - - output.push( - - - Wheel Node {elementKey} - - - {assoc.rigidNodeId} - - - ) - elementKey++ - }) - output.push() - } else if (behavior instanceof GenericArmBehavior) { - /* Adds the joints that the arm is associated with */ - // Get the rigid node associates for the two bodies - const assoc1 = World.PhysicsSystem.GetBodyAssociation( - behavior.hingeDriver.constraint.GetBody1().GetID() - ) as RigidNodeAssociate - const assoc2 = World.PhysicsSystem.GetBodyAssociation( - behavior.hingeDriver.constraint.GetBody2().GetID() - ) as RigidNodeAssociate - - if (!assoc1 || assoc1.sceneObject !== robot || !assoc2 || assoc2.sceneObject !== robot) { - return - } - - output.push( - - - Arm Nodes - - - {assoc1.rigidNodeId + " " + assoc2.rigidNodeId} - - - ) - elementKey++ - } else if (behavior instanceof GenericElevatorBehavior) { - /* Adds the joints that the elevator is associated with */ - // Get the rigid node associates for the two bodies - const assoc1 = World.PhysicsSystem.GetBodyAssociation( - behavior.sliderDriver.constraint.GetBody1().GetID() - ) as RigidNodeAssociate - const assoc2 = World.PhysicsSystem.GetBodyAssociation( - behavior.sliderDriver.constraint.GetBody2().GetID() - ) as RigidNodeAssociate - - if (!assoc1 || assoc1.sceneObject !== robot || !assoc2 || assoc2.sceneObject !== robot) { - return - } - - output.push( - - - Elevator Nodes - - - {assoc1.rigidNodeId + " " + assoc2.rigidNodeId} - - - ) - elementKey++ - } - }) - - return output -} - -const ConfigureRobotBrainPanel: React.FC = ({ panelId, openLocation, sidePadding }) => { - const [selectedRobot, setSelectedRobot] = useState(undefined) - const [viewType, setViewType] = useState(ConfigureRobotBrainTypes.SYNTHESIS) - const robots = useMemo(() => { - const assemblies = [...World.SceneRenderer.sceneObjects.values()].filter(x => { - if (x instanceof MirabufSceneObject) { - return x.miraType === MiraType.ROBOT - } - return false - }) as MirabufSceneObject[] - return assemblies - }, []) - - return ( - } - panelId={panelId} - openLocation={openLocation} - sidePadding={sidePadding} - onAccept={() => {}} - onCancel={() => {}} - > - {selectedRobot?.ejectorPreferences == undefined ? ( - <> - - {/** Scroll view for selecting a robot to configure */} -
- {robots.map(mirabufSceneObject => { - return ( - - ) - })} -
- {/* TODO: remove the accept button on this version */} - - ) : ( - <> -
- v != null && setViewType(v)} - sx={{ - alignSelf: "center", - }} - > - SynthesisBrain - WIPLIBBrain - - {viewType === ConfigureRobotBrainTypes.SYNTHESIS ? ( - <> - - Behaviors - - - {GetJoints(selectedRobot)} - - ) : ( - <> - - Example WIPLIB Brain - - - - - Example 2 - - - - - Example 3 - - - - - Example 4 - - - - )} -
- - )} -
- ) -} - -export default ConfigureRobotBrainPanel diff --git a/fission/src/ui/panels/configuring/ConfigureSubsystemsPanel.tsx b/fission/src/ui/panels/configuring/ConfigureSubsystemsPanel.tsx deleted file mode 100644 index cf343f012f..0000000000 --- a/fission/src/ui/panels/configuring/ConfigureSubsystemsPanel.tsx +++ /dev/null @@ -1,279 +0,0 @@ -import { MiraType } from "@/mirabuf/MirabufLoader" -import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" -import PreferencesSystem from "@/systems/preferences/PreferencesSystem" -import { RobotPreferences } from "@/systems/preferences/PreferenceTypes" -import Driver from "@/systems/simulation/driver/Driver" -import HingeDriver from "@/systems/simulation/driver/HingeDriver" -import SliderDriver from "@/systems/simulation/driver/SliderDriver" -import WheelDriver from "@/systems/simulation/driver/WheelDriver" -import World from "@/systems/World" -import Button from "@/ui/components/Button" -import Label, { LabelSize } from "@/ui/components/Label" -import Panel, { PanelPropsImpl } from "@/ui/components/Panel" -import ScrollView from "@/ui/components/ScrollView" -import Slider from "@/ui/components/Slider" -import Stack, { StackDirection } from "@/ui/components/Stack" -import { SectionDivider } from "@/ui/components/StyledComponents" -import { Box } from "@mui/material" -import { useCallback, useMemo, useState } from "react" -import { FaGear } from "react-icons/fa6" - -type SubsystemRowProps = { - robot: MirabufSceneObject - driver: Driver -} - -const SubsystemRow: React.FC = ({ robot, driver }) => { - const driverSwitch = (driver: Driver, slider: unknown, hinge: unknown, drivetrain: unknown) => { - switch (driver.constructor) { - case SliderDriver: - return slider - case HingeDriver: - return hinge - case WheelDriver: - return drivetrain - default: - return drivetrain - } - } - - const [velocity, setVelocity] = useState( - ((driver as SliderDriver) || (driver as HingeDriver) || (driver as WheelDriver)).maxVelocity - ) - const [force, setForce] = useState( - ((driver as SliderDriver) || (driver as HingeDriver) || (driver as WheelDriver)).maxForce - ) - - const onChange = useCallback( - (vel: number, force: number) => { - if (driver instanceof WheelDriver) { - const wheelDrivers = robot?.mechanism - ? World.SimulationSystem.GetSimulationLayer(robot.mechanism)?.drivers.filter( - x => x instanceof WheelDriver - ) - : undefined - wheelDrivers?.forEach(x => { - x.maxVelocity = vel - x.maxForce = force - }) - - // Preferences - PreferencesSystem.getRobotPreferences(robot.assemblyName).driveVelocity = vel - PreferencesSystem.getRobotPreferences(robot.assemblyName).driveAcceleration = force - } else { - // Preferences - if (driver.info && driver.info.name) { - const removedMotor = PreferencesSystem.getRobotPreferences(robot.assemblyName).motors - ? PreferencesSystem.getRobotPreferences(robot.assemblyName).motors.filter(x => { - if (x.name) return x.name != driver.info?.name - return false - }) - : [] - - removedMotor.push({ - name: driver.info?.name ?? "", - maxVelocity: vel, - maxForce: force, - }) - - PreferencesSystem.getRobotPreferences(robot.assemblyName).motors = removedMotor - } - - // eslint-disable-next-line no-extra-semi - ;((driver as SliderDriver) || (driver as HingeDriver)).maxVelocity = vel - ;((driver as SliderDriver) || (driver as HingeDriver)).maxForce = force - } - - PreferencesSystem.savePreferences() - }, - [driver, robot.mechanism, robot.assemblyName] - ) - - return ( - <> - - - - { - setVelocity(_velocity as number) - onChange(_velocity as number, force) - }} - step={0.01} - /> - {PreferencesSystem.getGlobalPreference("SubsystemGravity") || driver instanceof WheelDriver ? ( - { - setForce(_force as number) - onChange(velocity, _force as number) - }} - step={0.01} - /> - ) : driver instanceof HingeDriver ? ( - - ) : ( - - )} - - - - - ) -} - -const ConfigureSubsystemsPanel: React.FC = ({ panelId, openLocation, sidePadding }) => { - const [selectedRobot, setSelectedRobot] = useState(undefined) - const [origPref, setOrigPref] = useState(undefined) - - const robots = useMemo(() => { - const assemblies = [...World.SceneRenderer.sceneObjects.values()].filter(x => { - if (x instanceof MirabufSceneObject) { - return x.miraType === MiraType.ROBOT - } - return false - }) as MirabufSceneObject[] - return assemblies - }, []) - - const drivers = useMemo(() => { - return selectedRobot?.mechanism - ? World.SimulationSystem.GetSimulationLayer(selectedRobot.mechanism)?.drivers - : undefined - }, [selectedRobot]) - - // Gets motors in preferences for ease of saving into origPrefs which can be used to revert on Cancel() - function saveOrigMotors(robot: MirabufSceneObject) { - drivers?.forEach(driver => { - if (driver.info && driver.info.name && !(driver instanceof WheelDriver)) { - const motors = PreferencesSystem.getRobotPreferences(robot.assemblyName).motors - const removedMotor = motors.filter(x => { - if (x.name) return x.name != driver.info?.name - return false - }) - - if (removedMotor.length == drivers.length) { - removedMotor.push({ - name: driver.info?.name ?? "", - maxVelocity: ((driver as SliderDriver) || (driver as HingeDriver)).maxVelocity, - maxForce: ((driver as SliderDriver) || (driver as HingeDriver)).maxForce, - }) - PreferencesSystem.getRobotPreferences(robot.assemblyName).motors = removedMotor - } - } - }) - PreferencesSystem.savePreferences() - setOrigPref({ ...PreferencesSystem.getRobotPreferences(robot.assemblyName) }) // clone - } - - function Cancel() { - if (selectedRobot && origPref) { - drivers?.forEach(driver => { - if (driver instanceof WheelDriver) { - driver.maxVelocity = origPref.driveVelocity - driver.maxForce = origPref.driveAcceleration - } else { - if (driver.info && driver.info.name) { - const motor = origPref.motors.filter(x => { - if (x.name) return x.name == driver.info?.name - return false - })[0] - if (motor) { - // This line is a separate variable to get ES Lint and Prettier to agree on formatting the semicolon below - const forcePref = PreferencesSystem.getGlobalPreference("SubsystemGravity") - ? motor.maxForce - : driver instanceof SliderDriver - ? 500 - : 100 - ;((driver as SliderDriver) || (driver as HingeDriver)).maxVelocity = motor.maxVelocity - ;((driver as SliderDriver) || (driver as HingeDriver)).maxForce = forcePref - } - } - } - }) - PreferencesSystem.setRobotPreferences(selectedRobot.assemblyName, origPref) - } - PreferencesSystem.savePreferences() - } - - return ( - } - panelId={panelId} - openLocation={openLocation} - sidePadding={sidePadding} - onAccept={() => { - PreferencesSystem.savePreferences() - }} - onCancel={Cancel} - acceptEnabled={true} - > - {selectedRobot?.ejectorPreferences == undefined ? ( - <> - - {/** Scroll view for selecting a robot to configure */} -
- {robots.map(mirabufSceneObject => { - return ( - - ) - })} -
- - ) : ( - <> - {drivers ? ( - - {/** Drivetrain row. Then other SliderDrivers and HingeDrivers */} - { - return selectedRobot - })()} - driver={(() => { - return drivers.filter(x => x instanceof WheelDriver)[0] - })()} - /> - {drivers - .filter(x => x instanceof SliderDriver || x instanceof HingeDriver) - .map((driver: Driver, i: number) => ( - { - return selectedRobot - })()} - driver={(() => { - return driver - })()} - /> - ))} - - ) : ( - - )} - - )} -
- ) -} - -export default ConfigureSubsystemsPanel diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx index aa5d5b7f2a..9dbdb7e076 100644 --- a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx @@ -6,10 +6,7 @@ import Panel, { PanelPropsImpl } from "@/ui/components/Panel" import SelectMenu, { SelectMenuOption } from "@/ui/components/SelectMenu" import { ToggleButton, ToggleButtonGroup } from "@/ui/components/ToggleButtonGroup" import { useEffect, useMemo, useReducer, useState } from "react" -import ConfigureGamepiecePickupInterface from "./interfaces/ConfigureGamepiecePickupInterface" -import ConfigureShotTrajectoryInterface from "./interfaces/ConfigureShotTrajectoryInterface" import ConfigureScoringZonesInterface from "./interfaces/scoring/ConfigureScoringZonesInterface" -import SequentialBehaviorsInterface from "./interfaces/SequentialBehaviorsInterface" import ChangeInputsInterface from "./interfaces/inputs/ConfigureInputsInterface" import InputSystem from "@/systems/input/InputSystem" import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" @@ -18,12 +15,17 @@ import Button from "@/ui/components/Button" import { setSelectedBrainIndexGlobal } from "../ChooseInputSchemePanel" import ConfigureSchemeInterface from "./interfaces/inputs/ConfigureSchemeInterface" import { SynthesisIcons } from "@/ui/components/StyledComponents" +import ConfigureSubsystemsInterface from "./interfaces/ConfigureSubsystemsInterface" +import SequentialBehaviorsInterface from "./interfaces/SequentialBehaviorsInterface" +import ConfigureShotTrajectoryInterface from "./interfaces/ConfigureShotTrajectoryInterface" +import ConfigureGamepiecePickupInterface from "./interfaces/ConfigureGamepiecePickupInterface" enum ConfigMode { - INTAKE, + SUBSYSTEMS, EJECTOR, - MOTORS, + INTAKE, CONTROLS, + SEQUENTIAL, SCORING_ZONES, } @@ -117,8 +119,8 @@ const AssemblySelection: React.FC = ({ configuratio class ConfigModeSelectionOption extends SelectMenuOption { configMode: ConfigMode - constructor(name: string, configMode: ConfigMode) { - super(name) + constructor(name: string, configMode: ConfigMode, tooltip?: string) { + super(name, tooltip) this.configMode = configMode } } @@ -126,7 +128,16 @@ class ConfigModeSelectionOption extends SelectMenuOption { const robotModes = [ new ConfigModeSelectionOption("Intake", ConfigMode.INTAKE), new ConfigModeSelectionOption("Ejector", ConfigMode.EJECTOR), - new ConfigModeSelectionOption("Sequential Joints", ConfigMode.MOTORS), + new ConfigModeSelectionOption( + "Configure Joints", + ConfigMode.SUBSYSTEMS, + "Set the velocities, torques, and accelerations of your robot's motors." + ), + new ConfigModeSelectionOption( + "Sequence Joints", + ConfigMode.SEQUENTIAL, + "Set which joints follow each other. For example, the second stage of an elevator could follow the first, moving in unison with it." + ), new ConfigModeSelectionOption("Controls", ConfigMode.CONTROLS), ] const fieldModes = [new ConfigModeSelectionOption("Scoring Zones", ConfigMode.SCORING_ZONES)] @@ -162,8 +173,8 @@ const ConfigInterface: React.FC = ({ configMode, assembly, return case ConfigMode.EJECTOR: return - case ConfigMode.MOTORS: - return + case ConfigMode.SUBSYSTEMS: + return case ConfigMode.CONTROLS: { const brainIndex = (assembly.brain as SynthesisBrain).brainIndex const scheme = InputSystem.brainIndexSchemeMap.get(brainIndex) @@ -181,6 +192,8 @@ const ConfigInterface: React.FC = ({ configMode, assembly, ) } + case ConfigMode.SEQUENTIAL: + return case ConfigMode.SCORING_ZONES: { const zones = assembly.fieldPreferences?.scoringZones if (zones == undefined) { diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/ConfigureSubsystemsInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/ConfigureSubsystemsInterface.tsx new file mode 100644 index 0000000000..d2e98e94d2 --- /dev/null +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/ConfigureSubsystemsInterface.tsx @@ -0,0 +1,113 @@ +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import SelectMenu, { SelectMenuOption } from "@/ui/components/SelectMenu" +import React, { useMemo, useState } from "react" +import { ConfigurationSavedEvent } from "../ConfigurePanel" +import World from "@/systems/World" +import SliderDriver from "@/systems/simulation/driver/SliderDriver" +import HingeDriver from "@/systems/simulation/driver/HingeDriver" +import Driver from "@/systems/simulation/driver/Driver" +import WheelDriver from "@/systems/simulation/driver/WheelDriver" +import SubsystemRowInterface from "./SubsystemRowInterface" +import PreferencesSystem from "@/systems/preferences/PreferencesSystem" +import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" +import SequenceableBehavior from "@/systems/simulation/behavior/synthesis/SequenceableBehavior" +import { DefaultSequentialConfig, SequentialBehaviorPreferences } from "@/systems/preferences/PreferenceTypes" +import GenericArmBehavior from "@/systems/simulation/behavior/synthesis/GenericArmBehavior" + +class ConfigModeSelectionOption extends SelectMenuOption { + driver: Driver + sequential?: SequentialBehaviorPreferences + + constructor(name: string, driver: Driver, sequential?: SequentialBehaviorPreferences) { + super(name) + this.driver = driver + this.sequential = sequential + } +} + +interface ConfigSubsystemProps { + selectedRobot: MirabufSceneObject +} + +interface ConfigInterfaceProps { + configModeOption: ConfigModeSelectionOption + selectedRobot: MirabufSceneObject + saveBehaviors: () => void +} + +const ConfigInterface: React.FC = ({ configModeOption, selectedRobot, saveBehaviors }) => { + return ( + + ) +} + +const ConfigureSubsystemsInterface: React.FC = ({ selectedRobot }) => { + const [selectedConfigMode, setSelectedConfigMode] = useState(undefined) + + const behaviors = useMemo( + () => + PreferencesSystem.getRobotPreferences(selectedRobot.assemblyName)?.sequentialConfig ?? + (selectedRobot.brain as SynthesisBrain).behaviors + .filter(b => b instanceof SequenceableBehavior) + .map(b => DefaultSequentialConfig(b.jointIndex, b instanceof GenericArmBehavior ? "Arm" : "Elevator")), + [] + ) + + const drivers = useMemo(() => { + return World.SimulationSystem.GetSimulationLayer(selectedRobot.mechanism)?.drivers + }, [selectedRobot]) + + const getSubsystemOptions = () => { + if (drivers == undefined) return [] + const options = [new ConfigModeSelectionOption("Drivetrain", drivers.filter(x => x instanceof WheelDriver)[0])] + + let jointIndex = 0 + + drivers + .filter(x => x instanceof HingeDriver) + .forEach(d => { + options.push(new ConfigModeSelectionOption(d.info?.name ?? "UnnamedMotor", d, behaviors[jointIndex])) + jointIndex++ + }) + + drivers + .filter(x => x instanceof SliderDriver) + .forEach(d => { + options.push(new ConfigModeSelectionOption(d.info?.name ?? "UnnamedMotor", d, behaviors[jointIndex])) + jointIndex++ + }) + + return options + } + + return ( + <> + { + if (val != undefined) new ConfigurationSavedEvent() + setSelectedConfigMode(val as ConfigModeSelectionOption) + }} + defaultHeaderText="Select a Subsystem" + indentation={2} + /> + {selectedConfigMode != undefined && ( + { + PreferencesSystem.getRobotPreferences(selectedRobot.assemblyName).sequentialConfig = behaviors + PreferencesSystem.savePreferences() + }} + /> + )} + + ) +} + +export default ConfigureSubsystemsInterface diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/SequentialBehaviorsInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/SequentialBehaviorsInterface.tsx index 6a8523d465..bec98015dd 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/SequentialBehaviorsInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/SequentialBehaviorsInterface.tsx @@ -1,20 +1,15 @@ import React, { useCallback, useEffect, useReducer, useState } from "react" import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" import Label, { LabelSize } from "@/ui/components/Label" -import { FaArrowRightArrowLeft, FaXmark } from "react-icons/fa6" -import { Box, Button as MUIButton, styled, alpha, Icon } from "@mui/material" +import { Box, Button as MUIButton, styled, alpha } from "@mui/material" import Button, { ButtonSize } from "@/ui/components/Button" import { DefaultSequentialConfig, SequentialBehaviorPreferences } from "@/systems/preferences/PreferenceTypes" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import SequenceableBehavior from "@/systems/simulation/behavior/synthesis/SequenceableBehavior" -import Checkbox from "@/ui/components/Checkbox" import GenericArmBehavior from "@/systems/simulation/behavior/synthesis/GenericArmBehavior" import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" import { ConfigurationSavedEvent } from "../ConfigurePanel" -import { SectionLabel, Spacer } from "@/ui/components/StyledComponents" - -const UnselectParentIcon = -const InvertIcon = +import { SectionLabel, Spacer, SynthesisIcons } from "@/ui/components/StyledComponents" /** Grey label for a child behavior name */ const ChildLabelStyled = styled(Label)({ @@ -113,19 +108,8 @@ const BehaviorCard: React.FC = ({ /> {/* Spacer between the CustomButton and invert button */} - - - - {/* Invert joint icon & checkbox */} - {InvertIcon} - (behavior.inverted = val)} - hideLabel={true} - /> - - + {/* + */} {/* Button to set the parent of this behavior */}