diff --git a/.github/workflows/FusionTyping.yml b/.github/workflows/FusionTyping.yml
new file mode 100644
index 0000000000..13de49fc2f
--- /dev/null
+++ b/.github/workflows/FusionTyping.yml
@@ -0,0 +1,23 @@
+name: Fusion - mypy Typing Validation
+
+on:
+ workflow_dispatch: {}
+ push:
+ branches: [ prod, dev ]
+ pull_request:
+ branches: [ prod, dev ]
+
+jobs:
+ mypy:
+ name: Run mypy
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./exporter/SynthesisFusionAddin
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+ - run: pip install -r requirements-mypy.txt
+ - run: mypy
diff --git a/.gitignore b/.gitignore
index a7ea447430..a57a98b5ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
.vs/
.vscode/
-/build/
+build/
+dist/
*.log
.DS_Store
+*.pkg
+*.exe
diff --git a/exporter/SynthesisFusionAddin/Synthesis.manifest b/exporter/SynthesisFusionAddin/Synthesis.manifest
index f8e96ae366..47434ab6a2 100644
--- a/exporter/SynthesisFusionAddin/Synthesis.manifest
+++ b/exporter/SynthesisFusionAddin/Synthesis.manifest
@@ -6,7 +6,7 @@
"description": {
"": "Synthesis Exporter"
},
- "version": "1.0.0",
+ "version": "2.0.0",
"runOnStartup": true,
"supportedOS": "windows|mac",
"editEnabled": true
diff --git a/exporter/SynthesisFusionAddin/Synthesis.py b/exporter/SynthesisFusionAddin/Synthesis.py
index c5befeddde..ee630a12ca 100644
--- a/exporter/SynthesisFusionAddin/Synthesis.py
+++ b/exporter/SynthesisFusionAddin/Synthesis.py
@@ -1,5 +1,6 @@
import os
import sys
+from typing import Any
import adsk.core
@@ -43,7 +44,7 @@
@logFailure
-def run(_):
+def run(_context: dict[str, Any]) -> None:
"""## Entry point to application from Fusion.
Arguments:
@@ -63,7 +64,7 @@ def run(_):
@logFailure
-def stop(_):
+def stop(_context: dict[str, Any]) -> None:
"""## Fusion exit point - deconstructs buttons and handlers
Arguments:
diff --git a/exporter/SynthesisFusionAddin/mypy.ini b/exporter/SynthesisFusionAddin/mypy.ini
new file mode 100644
index 0000000000..057a1f78b5
--- /dev/null
+++ b/exporter/SynthesisFusionAddin/mypy.ini
@@ -0,0 +1,14 @@
+[mypy]
+files = Synthesis.py, src
+warn_unused_configs = True
+check_untyped_defs = True
+warn_unreachable = True
+warn_redundant_casts = True
+warn_unused_ignores = True
+warn_no_return = True
+warn_return_any = True
+strict = True
+ignore_missing_imports = True
+follow_imports = skip
+disallow_subclassing_any = False
+disable_error_code = no-untyped-call
diff --git a/exporter/SynthesisFusionAddin/proto/build.bat b/exporter/SynthesisFusionAddin/proto/build.bat
index 08b776b634..2d44d07102 100644
--- a/exporter/SynthesisFusionAddin/proto/build.bat
+++ b/exporter/SynthesisFusionAddin/proto/build.bat
@@ -2,5 +2,5 @@
md .\proto_out\
@RD /S /Q "./proto_out/__pycache__"
@echo on
-protoc -I=../../../mirabuf --python_out=./proto_out ../../../mirabuf/*.proto
+protoc -I=../../../mirabuf --python_out=./proto_out --mypy_out=./proto_out ../../../mirabuf/*.proto
@echo off
diff --git a/exporter/SynthesisFusionAddin/proto/build.sh b/exporter/SynthesisFusionAddin/proto/build.sh
index bf5fdeda7e..b4c48da239 100755
--- a/exporter/SynthesisFusionAddin/proto/build.sh
+++ b/exporter/SynthesisFusionAddin/proto/build.sh
@@ -1,4 +1,4 @@
rm -rf -v ./proto_out
mkdir ./proto_out
git submodule update --init --recursive
-protoc -I=../../../mirabuf --python_out=./proto_out ../../../mirabuf/*.proto
\ No newline at end of file
+protoc -I=../../../mirabuf --python_out=./proto_out --mypy_out=./proto_out ../../../mirabuf/*.proto
diff --git a/exporter/SynthesisFusionAddin/requirements.txt b/exporter/SynthesisFusionAddin/requirements-dev.txt
similarity index 73%
rename from exporter/SynthesisFusionAddin/requirements.txt
rename to exporter/SynthesisFusionAddin/requirements-dev.txt
index 75a3dbe98a..89846d2e08 100644
--- a/exporter/SynthesisFusionAddin/requirements.txt
+++ b/exporter/SynthesisFusionAddin/requirements-dev.txt
@@ -1,2 +1,3 @@
black
+isort
pyminifier
diff --git a/exporter/SynthesisFusionAddin/requirements-mypy.txt b/exporter/SynthesisFusionAddin/requirements-mypy.txt
new file mode 100644
index 0000000000..ef8b8dff85
--- /dev/null
+++ b/exporter/SynthesisFusionAddin/requirements-mypy.txt
@@ -0,0 +1,3 @@
+mypy
+types-protobuf
+types-requests
diff --git a/exporter/SynthesisFusionAddin/src/APS/APS.py b/exporter/SynthesisFusionAddin/src/APS/APS.py
index a6f4f34a5b..65ab01e4f7 100644
--- a/exporter/SynthesisFusionAddin/src/APS/APS.py
+++ b/exporter/SynthesisFusionAddin/src/APS/APS.py
@@ -1,3 +1,4 @@
+import http.client
import json
import os
import pathlib
@@ -52,21 +53,21 @@ def getAPSAuth() -> APSAuth | None:
return APS_AUTH
-def _res_json(res):
- return json.loads(res.read().decode(res.info().get_param("charset") or "utf-8"))
+def _res_json(res: http.client.HTTPResponse) -> dict[str, Any]:
+ return dict(json.loads(res.read().decode(str(res.info().get_param("charset")) or "utf-8")))
def getCodeChallenge() -> str | None:
endpoint = "https://synthesis.autodesk.com/api/aps/challenge/"
- res = urllib.request.urlopen(endpoint)
+ res: http.client.HTTPResponse = urllib.request.urlopen(endpoint)
data = _res_json(res)
- return data["challenge"]
+ return str(data["challenge"])
def getAuth() -> APSAuth | None:
global APS_AUTH
if APS_AUTH is not None:
- return APS_AUTH
+ return APS_AUTH # type: ignore[unreachable]
currTime = time.time()
if os.path.exists(auth_path):
@@ -86,7 +87,7 @@ def getAuth() -> APSAuth | None:
return APS_AUTH
-def convertAuthToken(code: str):
+def convertAuthToken(code: str) -> None:
global APS_AUTH
authUrl = f'https://synthesis.autodesk.com/api/aps/code/?code={code}&redirect_uri={urllib.parse.quote_plus("https://synthesis.autodesk.com/api/aps/exporter/")}'
res = urllib.request.urlopen(authUrl)
@@ -106,14 +107,14 @@ def convertAuthToken(code: str):
_ = loadUserInfo()
-def removeAuth():
+def removeAuth() -> None:
global APS_AUTH, APS_USER_INFO
APS_AUTH = None
APS_USER_INFO = None
pathlib.Path.unlink(pathlib.Path(auth_path))
-def refreshAuthToken():
+def refreshAuthToken() -> None:
global APS_AUTH
if APS_AUTH is None or APS_AUTH.refresh_token is None:
raise Exception("No refresh token found.")
@@ -178,6 +179,8 @@ def loadUserInfo() -> APSUserInfo | None:
removeAuth()
logger.error(f"User Info Error:\n{e.code} - {e.reason}")
gm.ui.messageBox("Please sign in again.")
+ finally:
+ return None
def getUserInfo() -> APSUserInfo | None:
@@ -259,20 +262,30 @@ def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_content
global APS_AUTH
if APS_AUTH is None:
gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR")
+ return None
+
auth = APS_AUTH.access_token
# Get token from APS API later
new_folder_id = get_item_id(auth, project_id, folder_id, "MirabufDir", "folders")
if new_folder_id is None:
- folder_id = create_folder(auth, project_id, folder_id, "MirabufDir")
+ created_folder_id = create_folder(auth, project_id, folder_id, "MirabufDir")
else:
- folder_id = new_folder_id
- (lineage_id, file_id, file_version) = get_file_id(auth, project_id, folder_id, file_name)
+ created_folder_id = new_folder_id
+
+ if created_folder_id is None:
+ return None
+
+ file_id_data = get_file_id(auth, project_id, created_folder_id, file_name)
+ if file_id_data is None:
+ return None
+
+ (lineage_id, file_id, file_version) = file_id_data
"""
Create APS Storage Location
"""
- object_id = create_storage_location(auth, project_id, folder_id, file_name)
+ object_id = create_storage_location(auth, project_id, created_folder_id, file_name)
if object_id is None:
gm.ui.messageBox("UPLOAD ERROR", "Object id is none; check create storage location")
return None
@@ -297,10 +310,10 @@ def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_content
return None
if file_id != "":
update_file_version(
- auth, project_id, folder_id, lineage_id, file_id, file_name, file_contents, file_version, object_id
+ auth, project_id, created_folder_id, lineage_id, file_id, file_name, file_contents, file_version, object_id
)
else:
- _lineage_info = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name)
+ _lineage_info = create_first_file_version(auth, str(object_id), project_id, str(created_folder_id), file_name)
return ""
@@ -376,7 +389,7 @@ def get_item_id(auth: str, project_id: str, parent_folder_id: str, folder_name:
return ""
for item in data:
if item["type"] == item_type and item["attributes"]["name"] == folder_name:
- return item["id"]
+ return str(item["id"])
return None
@@ -500,7 +513,7 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> t
elif not file_res.ok:
gm.ui.messageBox(f"UPLOAD ERROR: {file_res.text}", "Failed to get file")
return None
- file_json: list[dict[str, Any]] = file_res.json()
+ file_json: dict[str, Any] = file_res.json()
if len(file_json["data"]) == 0:
return ("", "", "")
id: str = str(file_json["data"][0]["id"])
diff --git a/exporter/SynthesisFusionAddin/src/Dependencies.py b/exporter/SynthesisFusionAddin/src/Dependencies.py
index 3bb7261501..284a8ad942 100644
--- a/exporter/SynthesisFusionAddin/src/Dependencies.py
+++ b/exporter/SynthesisFusionAddin/src/Dependencies.py
@@ -21,10 +21,14 @@
@logFailure
-def getInternalFusionPythonInstillationFolder() -> str:
+def getInternalFusionPythonInstillationFolder() -> str | os.PathLike[str]:
# Thank you Kris Kaplan
# Find the folder location where the Autodesk python instillation keeps the 'os' standard library module.
- pythonStandardLibraryModulePath = importlib.machinery.PathFinder.find_spec("os", sys.path).origin
+ pythonOSModulePath = importlib.machinery.PathFinder.find_spec("os", sys.path)
+ if pythonOSModulePath:
+ pythonStandardLibraryModulePath = pythonOSModulePath.origin or "ERROR"
+ else:
+ raise BaseException("Could not locate spec 'os'")
# Depending on platform, adjust to folder to where the python executable binaries are stored.
if SYSTEM == "Windows":
@@ -36,10 +40,10 @@ def getInternalFusionPythonInstillationFolder() -> str:
return folder
-def executeCommand(*args: str) -> subprocess.CompletedProcess:
+def executeCommand(*args: str) -> subprocess.CompletedProcess[str]:
logger.debug(f"Running Command -> {' '.join(args)}")
try:
- result: subprocess.CompletedProcess = subprocess.run(
+ result: subprocess.CompletedProcess[str] = subprocess.run(
args, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True
)
logger.debug(f"Command Output:\n{result.stdout}")
diff --git a/exporter/SynthesisFusionAddin/src/GlobalManager.py b/exporter/SynthesisFusionAddin/src/GlobalManager.py
index 6841a58750..bc9e23e6d3 100644
--- a/exporter/SynthesisFusionAddin/src/GlobalManager.py
+++ b/exporter/SynthesisFusionAddin/src/GlobalManager.py
@@ -1,62 +1,53 @@
""" Initializes the global variables that are set in the run method to reduce hanging commands. """
+from typing import Any
+
import adsk.core
import adsk.fusion
-class GlobalManager(object):
- """Global Manager instance"""
-
- class __GlobalManager:
- def __init__(self):
- self.app = adsk.core.Application.get()
-
- if self.app:
- self.ui = self.app.userInterface
-
- self.connected = False
- """ Is unity currently connected """
-
- self.uniqueIds = []
- """ Collection of unique ID values to not overlap """
-
- self.elements = []
- """ Unique constructed buttons to delete """
-
- self.palettes = []
- """ Unique constructed palettes to delete """
+class GlobalManager:
+ def __init__(self) -> None:
+ self.app = adsk.core.Application.get()
- self.handlers = []
- """ Object to store all event handlers to custom events like saving. """
+ if self.app:
+ self.ui = self.app.userInterface
- self.tabs = []
- """ Set of Tab objects to keep track of. """
+ self.connected = False
+ """ Is unity currently connected """
- self.queue = []
- """ This will eventually implement the Python SimpleQueue synchronized workflow
- - this is the list of objects being sent
- """
+ self.uniqueIds: list[str] = [] # type of HButton
+ """ Collection of unique ID values to not overlap """
- self.files = []
+ self.elements: list[Any] = []
+ """ Unique constructed buttons to delete """
- def __str__(self):
- return "GlobalManager"
+ # Transition: AARD-1765
+ # Will likely be removed later as this is no longer used. Avoiding adding typing for now.
+ self.palettes = [] # type: ignore
+ """ Unique constructed palettes to delete """
- def clear(self):
- for attr, value in self.__dict__.items():
- if isinstance(value, list):
- setattr(self, attr, [])
+ self.handlers: list[adsk.core.EventHandler] = []
+ """ Object to store all event handlers to custom events like saving. """
- instance = None
+ self.tabs: list[adsk.core.ToolbarPanel] = []
+ """ Set of Tab objects to keep track of. """
- def __new__(cls):
- if not GlobalManager.instance:
- GlobalManager.instance = GlobalManager.__GlobalManager()
+ # Transition: AARD-1765
+ # Will likely be removed later as this is no longer used. Avoiding adding typing for now.
+ self.queue = [] # type: ignore
+ """ This will eventually implement the Python SimpleQueue synchronized workflow
+ - this is the list of objects being sent
+ """
- return GlobalManager.instance
+ # Transition: AARD-1765
+ # Will likely be removed later as this is no longer used. Avoiding adding typing for now.
+ self.files = [] # type: ignore
- def __getattr__(self, name):
- return getattr(self.instance, name)
+ def __str__(self) -> str:
+ return "GlobalManager"
- def __setattr__(self, name):
- return setattr(self.instance, name)
+ def clear(self) -> None:
+ for attr, value in self.__dict__.items():
+ if isinstance(value, list):
+ setattr(self, attr, [])
diff --git a/exporter/SynthesisFusionAddin/src/Logging.py b/exporter/SynthesisFusionAddin/src/Logging.py
index e5f352f480..3b2bedb68c 100644
--- a/exporter/SynthesisFusionAddin/src/Logging.py
+++ b/exporter/SynthesisFusionAddin/src/Logging.py
@@ -7,19 +7,19 @@
import time
import traceback
from datetime import date, datetime
-from typing import cast
+from typing import Any, Callable, cast
import adsk.core
-from src import INTERNAL_ID
-from src.UI.OsHelper import getOSPath
+from src import INTERNAL_ID, IS_RELEASE, SUPPORT_PATH
+from src.Util import makeDirectories
MAX_LOG_FILES_TO_KEEP = 10
TIMING_LEVEL = 25
class SynthesisLogger(logging.Logger):
- def timing(self, msg: str, *args: any, **kwargs: any) -> None:
+ def timing(self, msg: str, *args: Any, **kwargs: Any) -> None:
return self.log(TIMING_LEVEL, msg, *args, **kwargs)
def cleanupHandlers(self) -> None:
@@ -30,14 +30,17 @@ def cleanupHandlers(self) -> None:
def setupLogger() -> SynthesisLogger:
now = datetime.now().strftime("%H-%M-%S")
today = date.today()
- logFileFolder = getOSPath(f"{pathlib.Path(__file__).parent.parent}", "logs")
- logFiles = [os.path.join(logFileFolder, file) for file in os.listdir(logFileFolder) if file.endswith(".log")]
- logFiles.sort()
- if len(logFiles) >= MAX_LOG_FILES_TO_KEEP:
- for file in logFiles[: len(logFiles) - MAX_LOG_FILES_TO_KEEP]:
- os.remove(file)
-
- logFileName = f"{logFileFolder}{getOSPath(f'{INTERNAL_ID}-{today}-{now}.log')}"
+ if IS_RELEASE:
+ logFileFolder = makeDirectories(os.path.join(SUPPORT_PATH, "Logs"))
+ else:
+ logFileFolder = makeDirectories(os.path.join(f"{pathlib.Path(__file__).parent.parent}", "logs"))
+ logFiles = [os.path.join(logFileFolder, file) for file in os.listdir(logFileFolder) if file.endswith(".log")]
+ logFiles.sort()
+ if len(logFiles) >= MAX_LOG_FILES_TO_KEEP:
+ for file in logFiles[: len(logFiles) - MAX_LOG_FILES_TO_KEEP]:
+ os.remove(file)
+
+ logFileName = os.path.join(logFileFolder, f"{today}-{now}.log")
logHandler = logging.handlers.WatchedFileHandler(logFileName, mode="w")
logHandler.setFormatter(logging.Formatter("%(name)s - %(levelname)s - %(message)s"))
@@ -46,27 +49,28 @@ def setupLogger() -> SynthesisLogger:
logger = getLogger(INTERNAL_ID)
logger.setLevel(10) # Debug
logger.addHandler(logHandler)
- return cast(SynthesisLogger, logger)
+ return logger
def getLogger(name: str | None = None) -> SynthesisLogger:
if not name:
# Inspect the caller stack to automatically get the module from which the function is being called from.
- name = f"{INTERNAL_ID}.{'.'.join(inspect.getmodule(inspect.stack()[1][0]).__name__.split('.')[1:])}"
+ pyModule = inspect.getmodule(inspect.stack()[1][0])
+ name = f"{INTERNAL_ID}.{'.'.join(pyModule.__name__.split('.')[1:])}" if pyModule else INTERNAL_ID
return cast(SynthesisLogger, logging.getLogger(name))
# Log function failure decorator.
-def logFailure(func: callable = None, /, *, messageBox: bool = False) -> callable:
- def wrap(func: callable) -> callable:
+def logFailure(func: Callable[..., Any] | None = None, /, *, messageBox: bool = False) -> Callable[..., Any]:
+ def wrap(func: Callable[..., Any]) -> Callable[..., Any]:
@functools.wraps(func)
- def wrapper(*args: any, **kwargs: any) -> any:
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
try:
return func(*args, **kwargs)
except BaseException:
excType, excValue, excTrace = sys.exc_info()
- tb = traceback.TracebackException(excType, excValue, excTrace)
+ tb = traceback.TracebackException(excType or BaseException, excValue or BaseException(), excTrace)
formattedTb = "".join(list(tb.format())[2:]) # Remove the wrapper func from the traceback.
clsName = ""
if args and hasattr(args[0], "__class__"):
@@ -88,8 +92,8 @@ def wrapper(*args: any, **kwargs: any) -> any:
# Time function decorator.
-def timed(func: callable) -> callable:
- def wrapper(*args: any, **kwargs: any) -> any:
+def timed(func: Callable[..., Any]) -> Callable[..., Any]:
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
startTime = time.perf_counter()
result = func(*args, **kwargs)
endTime = time.perf_counter()
diff --git a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py
index 69e9bbef5d..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,
@@ -36,17 +35,14 @@ class ExporterOptions:
fileLocation: str | None = field(
default=(os.getenv("HOME") if platform.system() == "Windows" else os.path.expanduser("~"))
)
- name: str = field(default=None)
- version: str = field(default=None)
+ name: str | None = field(default=None)
+ version: str | None = field(default=None)
materials: int = field(default=0)
exportMode: ExportMode = field(default=ExportMode.ROBOT)
- wheels: list[Wheel] = field(default=None)
- joints: list[Joint] = field(default=None)
- gamepieces: list[Gamepiece] = field(default=None)
- preferredUnits: PreferredUnits = field(default=PreferredUnits.IMPERIAL)
-
- # Always stored in kg regardless of 'preferredUnits'
- robotWeight: KG = field(default=0.0)
+ wheels: list[Wheel] = field(default_factory=list)
+ joints: list[Joint] = field(default_factory=list)
+ gamepieces: list[Gamepiece] = field(default_factory=list)
+ 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 6190d55a99..dd8726ad82 100644
--- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py
+++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py
@@ -45,7 +45,7 @@ def _MapAllComponents(
else:
partDefinition.dynamic = True
- def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody):
+ def processBody(body: adsk.fusion.BRepBody | adsk.fusion.MeshBody) -> None:
if progressDialog.wasCancelled():
raise RuntimeError("User canceled export")
if body.isLightBulbOn:
@@ -77,7 +77,7 @@ def _ParseComponentRoot(
progressDialog: PDMessage,
options: ExporterOptions,
partsData: assembly_pb2.Parts,
- material_map: dict,
+ material_map: dict[str, material_pb2.Appearance],
node: types_pb2.Node,
) -> None:
mapConstant = guid_component(component)
@@ -108,7 +108,7 @@ def __parseChildOccurrence(
progressDialog: PDMessage,
options: ExporterOptions,
partsData: assembly_pb2.Parts,
- material_map: dict,
+ material_map: dict[str, material_pb2.Appearance],
node: types_pb2.Node,
) -> None:
if occurrence.isLightBulbOn is False:
@@ -172,10 +172,10 @@ def __parseChildOccurrence(
# saw online someone used this to get the correct context but oh boy does it look pricey
# I think if I can make all parts relative to a parent it should return that parents transform maybe
# TESTED AND VERIFIED - but unoptimized
-def GetMatrixWorld(occurrence):
- matrix = occurrence.transform
+def GetMatrixWorld(occurrence: adsk.fusion.Occurrence) -> adsk.core.Matrix3D:
+ matrix = occurrence.transform2
while occurrence.assemblyContext:
- matrix.transformBy(occurrence.assemblyContext.transform)
+ matrix.transformBy(occurrence.assemblyContext.transform2)
occurrence = occurrence.assemblyContext
return matrix
@@ -185,7 +185,7 @@ def _ParseBRep(
body: adsk.fusion.BRepBody,
options: ExporterOptions,
trimesh: assembly_pb2.TriangleMesh,
-) -> any:
+) -> None:
meshManager = body.meshManager
calc = meshManager.createMeshCalculator()
calc.setQuality(options.visualQuality)
@@ -207,7 +207,7 @@ def _ParseMesh(
meshBody: adsk.fusion.MeshBody,
options: ExporterOptions,
trimesh: assembly_pb2.TriangleMesh,
-) -> any:
+) -> None:
mesh = meshBody.displayMesh
fill_info(trimesh, meshBody)
diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py
index 26dae4f2a4..0b5be182c2 100644
--- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py
+++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py
@@ -1,5 +1,5 @@
import enum
-from typing import Union
+from typing import Any, Iterator, cast
import adsk.core
import adsk.fusion
@@ -18,12 +18,12 @@
# this is more of a tree - todo rewrite
class GraphNode:
- def __init__(self, data: any):
+ def __init__(self, data: Any) -> None:
self.data = data
self.previous = None
- self.edges = list()
+ self.edges: list[GraphEdge] = list()
- def iter(self, filter_relationship=[]):
+ def iter(self, filter_relationship: list[enum.Enum] = []) -> Iterator["GraphNode"]:
"""Generator for Node Iterator that does not have the given relationship
Args:
@@ -37,19 +37,35 @@ def iter(self, filter_relationship=[]):
if edge.relationship not in filter_relationship:
yield from edge.node.iter(filter_relationship=filter_relationship)
- def __iter__(self):
+ def __iter__(self) -> Iterator["GraphNode"]:
for edge in self.edges:
yield edge.node
- def allChildren(self):
+ def allChildren(self) -> list["GraphNode"]:
nodes = [self]
for edge in self.edges:
- nodes.extend(edge.node.allNodes())
+ nodes.extend(edge.node.allChildren())
return nodes
+class RelationshipBase(enum.Enum): ...
+
+
+class OccurrenceRelationship(RelationshipBase):
+ TRANSFORM = 1 # As in hierarchy parenting
+ CONNECTION = 2 # As in a rigid joint or other designator
+ GROUP = 3 # As in a Rigid Grouping
+ NEXT = 4 # As in next_joint in list
+ END = 5 # Orphaned child relationship
+
+
+class JointRelationship(RelationshipBase):
+ GROUND = 1 # This currently has no bearing
+ ROTATIONAL = 2 # This currently has no bearing
+
+
class GraphEdge:
- def __init__(self, relationship: enum.Enum, node: GraphNode):
+ def __init__(self, relationship: RelationshipBase | None, node: GraphNode) -> None:
"""A GraphEdge representing a edge in the GraphNode
Args:
@@ -59,10 +75,15 @@ def __init__(self, relationship: enum.Enum, node: GraphNode):
self.relationship = relationship
self.node = node
- def print(self):
- print(f"Edge Containing {self.relationship.name} -> {self.node}")
+ def print(self) -> None:
+ if self.relationship is None:
+ name = "None"
+ else:
+ name = self.relationship.name
- def __iter__(self):
+ print(f"Edge Containing {name} -> {self.node}")
+
+ def __iter__(self) -> Iterator["GraphEdge"]:
"""Iterator for Edges within this edge
Yields:
@@ -71,34 +92,21 @@ def __iter__(self):
return (edge for edge in self.node.edges)
-class OccurrenceRelationship(enum.Enum):
- TRANSFORM = 1 # As in hierarchy parenting
- CONNECTION = 2 # As in a rigid joint or other designator
- GROUP = 3 # As in a Rigid Grouping
- NEXT = 4 # As in next_joint in list
- END = 5 # Orphaned child relationship
-
-
-class JointRelationship(enum.Enum):
- GROUND = 1 # This currently has no bearing
- ROTATIONAL = 2 # This currently has no bearing
-
-
# ______________________ INDIVIDUAL JOINT CHAINS ____________________________
class DynamicOccurrenceNode(GraphNode):
- def __init__(self, occurrence: adsk.fusion.Occurrence, isGround=False, previous=None):
+ def __init__(self, occurrence: adsk.fusion.Occurrence, isGround: bool = False, previous: GraphNode | None = None):
super().__init__(occurrence)
self.isGround = isGround
self.name = occurrence.name
- def print(self):
+ def print(self) -> None:
print(f"\n\t-------{self.data.name}-------")
for edge in self.edges:
edge.print()
- def getConnectedAxis(self) -> list:
+ def getConnectedAxis(self) -> list[Any]:
"""Gets all Axis with the NEXT relationship
Returns:
@@ -109,10 +117,10 @@ def getConnectedAxis(self) -> list:
if edge.relationship == OccurrenceRelationship.NEXT:
nextItems.append(edge.node.data)
else:
- nextItems.extend(edge.node.getConnectedAxis())
+ nextItems.extend(cast(DynamicOccurrenceNode, edge.node).getConnectedAxis())
return nextItems
- def getConnectedAxisTokens(self) -> list:
+ def getConnectedAxisTokens(self) -> list[str]:
"""Gets all Axis with the NEXT relationship
Returns:
@@ -123,18 +131,20 @@ def getConnectedAxisTokens(self) -> list:
if edge.relationship == OccurrenceRelationship.NEXT:
nextItems.append(edge.node.data.entityToken)
else:
- nextItems.extend(edge.node.getConnectedAxisTokens())
+ nextItems.extend(cast(DynamicOccurrenceNode, edge.node).getConnectedAxisTokens())
return nextItems
class DynamicEdge(GraphEdge):
- def __init__(self, relationship: OccurrenceRelationship, node: DynamicOccurrenceNode):
- super().__init__(relationship, node)
-
# should print all in this class
- def print(self):
- print(f"\t\t - {self.relationship.name} -> {self.node.data.name}")
- self.node.print()
+ def print(self) -> None:
+ if self.relationship is None:
+ name = "None"
+ else:
+ name = self.relationship.name
+
+ print(f"\t\t - {name} -> {self.node.data.name}")
+ cast(DynamicOccurrenceNode, self.node).print()
# ______________________ ENTIRE SIMULATION STRUCTURE _______________________
@@ -143,10 +153,10 @@ def print(self):
class SimulationNode(GraphNode):
def __init__(
self,
- dynamicJoint: DynamicOccurrenceNode,
+ dynamicJoint: DynamicOccurrenceNode | None,
joint: adsk.fusion.Joint,
- grounded=False,
- ):
+ grounded: bool = False,
+ ) -> None:
super().__init__(dynamicJoint)
self.joint = joint
self.grounded = grounded
@@ -156,30 +166,30 @@ def __init__(
else:
self.name = self.joint.name
- def print(self):
+ def print(self) -> None:
print(f"Simulation Node for joint : {self.name} ")
- def printLink(self):
+ def printLink(self) -> None:
if self.grounded:
print(f"GROUND -- {self.data.data.name}")
else:
print(f"--> {self.data.data.name}")
for edge in self.edges:
- edge.node.printLink()
+ cast(SimulationNode, edge.node).printLink()
-class SimulationEdge(GraphEdge):
- def __init__(self, relationship: JointRelationship, node: SimulationNode):
- super().__init__(relationship, node)
+class SimulationEdge(GraphEdge): ...
# ______________________________ PARSER ___________________________________
class JointParser:
+ grounded: adsk.fusion.Occurrence
+
@logFailure
- def __init__(self, design):
+ def __init__(self, design: adsk.fusion.Design) -> None:
# Create hierarchy with just joint assembly
# - Assembly
# - Grounded
@@ -211,15 +221,16 @@ def __init__(self, design):
gm.ui.messageBox("There is not currently a Grounded Component in the assembly, stopping kinematic export.")
raise RuntimeWarning("There is no grounded component")
- self.currentTraversal = dict()
- self.groundedConnections = []
+ self.currentTraversal: dict[str, DynamicOccurrenceNode | bool] = dict()
+ self.groundedConnections: list[adsk.fusion.Occurrence] = []
# populate the rigidJoints connected to a given occurrence
- self.rigidJoints = dict()
+ # Transition: AARD-1765
+ # self.rigidJoints = dict()
# populate all joints
- self.dynamicJoints = dict()
+ self.dynamicJoints: dict[str, adsk.fusion.Joint] = dict()
- self.simulationNodesRef = dict()
+ self.simulationNodesRef: dict[str, SimulationNode] = dict()
# TODO: need to look through every single joint and find the starting point that is connected to ground
# Next add that occurrence to the graph and then traverse down that path etc
@@ -243,13 +254,13 @@ def __init__(self, design):
# self.groundSimNode.printLink()
@logFailure
- def __getAllJoints(self):
+ def __getAllJoints(self) -> None:
for joint in list(self.design.rootComponent.allJoints) + list(self.design.rootComponent.allAsBuiltJoints):
if joint and joint.occurrenceOne and joint.occurrenceTwo:
occurrenceOne = joint.occurrenceOne
occurrenceTwo = joint.occurrenceTwo
else:
- return None
+ return
if occurrenceOne is None:
try:
@@ -286,19 +297,19 @@ def __getAllJoints(self):
logger.error(
f"Occurrences that connect joints could not be found\n\t1: {occurrenceOne}\n\t2: {occurrenceTwo}"
)
- return None
+ return
else:
if oneEntityToken == self.grounded.entityToken:
self.groundedConnections.append(occurrenceTwo)
elif twoEntityToken == self.grounded.entityToken:
self.groundedConnections.append(occurrenceOne)
- def _linkAllAxis(self):
+ def _linkAllAxis(self) -> None:
# looks through each simulation nood starting with ground and orders them using edges
# self.groundSimNode is ground
self._recurseLink(self.groundSimNode)
- def _recurseLink(self, simNode: SimulationNode):
+ def _recurseLink(self, simNode: SimulationNode) -> None:
connectedAxisNodes = [
self.simulationNodesRef.get(componentKeys, None) for componentKeys in simNode.data.getConnectedAxisTokens()
]
@@ -309,7 +320,7 @@ def _recurseLink(self, simNode: SimulationNode):
simNode.edges.append(edge)
self._recurseLink(connectedAxis)
- def _lookForGroundedJoints(self):
+ def _lookForGroundedJoints(self) -> None:
grounded_token = self.grounded.entityToken
rootDynamicJoint = self.groundSimNode.data
@@ -322,7 +333,7 @@ def _lookForGroundedJoints(self):
is_ground=False,
)
- def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint):
+ def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint) -> None:
occ = self.design.findEntityByToken(occ_token)[0]
if occ is None:
@@ -339,21 +350,21 @@ def _populateAxis(self, occ_token: str, joint: adsk.fusion.Joint):
def _populateNode(
self,
occ: adsk.fusion.Occurrence,
- prev: DynamicOccurrenceNode,
- relationship: OccurrenceRelationship,
- is_ground=False,
- ):
+ prev: DynamicOccurrenceNode | None,
+ relationship: OccurrenceRelationship | None,
+ is_ground: bool = False,
+ ) -> DynamicOccurrenceNode | None:
if occ.isGrounded and not is_ground:
- return
+ return None
elif (relationship == OccurrenceRelationship.NEXT) and (prev is not None):
node = DynamicOccurrenceNode(occ)
edge = DynamicEdge(relationship, node)
prev.edges.append(edge)
- return
+ return None
elif ((occ.entityToken in self.dynamicJoints.keys()) and (prev is not None)) or self.currentTraversal.get(
occ.entityToken
) is not None:
- return
+ return None
node = DynamicOccurrenceNode(occ)
@@ -363,6 +374,7 @@ def _populateNode(
self._populateNode(occurrence, node, OccurrenceRelationship.TRANSFORM, is_ground=is_ground)
# if not is_ground: # THIS IS A BUG - OCCURRENCE ACCESS VIOLATION
+ # this is the current reason for wrapping in try except pass
try:
for joint in occ.joints:
if joint and joint.occurrenceOne and joint.occurrenceTwo:
@@ -391,7 +403,7 @@ def _populateNode(
else:
continue
except:
- pass # This is to temporarily bypass the bug
+ pass
if prev is not None:
edge = DynamicEdge(relationship, node)
@@ -403,7 +415,7 @@ def _populateNode(
def searchForGrounded(
occ: adsk.fusion.Occurrence,
-) -> Union[adsk.fusion.Occurrence, None]:
+) -> adsk.fusion.Occurrence | None:
"""Search for a grounded component or occurrence in the assembly
Args:
@@ -442,7 +454,7 @@ def BuildJointPartHierarchy(
joints: joint_pb2.Joints,
options: ExporterOptions,
progressDialog: PDMessage,
-):
+) -> None:
try:
progressDialog.currentMessage = f"Constructing Simulation Hierarchy"
progressDialog.update()
@@ -466,10 +478,10 @@ def BuildJointPartHierarchy(
raise RuntimeError("User canceled export")
except Warning:
- return False
+ pass
-def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDialog):
+def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDialog: PDMessage) -> None:
if progressDialog.wasCancelled():
raise RuntimeError("User canceled export")
@@ -494,15 +506,15 @@ def populateJoint(simNode: SimulationNode, joints: joint_pb2.Joints, progressDia
# next in line to be populated
for edge in simNode.edges:
- populateJoint(edge.node, joints, progressDialog)
+ populateJoint(cast(SimulationNode, edge.node), joints, progressDialog)
def createTreeParts(
dynNode: DynamicOccurrenceNode,
- relationship: OccurrenceRelationship,
+ relationship: RelationshipBase | None,
node: types_pb2.Node,
- progressDialog,
-):
+ progressDialog: PDMessage,
+) -> None:
if progressDialog.wasCancelled():
raise RuntimeError("User canceled export")
@@ -531,5 +543,5 @@ def createTreeParts(
# recurse and add all children connections
for edge in dynNode.edges:
child_node = types_pb2.Node()
- createTreeParts(edge.node, edge.relationship, child_node, progressDialog)
+ createTreeParts(cast(DynamicOccurrenceNode, edge.node), edge.relationship, child_node, progressDialog)
node.children.append(child_node)
diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py
index 7a3e0da3b9..71c01326dd 100644
--- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py
+++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py
@@ -24,7 +24,7 @@
import traceback
import uuid
-from typing import Union
+from typing import Any, Callable, Union
import adsk.core
import adsk.fusion
@@ -38,7 +38,7 @@
guid_occurrence,
)
from src.Proto import assembly_pb2, joint_pb2, signal_pb2, types_pb2
-from src.Types import JointParentType, SignalType
+from src.Types import Joint, JointParentType, SignalType, Wheel
logger = getLogger()
@@ -71,7 +71,7 @@ def populateJoints(
progressDialog: PDMessage,
options: ExporterOptions,
assembly: assembly_pb2.Assembly,
-):
+) -> None:
fill_info(joints, None)
# This is for creating all of the Joint Definition objects
@@ -152,7 +152,7 @@ def populateJoints(
continue
-def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint):
+def _addJoint(joint: adsk.fusion.Joint, joint_definition: joint_pb2.Joint) -> None:
fill_info(joint_definition, joint)
jointPivotTranslation = _jointOrigin(joint)
@@ -177,7 +177,7 @@ def _addJointInstance(
joint_definition: joint_pb2.Joint,
signals: signal_pb2.Signals,
options: ExporterOptions,
-):
+) -> None:
fill_info(joint_instance, joint)
# because there is only one and we are using the token - should be the same
joint_instance.joint_reference = joint_instance.info.GUID
@@ -232,7 +232,7 @@ def _addJointInstance(
joint_instance.signal_reference = ""
-def _addRigidGroup(joint: adsk.fusion.Joint, assembly: assembly_pb2.Assembly):
+def _addRigidGroup(joint: adsk.fusion.Joint, assembly: assembly_pb2.Assembly) -> None:
if joint.jointMotion.jointType != 0 or not (
joint.occurrenceOne.isLightBulbOn and joint.occurrenceTwo.isLightBulbOn
):
@@ -249,22 +249,22 @@ def _motionFromJoint(fusionMotionDefinition: adsk.fusion.JointMotion, proto_join
# if fusionJoint.geometryOrOriginOne.objectType == "adsk::fusion::JointGeometry"
# create the DOF depending on what kind of information the joint has
- fillJointMotionFuncSwitcher = {
- 0: noop, # this should be ignored
+ fillJointMotionFuncSwitcher: dict[int, Callable[..., None]] = {
+ 0: notImplementedPlaceholder, # this should be ignored
1: fillRevoluteJointMotion,
2: fillSliderJointMotion,
- 3: noop, # TODO: Implement - Ball Joint at least
- 4: noop, # TODO: Implement
- 5: noop, # TODO: Implement
- 6: noop, # TODO: Implement
+ 3: notImplementedPlaceholder, # TODO: Implement - Ball Joint at least
+ 4: notImplementedPlaceholder, # TODO: Implement
+ 5: notImplementedPlaceholder, # TODO: Implement
+ 6: notImplementedPlaceholder, # TODO: Implement
}
- fillJointMotionFunc = fillJointMotionFuncSwitcher.get(fusionMotionDefinition.jointType, lambda: None)
+ fillJointMotionFunc = fillJointMotionFuncSwitcher.get(fusionMotionDefinition.jointType, notImplementedPlaceholder)
fillJointMotionFunc(fusionMotionDefinition, proto_joint)
-def fillRevoluteJointMotion(revoluteMotion: adsk.fusion.RevoluteJointMotion, proto_joint: joint_pb2.Joint):
+def fillRevoluteJointMotion(revoluteMotion: adsk.fusion.RevoluteJointMotion, proto_joint: joint_pb2.Joint) -> None:
"""#### Fill Protobuf revolute joint motion data
Args:
@@ -338,9 +338,7 @@ def fillSliderJointMotion(sliderMotion: adsk.fusion.SliderJointMotion, proto_joi
dof.value = sliderMotion.slideValue
-def noop(*argv):
- """Easy way to keep track of no-op code that required function pointers"""
- pass
+def notImplementedPlaceholder(*argv: Any) -> None: ...
def _searchForGrounded(
@@ -431,9 +429,9 @@ def _jointOrigin(fusionJoint: Union[adsk.fusion.Joint, adsk.fusion.AsBuiltJoint]
def createJointGraph(
- supplied_joints: list,
- wheels: list,
- joint_tree: types_pb2.GraphContainer,
+ suppliedJoints: list[Joint],
+ _wheels: list[Wheel],
+ jointTree: types_pb2.GraphContainer,
progressDialog: PDMessage,
) -> None:
# progressDialog.message = f"Building Joint Graph Map from given joints"
@@ -442,44 +440,43 @@ def createJointGraph(
progressDialog.update()
# keep track of current nodes to link them
- node_map = dict({})
+ nodeMap = dict()
# contains all of the static ground objects
groundNode = types_pb2.Node()
groundNode.value = "ground"
- node_map[groundNode.value] = groundNode
+ nodeMap[groundNode.value] = groundNode
# addWheelsToGraph(wheels, groundNode, joint_tree)
# first iterate through to create the nodes
- for supplied_joint in supplied_joints:
+ for suppliedJoint in suppliedJoints:
newNode = types_pb2.Node()
- newNode.value = supplied_joint.jointToken
- node_map[newNode.value] = newNode
+ newNode.value = suppliedJoint.jointToken
+ nodeMap[newNode.value] = newNode
# second sort them
- for supplied_joint in supplied_joints:
- current_node = node_map[supplied_joint.jointToken]
- if supplied_joint.parent == JointParentType.ROOT:
- node_map["ground"].children.append(node_map[supplied_joint.jointToken])
- elif node_map[supplied_joint.parent.value] is not None and node_map[supplied_joint.jointToken] is not None:
- node_map[supplied_joint.parent].children.append(node_map[supplied_joint.jointToken])
+ for suppliedJoint in suppliedJoints:
+ if suppliedJoint.parent == JointParentType.ROOT:
+ nodeMap["ground"].children.append(nodeMap[suppliedJoint.jointToken])
+ elif nodeMap[suppliedJoint.parent.value] is not None and nodeMap[suppliedJoint.jointToken] is not None:
+ nodeMap[str(suppliedJoint.parent)].children.append(nodeMap[suppliedJoint.jointToken])
else:
- logger.error(f"Cannot construct hierarhcy because of detached tree at : {supplied_joint.jointToken}")
+ logger.error(f"Cannot construct hierarhcy because of detached tree at : {suppliedJoint.jointToken}")
- for node in node_map.values():
+ for node in nodeMap.values():
# append everything at top level to isolate kinematics
- joint_tree.nodes.append(node)
+ jointTree.nodes.append(node)
-def addWheelsToGraph(wheels: list, rootNode: types_pb2.Node, joint_tree: types_pb2.GraphContainer):
+def addWheelsToGraph(wheels: list[Wheel], rootNode: types_pb2.Node, jointTree: types_pb2.GraphContainer) -> None:
for wheel in wheels:
# wheel name
# wheel signal
# wheel occ id
# these don't have children
wheelNode = types_pb2.Node()
- wheelNode.value = wheel.occurrenceToken
+ wheelNode.value = wheel.jointToken
rootNode.children.append(wheelNode)
- joint_tree.nodes.append(wheelNode)
+ jointTree.nodes.append(wheelNode)
diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py
index 22b42180a2..1ff5d3fe6c 100644
--- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py
+++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py
@@ -1,4 +1,4 @@
-import adsk
+import adsk.core
from src.Logging import logFailure
from src.Parser.ExporterOptions import ExporterOptions
@@ -27,7 +27,7 @@
def _MapAllPhysicalMaterials(
- physicalMaterials: list,
+ physicalMaterials: list[material_pb2.PhysicalMaterial],
materials: material_pb2.Materials,
options: ExporterOptions,
progressDialog: PDMessage,
@@ -44,24 +44,26 @@ def _MapAllPhysicalMaterials(
getPhysicalMaterialData(material, newmaterial, options)
-def setDefaultMaterial(physical_material: material_pb2.PhysicalMaterial, options: ExporterOptions):
- construct_info("default", physical_material)
+def setDefaultMaterial(physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions) -> None:
+ construct_info("default", physicalMaterial)
- physical_material.description = "A default physical material"
+ physicalMaterial.description = "A default physical material"
if options.frictionOverride:
- physical_material.dynamic_friction = options.frictionOverrideCoeff
- physical_material.static_friction = options.frictionOverrideCoeff
+ physicalMaterial.dynamic_friction = options.frictionOverrideCoeff
+ physicalMaterial.static_friction = options.frictionOverrideCoeff
else:
- physical_material.dynamic_friction = 0.5
- physical_material.static_friction = 0.5
+ physicalMaterial.dynamic_friction = 0.5
+ physicalMaterial.static_friction = 0.5
- physical_material.restitution = 0.5
- physical_material.deformable = False
- physical_material.matType = 0
+ physicalMaterial.restitution = 0.5
+ physicalMaterial.deformable = False
+ physicalMaterial.matType = 0 # type: ignore[assignment]
@logFailure
-def getPhysicalMaterialData(fusion_material, proto_material, options):
+def getPhysicalMaterialData(
+ fusionMaterial: adsk.core.Material, physicalMaterial: material_pb2.PhysicalMaterial, options: ExporterOptions
+) -> None:
"""Gets the material data and adds it to protobuf
Args:
@@ -69,26 +71,26 @@ def getPhysicalMaterialData(fusion_material, proto_material, options):
proto_material (protomaterial): proto material mirabuf
options (parseoptions): parse options
"""
- construct_info("", proto_material, fus_object=fusion_material)
+ construct_info("", physicalMaterial, fus_object=fusionMaterial)
- proto_material.deformable = False
- proto_material.matType = 0
+ physicalMaterial.deformable = False
+ physicalMaterial.matType = 0 # type: ignore[assignment]
- materialProperties = fusion_material.materialProperties
+ materialProperties = fusionMaterial.materialProperties
- thermalProperties = proto_material.thermal
- mechanicalProperties = proto_material.mechanical
- strengthProperties = proto_material.strength
+ thermalProperties = physicalMaterial.thermal
+ mechanicalProperties = physicalMaterial.mechanical
+ strengthProperties = physicalMaterial.strength
if options.frictionOverride:
- proto_material.dynamic_friction = options.frictionOverrideCoeff
- proto_material.static_friction = options.frictionOverrideCoeff
+ physicalMaterial.dynamic_friction = options.frictionOverrideCoeff
+ physicalMaterial.static_friction = options.frictionOverrideCoeff
else:
- proto_material.dynamic_friction = DYNAMIC_FRICTION_COEFFS.get(fusion_material.name, 0.5)
- proto_material.static_friction = STATIC_FRICTION_COEFFS.get(fusion_material.name, 0.5)
+ physicalMaterial.dynamic_friction = DYNAMIC_FRICTION_COEFFS.get(fusionMaterial.name, 0.5)
+ physicalMaterial.static_friction = STATIC_FRICTION_COEFFS.get(fusionMaterial.name, 0.5)
- proto_material.restitution = 0.5
- proto_material.description = f"{fusion_material.name} exported from FUSION"
+ physicalMaterial.restitution = 0.5
+ physicalMaterial.description = f"{fusionMaterial.name} exported from FUSION"
"""
Thermal Properties
@@ -138,7 +140,7 @@ def getPhysicalMaterialData(fusion_material, proto_material, options):
def _MapAllAppearances(
- appearances: list,
+ appearances: list[material_pb2.Appearance],
materials: material_pb2.Materials,
options: ExporterOptions,
progressDialog: PDMessage,
diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PDMessage.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PDMessage.py
index ad441fc901..c157495a0f 100644
--- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PDMessage.py
+++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PDMessage.py
@@ -33,7 +33,7 @@ def __init__(
self.progressDialog = progressDialog
- def _format(self):
+ def _format(self) -> str:
# USE FORMATTING TO CENTER THESE BAD BOIS
# TABS DO NOTHING HALP
out = f"{self.assemblyName} parsing:\n"
@@ -45,44 +45,44 @@ def _format(self):
return out
- def addComponent(self, name=None):
+ def addComponent(self, name: str | None = None) -> None:
self.currentValue += 1
self.currentCompCount += 1
self.currentMessage = f"Exporting Component {name}"
self.update()
- def addOccurrence(self, name=None):
+ def addOccurrence(self, name: str | None = None) -> None:
self.currentValue += 1
self.currentOccCount += 1
self.currentMessage = f"Exporting Occurrence {name}"
self.update()
- def addMaterial(self, name=None):
+ def addMaterial(self, name: str | None = None) -> None:
self.currentValue += 1
self.currentMatCount += 1
self.currentMessage = f"Exporting Physical Material {name}"
self.update()
- def addAppearance(self, name=None):
+ def addAppearance(self, name: str | None = None) -> None:
self.currentValue += 1
self.currentAppCount += 1
self.currentMessage = f"Exporting Appearance Material {name}"
self.update()
- def addJoint(self, name=None):
+ def addJoint(self, name: str | None = None) -> None:
self.currentMessage = f"Connecting Joints {name}"
self.update()
- def update(self):
+ def update(self) -> None:
self.progressDialog.message = self._format()
self.progressDialog.progressValue = self.currentValue
self.value = self.currentValue
def wasCancelled(self) -> bool:
- return self.progressDialog.wasCancelled
+ return self.progressDialog.wasCancelled # type: ignore[no-any-return]
- def __str__(self):
+ def __str__(self) -> str:
return self._format()
- def __repr__(self):
+ def __repr__(self) -> str:
return self._format()
diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py
index c44fd50fdb..b085b48d32 100644
--- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py
+++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py
@@ -179,8 +179,7 @@ def export(self) -> None:
logger.debug("Uploading file to APS")
project = app.data.activeProject
if not project.isValid:
- gm.ui.messageBox("Project is invalid", "")
- return False # add throw later
+ raise RuntimeError("Project is invalid")
project_id = project.id
folder_id = project.rootFolder.id
file_name = f"{self.exporterOptions.fileLocation}.mira"
@@ -189,18 +188,17 @@ def export(self) -> None:
else:
assert self.exporterOptions.exportLocation == ExportLocation.DOWNLOAD
# check if entire path exists and create if not since gzip doesn't do that.
- path = pathlib.Path(self.exporterOptions.fileLocation).parent
+ path = pathlib.Path(str(self.exporterOptions.fileLocation)).parent
path.mkdir(parents=True, exist_ok=True)
+ self.pdMessage.currentMessage = "Saving File..."
+ self.pdMessage.update()
if self.exporterOptions.compressOutput:
logger.debug("Compressing file")
- with gzip.open(self.exporterOptions.fileLocation, "wb", 9) as f:
- self.pdMessage.currentMessage = "Saving File..."
- self.pdMessage.update()
+ with gzip.open(str(self.exporterOptions.fileLocation), "wb", 9) as f:
f.write(assembly_out.SerializeToString())
else:
- f = open(self.exporterOptions.fileLocation, "wb")
- f.write(assembly_out.SerializeToString())
- f.close()
+ with open(str(self.exporterOptions.fileLocation), "wb") as f:
+ f.write(assembly_out.SerializeToString())
_ = progressDialog.hide()
diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py
index c19b0bd6b3..53af57dfe5 100644
--- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py
+++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py
@@ -28,8 +28,8 @@
def GetPhysicalProperties(
fusionObject: Union[adsk.fusion.BRepBody, adsk.fusion.Occurrence, adsk.fusion.Component],
physicalProperties: types_pb2.PhysicalProperties,
- level=1,
-):
+ level: int = 1,
+) -> None:
"""Will populate a physical properties section of an exported file
Args:
diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py
index 78da93ffec..a5e389beda 100644
--- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py
+++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py
@@ -21,10 +21,13 @@
from src.Proto import assembly_pb2
+# Transition: AARD-1765
+# According to the type errors I'm getting here this code would have never compiled.
+# Should be removed later
@logFailure
def ExportRigidGroups(
fus_occ: Union[adsk.fusion.Occurrence, adsk.fusion.Component],
- hel_occ: assembly_pb2.Occurrence,
+ hel_occ: assembly_pb2.Occurrence, # type: ignore[name-defined]
) -> None:
"""Takes a Fusion and Protobuf Occurrence and will assign Rigidbody data per the occurrence if any exist and are not surpressed.
diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py
index 0a5d277766..d8f38d921f 100644
--- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py
+++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py
@@ -1,15 +1,17 @@
import math
import uuid
-from adsk.core import Vector3D
-from adsk.fusion import Component, Occurrence
+import adsk.core
+import adsk.fusion
+from src.Proto import assembly_pb2
-def guid_component(comp: Component) -> str:
+
+def guid_component(comp: adsk.fusion.Component) -> str:
return f"{comp.entityToken}_{comp.id}"
-def guid_occurrence(occ: Occurrence) -> str:
+def guid_occurrence(occ: adsk.fusion.Occurrence) -> str:
return f"{occ.entityToken}_{guid_component(occ.component)}"
@@ -17,11 +19,17 @@ def guid_none(_: None) -> str:
return str(uuid.uuid4())
-def fill_info(proto_obj, fus_object, override_guid=None) -> None:
+def fill_info(proto_obj: assembly_pb2.Assembly, fus_object: adsk.core.Base, override_guid: str | None = None) -> None:
construct_info("", proto_obj, fus_object=fus_object, GUID=override_guid)
-def construct_info(name: str, proto_obj, version=5, fus_object=None, GUID=None) -> None:
+def construct_info(
+ name: str,
+ proto_obj: assembly_pb2.Assembly,
+ version: int = 5,
+ fus_object: adsk.core.Base | None = None,
+ GUID: str | None = None,
+) -> None:
"""Constructs a info object from either a name or a fus_object
Args:
@@ -41,24 +49,21 @@ def construct_info(name: str, proto_obj, version=5, fus_object=None, GUID=None)
if fus_object is not None:
proto_obj.info.name = fus_object.name
- elif name is not None:
- proto_obj.info.name = name
else:
- raise ValueError("Cannot construct info from no name or fus_object")
+ proto_obj.info.name = name
if GUID is not None:
proto_obj.info.GUID = str(GUID)
+ elif fus_object is not None and hasattr(fus_object, "entityToken"):
+ proto_obj.info.GUID = fus_object.entityToken
else:
- try:
- # attempt to get entity token
- proto_obj.info.GUID = fus_object.entityToken
- except AttributeError:
- # fails and gets new uuid
- proto_obj.info.GUID = str(uuid.uuid4())
+ proto_obj.info.GUID = str(uuid.uuid4())
+# Transition: AARD-1765
+# Will likely be removed later as this is no longer used. Avoiding adding typing for now.
# My previous function was alot more optimized however now I realize the bug was this doesn't work well with degrees
-def euler_to_quaternion(r):
+def euler_to_quaternion(r): # type: ignore
(yaw, pitch, roll) = (r[0], r[1], r[2])
qx = math.sin(roll / 2) * math.cos(pitch / 2) * math.cos(yaw / 2) - math.cos(roll / 2) * math.sin(
pitch / 2
@@ -75,7 +80,7 @@ def euler_to_quaternion(r):
return [qx, qy, qz, qw]
-def rad_to_deg(rad):
+def rad_to_deg(rad): # type: ignore
"""Very simple method to convert Radians to degrees
Args:
@@ -87,7 +92,7 @@ def rad_to_deg(rad):
return (rad * 180) / math.pi
-def quaternion_to_euler(qx, qy, qz, qw):
+def quaternion_to_euler(qx, qy, qz, qw): # type: ignore
"""Takes in quat values and converts to degrees
- roll is x axis - atan2(2(qwqy + qzqw), 1-2(qy^2 + qz^2))
@@ -127,7 +132,7 @@ def quaternion_to_euler(qx, qy, qz, qw):
return round(roll, 4), round(pitch, 4), round(yaw, 4)
-def throwZero():
+def throwZero(): # type: ignore
"""Simple function to report incorrect quat values
Raises:
@@ -136,7 +141,7 @@ def throwZero():
raise RuntimeError("While computing the quaternion the trace was reported as 0 which is invalid")
-def spatial_to_quaternion(mat):
+def spatial_to_quaternion(mat): # type: ignore
"""Takes a 1D Spatial Transform Matrix and derives rotational quaternion
I wrote this however it is difficult to extensibly test so use with caution
@@ -194,13 +199,13 @@ def spatial_to_quaternion(mat):
raise RuntimeError("Supplied matrix to spatial_to_quaternion is not a 1D spatial matrix in size.")
-def normalize_quaternion(x, y, z, w):
+def normalize_quaternion(x, y, z, w): # type: ignore
f = 1.0 / math.sqrt((x * x) + (y * y) + (z * z) + (w * w))
return x * f, y * f, z * f, w * f
-def _getAngleTo(vec_origin: list, vec_current: Vector3D) -> int:
- origin = Vector3D.create(vec_origin[0], vec_origin[1], vec_origin[2])
+def _getAngleTo(vec_origin: list, vec_current: adsk.core.Vector3D) -> int: # type: ignore
+ origin = adsk.core.Vector3D.create(vec_origin[0], vec_origin[1], vec_origin[2])
val = origin.angleTo(vec_current)
deg = val * (180 / math.pi)
- return val
+ return val # type: ignore
diff --git a/exporter/SynthesisFusionAddin/src/Proto/assembly_pb2.pyi b/exporter/SynthesisFusionAddin/src/Proto/assembly_pb2.pyi
new file mode 100644
index 0000000000..84548670b8
--- /dev/null
+++ b/exporter/SynthesisFusionAddin/src/Proto/assembly_pb2.pyi
@@ -0,0 +1,449 @@
+"""
+@generated by mypy-protobuf. Do not edit manually!
+isort:skip_file
+"""
+
+import builtins
+import collections.abc
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.message
+import joint_pb2
+import material_pb2
+import signal_pb2
+import types_pb2
+import typing
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+@typing.final
+class Assembly(google.protobuf.message.Message):
+ """*
+ Assembly
+ Base Design to be interacted with
+ THIS IS THE CURRENT FILE EXPORTED
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ INFO_FIELD_NUMBER: builtins.int
+ DATA_FIELD_NUMBER: builtins.int
+ DYNAMIC_FIELD_NUMBER: builtins.int
+ PHYSICAL_DATA_FIELD_NUMBER: builtins.int
+ DESIGN_HIERARCHY_FIELD_NUMBER: builtins.int
+ JOINT_HIERARCHY_FIELD_NUMBER: builtins.int
+ TRANSFORM_FIELD_NUMBER: builtins.int
+ THUMBNAIL_FIELD_NUMBER: builtins.int
+ dynamic: builtins.bool
+ """/ Can it be effected by the simulation dynamically"""
+ @property
+ def info(self) -> types_pb2.Info:
+ """/ Basic information (name, Author, etc)"""
+
+ @property
+ def data(self) -> global___AssemblyData:
+ """/ All of the data in the assembly"""
+
+ @property
+ def physical_data(self) -> types_pb2.PhysicalProperties:
+ """/ Overall physical data of the assembly"""
+
+ @property
+ def design_hierarchy(self) -> types_pb2.GraphContainer:
+ """/ The Design hierarchy represented by Part Refs - The first object is a root container for all top level items"""
+
+ @property
+ def joint_hierarchy(self) -> types_pb2.GraphContainer:
+ """/ The Joint hierarchy for compound shapes"""
+
+ @property
+ def transform(self) -> types_pb2.Transform:
+ """/ The Transform in space currently"""
+
+ @property
+ def thumbnail(self) -> types_pb2.Thumbnail:
+ """/ Optional thumbnail saved from Fusion 360 or scraped from previous configuration"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ data: global___AssemblyData | None = ...,
+ dynamic: builtins.bool = ...,
+ physical_data: types_pb2.PhysicalProperties | None = ...,
+ design_hierarchy: types_pb2.GraphContainer | None = ...,
+ joint_hierarchy: types_pb2.GraphContainer | None = ...,
+ transform: types_pb2.Transform | None = ...,
+ thumbnail: types_pb2.Thumbnail | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["data", b"data", "design_hierarchy", b"design_hierarchy", "info", b"info", "joint_hierarchy", b"joint_hierarchy", "physical_data", b"physical_data", "thumbnail", b"thumbnail", "transform", b"transform"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["data", b"data", "design_hierarchy", b"design_hierarchy", "dynamic", b"dynamic", "info", b"info", "joint_hierarchy", b"joint_hierarchy", "physical_data", b"physical_data", "thumbnail", b"thumbnail", "transform", b"transform"]) -> None: ...
+
+global___Assembly = Assembly
+
+@typing.final
+class AssemblyData(google.protobuf.message.Message):
+ """*
+ Data used to construct the assembly
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ PARTS_FIELD_NUMBER: builtins.int
+ JOINTS_FIELD_NUMBER: builtins.int
+ MATERIALS_FIELD_NUMBER: builtins.int
+ SIGNALS_FIELD_NUMBER: builtins.int
+ @property
+ def parts(self) -> global___Parts:
+ """/ Meshes and Design Objects"""
+
+ @property
+ def joints(self) -> joint_pb2.Joints:
+ """/ Joint Definition Set"""
+
+ @property
+ def materials(self) -> material_pb2.Materials:
+ """/ Appearance and Physical Material Set"""
+
+ @property
+ def signals(self) -> signal_pb2.Signals:
+ """Contains table of all signals with ID reference"""
+
+ def __init__(
+ self,
+ *,
+ parts: global___Parts | None = ...,
+ joints: joint_pb2.Joints | None = ...,
+ materials: material_pb2.Materials | None = ...,
+ signals: signal_pb2.Signals | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["joints", b"joints", "materials", b"materials", "parts", b"parts", "signals", b"signals"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["joints", b"joints", "materials", b"materials", "parts", b"parts", "signals", b"signals"]) -> None: ...
+
+global___AssemblyData = AssemblyData
+
+@typing.final
+class Parts(google.protobuf.message.Message):
+ """Part file can be exported seperately in the future"""
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ @typing.final
+ class PartDefinitionsEntry(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ KEY_FIELD_NUMBER: builtins.int
+ VALUE_FIELD_NUMBER: builtins.int
+ key: builtins.str
+ @property
+ def value(self) -> global___PartDefinition: ...
+ def __init__(
+ self,
+ *,
+ key: builtins.str = ...,
+ value: global___PartDefinition | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ...
+
+ @typing.final
+ class PartInstancesEntry(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ KEY_FIELD_NUMBER: builtins.int
+ VALUE_FIELD_NUMBER: builtins.int
+ key: builtins.str
+ @property
+ def value(self) -> global___PartInstance: ...
+ def __init__(
+ self,
+ *,
+ key: builtins.str = ...,
+ value: global___PartInstance | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ...
+
+ INFO_FIELD_NUMBER: builtins.int
+ PART_DEFINITIONS_FIELD_NUMBER: builtins.int
+ PART_INSTANCES_FIELD_NUMBER: builtins.int
+ USER_DATA_FIELD_NUMBER: builtins.int
+ @property
+ def info(self) -> types_pb2.Info:
+ """/ Part name, version, GUID"""
+
+ @property
+ def part_definitions(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___PartDefinition]:
+ """/ Map of the Exported Part Definitions"""
+
+ @property
+ def part_instances(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___PartInstance]:
+ """/ Map of the Exported Parts that make up the object"""
+
+ @property
+ def user_data(self) -> types_pb2.UserData:
+ """/ other associated data that can be used
+ end effector, wheel, etc
+ """
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ part_definitions: collections.abc.Mapping[builtins.str, global___PartDefinition] | None = ...,
+ part_instances: collections.abc.Mapping[builtins.str, global___PartInstance] | None = ...,
+ user_data: types_pb2.UserData | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["info", b"info", "user_data", b"user_data"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["info", b"info", "part_definitions", b"part_definitions", "part_instances", b"part_instances", "user_data", b"user_data"]) -> None: ...
+
+global___Parts = Parts
+
+@typing.final
+class PartDefinition(google.protobuf.message.Message):
+ """*
+ Part Definition
+ Unique Definition of a part that can be replicated.
+ Useful for keeping the object counter down in the scene.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ INFO_FIELD_NUMBER: builtins.int
+ PHYSICAL_DATA_FIELD_NUMBER: builtins.int
+ BASE_TRANSFORM_FIELD_NUMBER: builtins.int
+ BODIES_FIELD_NUMBER: builtins.int
+ DYNAMIC_FIELD_NUMBER: builtins.int
+ FRICTION_OVERRIDE_FIELD_NUMBER: builtins.int
+ MASS_OVERRIDE_FIELD_NUMBER: builtins.int
+ dynamic: builtins.bool
+ """/ Optional value to state whether an object is a dynamic object in a static assembly - all children are also considered overriden"""
+ friction_override: builtins.float
+ """/ Optional value for overriding the friction value 0-1"""
+ mass_override: builtins.float
+ """/ Optional value for overriding an indiviaul object's mass"""
+ @property
+ def info(self) -> types_pb2.Info:
+ """/ Information about version - id - name"""
+
+ @property
+ def physical_data(self) -> types_pb2.PhysicalProperties:
+ """/ Physical data associated with Part"""
+
+ @property
+ def base_transform(self) -> types_pb2.Transform:
+ """/ Base Transform applied - Most Likely Identity Matrix"""
+
+ @property
+ def bodies(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Body]:
+ """/ Mesh Bodies to populate part"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ physical_data: types_pb2.PhysicalProperties | None = ...,
+ base_transform: types_pb2.Transform | None = ...,
+ bodies: collections.abc.Iterable[global___Body] | None = ...,
+ dynamic: builtins.bool = ...,
+ friction_override: builtins.float = ...,
+ mass_override: builtins.float = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["base_transform", b"base_transform", "info", b"info", "physical_data", b"physical_data"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["base_transform", b"base_transform", "bodies", b"bodies", "dynamic", b"dynamic", "friction_override", b"friction_override", "info", b"info", "mass_override", b"mass_override", "physical_data", b"physical_data"]) -> None: ...
+
+global___PartDefinition = PartDefinition
+
+@typing.final
+class PartInstance(google.protobuf.message.Message):
+ """
+ Part
+ Represents a object that does not have to be unique
+ Can be an override for an existing definition
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ INFO_FIELD_NUMBER: builtins.int
+ PART_DEFINITION_REFERENCE_FIELD_NUMBER: builtins.int
+ TRANSFORM_FIELD_NUMBER: builtins.int
+ GLOBAL_TRANSFORM_FIELD_NUMBER: builtins.int
+ JOINTS_FIELD_NUMBER: builtins.int
+ APPEARANCE_FIELD_NUMBER: builtins.int
+ PHYSICAL_MATERIAL_FIELD_NUMBER: builtins.int
+ SKIP_COLLIDER_FIELD_NUMBER: builtins.int
+ part_definition_reference: builtins.str
+ """/ Reference to the Part Definition defined in Assembly Data"""
+ appearance: builtins.str
+ """Appearance Reference to link to `Materials->Appearance->Info->id`"""
+ physical_material: builtins.str
+ """/ Physical Material Reference to link to `Materials->PhysicalMaterial->Info->id`"""
+ skip_collider: builtins.bool
+ """/ Flag that if enabled indicates we should skip generating a collider, defaults to FALSE or undefined"""
+ @property
+ def info(self) -> types_pb2.Info: ...
+ @property
+ def transform(self) -> types_pb2.Transform:
+ """/ Overriding the object transform (moves the part from the def) - in design hierarchy context"""
+
+ @property
+ def global_transform(self) -> types_pb2.Transform:
+ """/ Position transform from a global scope"""
+
+ @property
+ def joints(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
+ """/ Joints that interact with this element"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ part_definition_reference: builtins.str = ...,
+ transform: types_pb2.Transform | None = ...,
+ global_transform: types_pb2.Transform | None = ...,
+ joints: collections.abc.Iterable[builtins.str] | None = ...,
+ appearance: builtins.str = ...,
+ physical_material: builtins.str = ...,
+ skip_collider: builtins.bool = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["global_transform", b"global_transform", "info", b"info", "transform", b"transform"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["appearance", b"appearance", "global_transform", b"global_transform", "info", b"info", "joints", b"joints", "part_definition_reference", b"part_definition_reference", "physical_material", b"physical_material", "skip_collider", b"skip_collider", "transform", b"transform"]) -> None: ...
+
+global___PartInstance = PartInstance
+
+@typing.final
+class Body(google.protobuf.message.Message):
+ """
+ Body object
+ Can contain a TriangleMesh or Collection of Faces.
+ Must be unique in the context of the Assembly.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ INFO_FIELD_NUMBER: builtins.int
+ PART_FIELD_NUMBER: builtins.int
+ TRIANGLE_MESH_FIELD_NUMBER: builtins.int
+ APPEARANCE_OVERRIDE_FIELD_NUMBER: builtins.int
+ part: builtins.str
+ """/ Reference to Part Definition"""
+ appearance_override: builtins.str
+ """/ Override Visual Appearance for the body"""
+ @property
+ def info(self) -> types_pb2.Info: ...
+ @property
+ def triangle_mesh(self) -> global___TriangleMesh:
+ """/ Triangle Mesh for rendering"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ part: builtins.str = ...,
+ triangle_mesh: global___TriangleMesh | None = ...,
+ appearance_override: builtins.str = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["info", b"info", "triangle_mesh", b"triangle_mesh"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["appearance_override", b"appearance_override", "info", b"info", "part", b"part", "triangle_mesh", b"triangle_mesh"]) -> None: ...
+
+global___Body = Body
+
+@typing.final
+class TriangleMesh(google.protobuf.message.Message):
+ """*
+ Traingle Mesh for Storing Display Mesh data
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ INFO_FIELD_NUMBER: builtins.int
+ HAS_VOLUME_FIELD_NUMBER: builtins.int
+ MATERIAL_REFERENCE_FIELD_NUMBER: builtins.int
+ MESH_FIELD_NUMBER: builtins.int
+ BMESH_FIELD_NUMBER: builtins.int
+ has_volume: builtins.bool
+ """/ Is this object a Plane ? (Does it have volume)"""
+ material_reference: builtins.str
+ """/ Rendered Appearance properties referenced from Assembly Data"""
+ @property
+ def info(self) -> types_pb2.Info: ...
+ @property
+ def mesh(self) -> global___Mesh:
+ """/ Stored as true types, inidicies, verts, uv"""
+
+ @property
+ def bmesh(self) -> global___BinaryMesh:
+ """/ Stored as binary data in bytes"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ has_volume: builtins.bool = ...,
+ material_reference: builtins.str = ...,
+ mesh: global___Mesh | None = ...,
+ bmesh: global___BinaryMesh | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["bmesh", b"bmesh", "info", b"info", "mesh", b"mesh", "mesh_type", b"mesh_type"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["bmesh", b"bmesh", "has_volume", b"has_volume", "info", b"info", "material_reference", b"material_reference", "mesh", b"mesh", "mesh_type", b"mesh_type"]) -> None: ...
+ def WhichOneof(self, oneof_group: typing.Literal["mesh_type", b"mesh_type"]) -> typing.Literal["mesh", "bmesh"] | None: ...
+
+global___TriangleMesh = TriangleMesh
+
+@typing.final
+class Mesh(google.protobuf.message.Message):
+ """*
+ Mesh Data stored as generic Data Structure
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ VERTS_FIELD_NUMBER: builtins.int
+ NORMALS_FIELD_NUMBER: builtins.int
+ UV_FIELD_NUMBER: builtins.int
+ INDICES_FIELD_NUMBER: builtins.int
+ @property
+ def verts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]:
+ """/ Tri Mesh Verts vec3"""
+
+ @property
+ def normals(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]:
+ """/ Tri Mesh Normals vec3"""
+
+ @property
+ def uv(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]:
+ """/ Tri Mesh uv Mapping vec2"""
+
+ @property
+ def indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]:
+ """/ Tri Mesh indicies (Vert Map)"""
+
+ def __init__(
+ self,
+ *,
+ verts: collections.abc.Iterable[builtins.float] | None = ...,
+ normals: collections.abc.Iterable[builtins.float] | None = ...,
+ uv: collections.abc.Iterable[builtins.float] | None = ...,
+ indices: collections.abc.Iterable[builtins.int] | None = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["indices", b"indices", "normals", b"normals", "uv", b"uv", "verts", b"verts"]) -> None: ...
+
+global___Mesh = Mesh
+
+@typing.final
+class BinaryMesh(google.protobuf.message.Message):
+ """/ Mesh used for more effective file transfers"""
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ DATA_FIELD_NUMBER: builtins.int
+ data: builtins.bytes
+ """/ BEWARE of ENDIANESS"""
+ def __init__(
+ self,
+ *,
+ data: builtins.bytes = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["data", b"data"]) -> None: ...
+
+global___BinaryMesh = BinaryMesh
diff --git a/exporter/SynthesisFusionAddin/src/Proto/joint_pb2.pyi b/exporter/SynthesisFusionAddin/src/Proto/joint_pb2.pyi
new file mode 100644
index 0000000000..f716bc2705
--- /dev/null
+++ b/exporter/SynthesisFusionAddin/src/Proto/joint_pb2.pyi
@@ -0,0 +1,570 @@
+"""
+@generated by mypy-protobuf. Do not edit manually!
+isort:skip_file
+"""
+
+import builtins
+import collections.abc
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.internal.enum_type_wrapper
+import google.protobuf.message
+import motor_pb2
+import sys
+import types_pb2
+import typing
+
+if sys.version_info >= (3, 10):
+ import typing as typing_extensions
+else:
+ import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class _JointMotion:
+ ValueType = typing.NewType("ValueType", builtins.int)
+ V: typing_extensions.TypeAlias = ValueType
+
+class _JointMotionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_JointMotion.ValueType], builtins.type):
+ DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+ RIGID: _JointMotion.ValueType # 0
+ REVOLUTE: _JointMotion.ValueType # 1
+ SLIDER: _JointMotion.ValueType # 2
+ CYLINDRICAL: _JointMotion.ValueType # 3
+ PINSLOT: _JointMotion.ValueType # 4
+ PLANAR: _JointMotion.ValueType # 5
+ BALL: _JointMotion.ValueType # 6
+ CUSTOM: _JointMotion.ValueType # 7
+
+class JointMotion(_JointMotion, metaclass=_JointMotionEnumTypeWrapper):
+ """Describes the joint - Not really sure what to do with this for now - TBD"""
+
+RIGID: JointMotion.ValueType # 0
+REVOLUTE: JointMotion.ValueType # 1
+SLIDER: JointMotion.ValueType # 2
+CYLINDRICAL: JointMotion.ValueType # 3
+PINSLOT: JointMotion.ValueType # 4
+PLANAR: JointMotion.ValueType # 5
+BALL: JointMotion.ValueType # 6
+CUSTOM: JointMotion.ValueType # 7
+global___JointMotion = JointMotion
+
+@typing.final
+class Joints(google.protobuf.message.Message):
+ """You can have an Open-Chain robot meaning a single path
+ You can have a closed chain mechanism or Four-bar (closed loop)
+ Or multiple paths with closed loop like a stewart platform
+
+ *
+ Joints
+ A way to define the motion between various group connections
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ @typing.final
+ class JointDefinitionsEntry(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ KEY_FIELD_NUMBER: builtins.int
+ VALUE_FIELD_NUMBER: builtins.int
+ key: builtins.str
+ @property
+ def value(self) -> global___Joint: ...
+ def __init__(
+ self,
+ *,
+ key: builtins.str = ...,
+ value: global___Joint | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ...
+
+ @typing.final
+ class JointInstancesEntry(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ KEY_FIELD_NUMBER: builtins.int
+ VALUE_FIELD_NUMBER: builtins.int
+ key: builtins.str
+ @property
+ def value(self) -> global___JointInstance: ...
+ def __init__(
+ self,
+ *,
+ key: builtins.str = ...,
+ value: global___JointInstance | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ...
+
+ @typing.final
+ class MotorDefinitionsEntry(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ KEY_FIELD_NUMBER: builtins.int
+ VALUE_FIELD_NUMBER: builtins.int
+ key: builtins.str
+ @property
+ def value(self) -> motor_pb2.Motor: ...
+ def __init__(
+ self,
+ *,
+ key: builtins.str = ...,
+ value: motor_pb2.Motor | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ...
+
+ INFO_FIELD_NUMBER: builtins.int
+ JOINT_DEFINITIONS_FIELD_NUMBER: builtins.int
+ JOINT_INSTANCES_FIELD_NUMBER: builtins.int
+ RIGID_GROUPS_FIELD_NUMBER: builtins.int
+ MOTOR_DEFINITIONS_FIELD_NUMBER: builtins.int
+ @property
+ def info(self) -> types_pb2.Info:
+ """/ name, version, uid"""
+
+ @property
+ def joint_definitions(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Joint]:
+ """/ Unique Joint Implementations"""
+
+ @property
+ def joint_instances(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___JointInstance]:
+ """/ Instances of the Joint Implementations"""
+
+ @property
+ def rigid_groups(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___RigidGroup]:
+ """/ Rigidgroups ?"""
+
+ @property
+ def motor_definitions(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, motor_pb2.Motor]:
+ """/ Collection of all Motors exported"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ joint_definitions: collections.abc.Mapping[builtins.str, global___Joint] | None = ...,
+ joint_instances: collections.abc.Mapping[builtins.str, global___JointInstance] | None = ...,
+ rigid_groups: collections.abc.Iterable[global___RigidGroup] | None = ...,
+ motor_definitions: collections.abc.Mapping[builtins.str, motor_pb2.Motor] | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["info", b"info"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["info", b"info", "joint_definitions", b"joint_definitions", "joint_instances", b"joint_instances", "motor_definitions", b"motor_definitions", "rigid_groups", b"rigid_groups"]) -> None: ...
+
+global___Joints = Joints
+
+@typing.final
+class JointInstance(google.protobuf.message.Message):
+ """*
+ Instance of a Joint that has a defined motion and limits.
+ Instancing helps with identifiy closed loop systems.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ INFO_FIELD_NUMBER: builtins.int
+ ISENDEFFECTOR_FIELD_NUMBER: builtins.int
+ PARENT_PART_FIELD_NUMBER: builtins.int
+ CHILD_PART_FIELD_NUMBER: builtins.int
+ JOINT_REFERENCE_FIELD_NUMBER: builtins.int
+ OFFSET_FIELD_NUMBER: builtins.int
+ PARTS_FIELD_NUMBER: builtins.int
+ SIGNAL_REFERENCE_FIELD_NUMBER: builtins.int
+ MOTION_LINK_FIELD_NUMBER: builtins.int
+ isEndEffector: builtins.bool
+ """Is this joint the end effector in the tree ? - might remove this"""
+ parent_part: builtins.str
+ """Object that contains the joint - the ID - Part usually"""
+ child_part: builtins.str
+ """Object that is affected by the joint - the ID - Part usually"""
+ joint_reference: builtins.str
+ """Reference to the Joint Definition"""
+ signal_reference: builtins.str
+ """Reference to the Signals as Drivers - use for signal_map in Assembly Data"""
+ @property
+ def info(self) -> types_pb2.Info:
+ """Joint name, ID, version, etc"""
+
+ @property
+ def offset(self) -> types_pb2.Vector3:
+ """Offset from Joint Definition Origin"""
+
+ @property
+ def parts(self) -> types_pb2.GraphContainer:
+ """Part Instances all contained and affected by this joint directly - tree"""
+
+ @property
+ def motion_link(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MotionLink]:
+ """Motion Links to other joints - ways to preserve motion between dynamic objects"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ isEndEffector: builtins.bool = ...,
+ parent_part: builtins.str = ...,
+ child_part: builtins.str = ...,
+ joint_reference: builtins.str = ...,
+ offset: types_pb2.Vector3 | None = ...,
+ parts: types_pb2.GraphContainer | None = ...,
+ signal_reference: builtins.str = ...,
+ motion_link: collections.abc.Iterable[global___MotionLink] | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["info", b"info", "offset", b"offset", "parts", b"parts"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["child_part", b"child_part", "info", b"info", "isEndEffector", b"isEndEffector", "joint_reference", b"joint_reference", "motion_link", b"motion_link", "offset", b"offset", "parent_part", b"parent_part", "parts", b"parts", "signal_reference", b"signal_reference"]) -> None: ...
+
+global___JointInstance = JointInstance
+
+@typing.final
+class MotionLink(google.protobuf.message.Message):
+ """*
+ Motion Link Feature
+ Enables the restriction on a joint to a certain range of motion as it is relative to another joint
+ This is useful for moving parts restricted by belts and gears
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ JOINT_INSTANCE_FIELD_NUMBER: builtins.int
+ RATIO_FIELD_NUMBER: builtins.int
+ REVERSED_FIELD_NUMBER: builtins.int
+ joint_instance: builtins.str
+ """The Joint that this is linked to"""
+ ratio: builtins.float
+ """Ratio of motion between joint 1 and joint 2, we assume this is in mm for linear and deg for rotational"""
+ reversed: builtins.bool
+ """Reverse the relationship - turn in the same or opposite directions - useful when moving axis arent both the same way."""
+ def __init__(
+ self,
+ *,
+ joint_instance: builtins.str = ...,
+ ratio: builtins.float = ...,
+ reversed: builtins.bool = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["joint_instance", b"joint_instance", "ratio", b"ratio", "reversed", b"reversed"]) -> None: ...
+
+global___MotionLink = MotionLink
+
+@typing.final
+class Joint(google.protobuf.message.Message):
+ """*
+ A unqiue implementation of a joint motion
+ Contains information about motion but not assembly relation
+ NOTE: A spring motion is a joint with no driver
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ INFO_FIELD_NUMBER: builtins.int
+ ORIGIN_FIELD_NUMBER: builtins.int
+ JOINT_MOTION_TYPE_FIELD_NUMBER: builtins.int
+ BREAK_MAGNITUDE_FIELD_NUMBER: builtins.int
+ ROTATIONAL_FIELD_NUMBER: builtins.int
+ PRISMATIC_FIELD_NUMBER: builtins.int
+ CUSTOM_FIELD_NUMBER: builtins.int
+ USER_DATA_FIELD_NUMBER: builtins.int
+ MOTOR_REFERENCE_FIELD_NUMBER: builtins.int
+ joint_motion_type: global___JointMotion.ValueType
+ """type of motion described by the joint"""
+ break_magnitude: builtins.float
+ """At what effort does it come apart at. - leave blank if it doesn't"""
+ motor_reference: builtins.str
+ """/ Motor definition reference to lookup in joints collection"""
+ @property
+ def info(self) -> types_pb2.Info:
+ """/ Joint name, ID, version, etc"""
+
+ @property
+ def origin(self) -> types_pb2.Vector3:
+ """Transform relative to the parent"""
+
+ @property
+ def rotational(self) -> global___RotationalJoint:
+ """/ ONEOF rotational joint"""
+
+ @property
+ def prismatic(self) -> global___PrismaticJoint:
+ """/ ONEOF prismatic joint"""
+
+ @property
+ def custom(self) -> global___CustomJoint:
+ """/ ONEOF custom joint"""
+
+ @property
+ def user_data(self) -> types_pb2.UserData:
+ """/ Additional information someone can query or store relative to your joint."""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ origin: types_pb2.Vector3 | None = ...,
+ joint_motion_type: global___JointMotion.ValueType = ...,
+ break_magnitude: builtins.float = ...,
+ rotational: global___RotationalJoint | None = ...,
+ prismatic: global___PrismaticJoint | None = ...,
+ custom: global___CustomJoint | None = ...,
+ user_data: types_pb2.UserData | None = ...,
+ motor_reference: builtins.str = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["JointMotion", b"JointMotion", "custom", b"custom", "info", b"info", "origin", b"origin", "prismatic", b"prismatic", "rotational", b"rotational", "user_data", b"user_data"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["JointMotion", b"JointMotion", "break_magnitude", b"break_magnitude", "custom", b"custom", "info", b"info", "joint_motion_type", b"joint_motion_type", "motor_reference", b"motor_reference", "origin", b"origin", "prismatic", b"prismatic", "rotational", b"rotational", "user_data", b"user_data"]) -> None: ...
+ def WhichOneof(self, oneof_group: typing.Literal["JointMotion", b"JointMotion"]) -> typing.Literal["rotational", "prismatic", "custom"] | None: ...
+
+global___Joint = Joint
+
+@typing.final
+class Dynamics(google.protobuf.message.Message):
+ """*
+ Dynamics specify the mechanical effects on the motion.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ DAMPING_FIELD_NUMBER: builtins.int
+ FRICTION_FIELD_NUMBER: builtins.int
+ damping: builtins.float
+ """/ Damping effect on a given joint motion"""
+ friction: builtins.float
+ """/ Friction effect on a given joint motion"""
+ def __init__(
+ self,
+ *,
+ damping: builtins.float = ...,
+ friction: builtins.float = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["damping", b"damping", "friction", b"friction"]) -> None: ...
+
+global___Dynamics = Dynamics
+
+@typing.final
+class Limits(google.protobuf.message.Message):
+ """*
+ Limits specify the mechanical range of a given joint.
+
+ TODO: Add units
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ LOWER_FIELD_NUMBER: builtins.int
+ UPPER_FIELD_NUMBER: builtins.int
+ VELOCITY_FIELD_NUMBER: builtins.int
+ EFFORT_FIELD_NUMBER: builtins.int
+ lower: builtins.float
+ """/ Lower Limit corresponds to default displacement"""
+ upper: builtins.float
+ """/ Upper Limit is the joint extent"""
+ velocity: builtins.float
+ """/ Velocity Max in m/s^2 (angular for rotational)"""
+ effort: builtins.float
+ """/ Effort is the absolute force a joint can apply for a given instant - ROS has a great article on it http://wiki.ros.org/pr2_controller_manager/safety_limits"""
+ def __init__(
+ self,
+ *,
+ lower: builtins.float = ...,
+ upper: builtins.float = ...,
+ velocity: builtins.float = ...,
+ effort: builtins.float = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["effort", b"effort", "lower", b"lower", "upper", b"upper", "velocity", b"velocity"]) -> None: ...
+
+global___Limits = Limits
+
+@typing.final
+class Safety(google.protobuf.message.Message):
+ """*
+ Safety switch configuration for a given joint.
+ Can usefully indicate a bounds issue.
+ Inspired by the URDF implementation.
+
+ This should really just be created by the controller.
+ http://wiki.ros.org/pr2_controller_manager/safety_limits
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ LOWER_LIMIT_FIELD_NUMBER: builtins.int
+ UPPER_LIMIT_FIELD_NUMBER: builtins.int
+ K_POSITION_FIELD_NUMBER: builtins.int
+ K_VELOCITY_FIELD_NUMBER: builtins.int
+ lower_limit: builtins.float
+ """/ Lower software limit"""
+ upper_limit: builtins.float
+ """/ Upper Software limit"""
+ k_position: builtins.float
+ """/ Relation between position and velocity limit"""
+ k_velocity: builtins.float
+ """/ Relation between effort and velocity limit"""
+ def __init__(
+ self,
+ *,
+ lower_limit: builtins.float = ...,
+ upper_limit: builtins.float = ...,
+ k_position: builtins.float = ...,
+ k_velocity: builtins.float = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["k_position", b"k_position", "k_velocity", b"k_velocity", "lower_limit", b"lower_limit", "upper_limit", b"upper_limit"]) -> None: ...
+
+global___Safety = Safety
+
+@typing.final
+class DOF(google.protobuf.message.Message):
+ """*
+ DOF - representing the construction of a joint motion
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ NAME_FIELD_NUMBER: builtins.int
+ AXIS_FIELD_NUMBER: builtins.int
+ PIVOTDIRECTION_FIELD_NUMBER: builtins.int
+ DYNAMICS_FIELD_NUMBER: builtins.int
+ LIMITS_FIELD_NUMBER: builtins.int
+ VALUE_FIELD_NUMBER: builtins.int
+ name: builtins.str
+ """/ In case you want to name this degree of freedom"""
+ pivotDirection: types_pb2.Axis.ValueType
+ """/ Direction the axis vector is offset from - this has an incorrect naming scheme"""
+ value: builtins.float
+ """/ Current value of the DOF"""
+ @property
+ def axis(self) -> types_pb2.Vector3:
+ """/ Axis the degree of freedom is pivoting by"""
+
+ @property
+ def dynamics(self) -> global___Dynamics:
+ """/ Dynamic properties of this joint pivot"""
+
+ @property
+ def limits(self) -> global___Limits:
+ """/ Limits of this freedom"""
+
+ def __init__(
+ self,
+ *,
+ name: builtins.str = ...,
+ axis: types_pb2.Vector3 | None = ...,
+ pivotDirection: types_pb2.Axis.ValueType = ...,
+ dynamics: global___Dynamics | None = ...,
+ limits: global___Limits | None = ...,
+ value: builtins.float = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["axis", b"axis", "dynamics", b"dynamics", "limits", b"limits"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["axis", b"axis", "dynamics", b"dynamics", "limits", b"limits", "name", b"name", "pivotDirection", b"pivotDirection", "value", b"value"]) -> None: ...
+
+global___DOF = DOF
+
+@typing.final
+class CustomJoint(google.protobuf.message.Message):
+ """*
+ CustomJoint is a joint with N degrees of freedom specified.
+ There should be input validation to handle max freedom case.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ DOFS_FIELD_NUMBER: builtins.int
+ @property
+ def dofs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___DOF]:
+ """/ A list of degrees of freedom that the joint can contain"""
+
+ def __init__(
+ self,
+ *,
+ dofs: collections.abc.Iterable[global___DOF] | None = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["dofs", b"dofs"]) -> None: ...
+
+global___CustomJoint = CustomJoint
+
+@typing.final
+class RotationalJoint(google.protobuf.message.Message):
+ """*
+ RotationalJoint describes a joint with rotational translation.
+ This is the exact same as prismatic for now.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ ROTATIONAL_FREEDOM_FIELD_NUMBER: builtins.int
+ @property
+ def rotational_freedom(self) -> global___DOF: ...
+ def __init__(
+ self,
+ *,
+ rotational_freedom: global___DOF | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["rotational_freedom", b"rotational_freedom"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["rotational_freedom", b"rotational_freedom"]) -> None: ...
+
+global___RotationalJoint = RotationalJoint
+
+@typing.final
+class BallJoint(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ YAW_FIELD_NUMBER: builtins.int
+ PITCH_FIELD_NUMBER: builtins.int
+ ROTATION_FIELD_NUMBER: builtins.int
+ @property
+ def yaw(self) -> global___DOF: ...
+ @property
+ def pitch(self) -> global___DOF: ...
+ @property
+ def rotation(self) -> global___DOF: ...
+ def __init__(
+ self,
+ *,
+ yaw: global___DOF | None = ...,
+ pitch: global___DOF | None = ...,
+ rotation: global___DOF | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["pitch", b"pitch", "rotation", b"rotation", "yaw", b"yaw"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["pitch", b"pitch", "rotation", b"rotation", "yaw", b"yaw"]) -> None: ...
+
+global___BallJoint = BallJoint
+
+@typing.final
+class PrismaticJoint(google.protobuf.message.Message):
+ """*
+ Prismatic Joint describes a motion that translates the position in a single axis
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ PRISMATIC_FREEDOM_FIELD_NUMBER: builtins.int
+ @property
+ def prismatic_freedom(self) -> global___DOF: ...
+ def __init__(
+ self,
+ *,
+ prismatic_freedom: global___DOF | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["prismatic_freedom", b"prismatic_freedom"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["prismatic_freedom", b"prismatic_freedom"]) -> None: ...
+
+global___PrismaticJoint = PrismaticJoint
+
+@typing.final
+class RigidGroup(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ NAME_FIELD_NUMBER: builtins.int
+ OCCURRENCES_FIELD_NUMBER: builtins.int
+ name: builtins.str
+ @property
+ def occurrences(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
+ """this could be the full path of the occurrence in order to make it easier to assembly them possibly - just parse on the unity side"""
+
+ def __init__(
+ self,
+ *,
+ name: builtins.str = ...,
+ occurrences: collections.abc.Iterable[builtins.str] | None = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["name", b"name", "occurrences", b"occurrences"]) -> None: ...
+
+global___RigidGroup = RigidGroup
diff --git a/exporter/SynthesisFusionAddin/src/Proto/material_pb2.pyi b/exporter/SynthesisFusionAddin/src/Proto/material_pb2.pyi
new file mode 100644
index 0000000000..8d81262376
--- /dev/null
+++ b/exporter/SynthesisFusionAddin/src/Proto/material_pb2.pyi
@@ -0,0 +1,302 @@
+"""
+@generated by mypy-protobuf. Do not edit manually!
+isort:skip_file
+"""
+
+import builtins
+import collections.abc
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.internal.enum_type_wrapper
+import google.protobuf.message
+import sys
+import types_pb2
+import typing
+
+if sys.version_info >= (3, 10):
+ import typing as typing_extensions
+else:
+ import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+@typing.final
+class Materials(google.protobuf.message.Message):
+ """*
+ Represents a File or Set of Materials with Appearances and Physical Data
+
+ Can be Stored in AssemblyData
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ @typing.final
+ class PhysicalMaterialsEntry(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ KEY_FIELD_NUMBER: builtins.int
+ VALUE_FIELD_NUMBER: builtins.int
+ key: builtins.str
+ @property
+ def value(self) -> global___PhysicalMaterial: ...
+ def __init__(
+ self,
+ *,
+ key: builtins.str = ...,
+ value: global___PhysicalMaterial | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ...
+
+ @typing.final
+ class AppearancesEntry(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ KEY_FIELD_NUMBER: builtins.int
+ VALUE_FIELD_NUMBER: builtins.int
+ key: builtins.str
+ @property
+ def value(self) -> global___Appearance: ...
+ def __init__(
+ self,
+ *,
+ key: builtins.str = ...,
+ value: global___Appearance | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ...
+
+ INFO_FIELD_NUMBER: builtins.int
+ PHYSICALMATERIALS_FIELD_NUMBER: builtins.int
+ APPEARANCES_FIELD_NUMBER: builtins.int
+ @property
+ def info(self) -> types_pb2.Info:
+ """/ Identifiable information (id, name, version)"""
+
+ @property
+ def physicalMaterials(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___PhysicalMaterial]:
+ """/ Map of Physical Materials"""
+
+ @property
+ def appearances(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Appearance]:
+ """/ Map of Appearances that are purely visual"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ physicalMaterials: collections.abc.Mapping[builtins.str, global___PhysicalMaterial] | None = ...,
+ appearances: collections.abc.Mapping[builtins.str, global___Appearance] | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["info", b"info"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["appearances", b"appearances", "info", b"info", "physicalMaterials", b"physicalMaterials"]) -> None: ...
+
+global___Materials = Materials
+
+@typing.final
+class Appearance(google.protobuf.message.Message):
+ """*
+ Contains information on how a object looks
+ Limited to just color for now
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ INFO_FIELD_NUMBER: builtins.int
+ ALBEDO_FIELD_NUMBER: builtins.int
+ ROUGHNESS_FIELD_NUMBER: builtins.int
+ METALLIC_FIELD_NUMBER: builtins.int
+ SPECULAR_FIELD_NUMBER: builtins.int
+ roughness: builtins.float
+ """/ roughness value 0-1"""
+ metallic: builtins.float
+ """/ metallic value 0-1"""
+ specular: builtins.float
+ """/ specular value 0-1"""
+ @property
+ def info(self) -> types_pb2.Info:
+ """/ Identfiable information (id, name, version)"""
+
+ @property
+ def albedo(self) -> types_pb2.Color:
+ """/ albedo map RGBA 0-255"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ albedo: types_pb2.Color | None = ...,
+ roughness: builtins.float = ...,
+ metallic: builtins.float = ...,
+ specular: builtins.float = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["albedo", b"albedo", "info", b"info"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["albedo", b"albedo", "info", b"info", "metallic", b"metallic", "roughness", b"roughness", "specular", b"specular"]) -> None: ...
+
+global___Appearance = Appearance
+
+@typing.final
+class PhysicalMaterial(google.protobuf.message.Message):
+ """*
+ Data to represent any given Physical Material
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ class _MaterialType:
+ ValueType = typing.NewType("ValueType", builtins.int)
+ V: typing_extensions.TypeAlias = ValueType
+
+ class _MaterialTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[PhysicalMaterial._MaterialType.ValueType], builtins.type):
+ DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+ METAL: PhysicalMaterial._MaterialType.ValueType # 0
+ PLASTIC: PhysicalMaterial._MaterialType.ValueType # 1
+
+ class MaterialType(_MaterialType, metaclass=_MaterialTypeEnumTypeWrapper): ...
+ METAL: PhysicalMaterial.MaterialType.ValueType # 0
+ PLASTIC: PhysicalMaterial.MaterialType.ValueType # 1
+
+ @typing.final
+ class Thermal(google.protobuf.message.Message):
+ """*
+ Thermal Properties Set Definition for Simulation.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ THERMAL_CONDUCTIVITY_FIELD_NUMBER: builtins.int
+ SPECIFIC_HEAT_FIELD_NUMBER: builtins.int
+ THERMAL_EXPANSION_COEFFICIENT_FIELD_NUMBER: builtins.int
+ thermal_conductivity: builtins.float
+ """/ W/(m*K)"""
+ specific_heat: builtins.float
+ """/ J/(g*C)"""
+ thermal_expansion_coefficient: builtins.float
+ """/ um/(m*C)"""
+ def __init__(
+ self,
+ *,
+ thermal_conductivity: builtins.float = ...,
+ specific_heat: builtins.float = ...,
+ thermal_expansion_coefficient: builtins.float = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["specific_heat", b"specific_heat", "thermal_conductivity", b"thermal_conductivity", "thermal_expansion_coefficient", b"thermal_expansion_coefficient"]) -> None: ...
+
+ @typing.final
+ class Mechanical(google.protobuf.message.Message):
+ """*
+ Mechanical Properties Set Definition for Simulation.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ YOUNG_MOD_FIELD_NUMBER: builtins.int
+ POISSON_RATIO_FIELD_NUMBER: builtins.int
+ SHEAR_MOD_FIELD_NUMBER: builtins.int
+ DENSITY_FIELD_NUMBER: builtins.int
+ DAMPING_COEFFICIENT_FIELD_NUMBER: builtins.int
+ young_mod: builtins.float
+ """naming scheme changes here
+ / GPa
+ """
+ poisson_ratio: builtins.float
+ """/ ?"""
+ shear_mod: builtins.float
+ """/ MPa"""
+ density: builtins.float
+ """/ g/cm^3"""
+ damping_coefficient: builtins.float
+ """/ ?"""
+ def __init__(
+ self,
+ *,
+ young_mod: builtins.float = ...,
+ poisson_ratio: builtins.float = ...,
+ shear_mod: builtins.float = ...,
+ density: builtins.float = ...,
+ damping_coefficient: builtins.float = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["damping_coefficient", b"damping_coefficient", "density", b"density", "poisson_ratio", b"poisson_ratio", "shear_mod", b"shear_mod", "young_mod", b"young_mod"]) -> None: ...
+
+ @typing.final
+ class Strength(google.protobuf.message.Message):
+ """*
+ Strength Properties Set Definition for Simulation.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ YIELD_STRENGTH_FIELD_NUMBER: builtins.int
+ TENSILE_STRENGTH_FIELD_NUMBER: builtins.int
+ THERMAL_TREATMENT_FIELD_NUMBER: builtins.int
+ yield_strength: builtins.float
+ """/ MPa"""
+ tensile_strength: builtins.float
+ """/ MPa"""
+ thermal_treatment: builtins.bool
+ """/ yes / no"""
+ def __init__(
+ self,
+ *,
+ yield_strength: builtins.float = ...,
+ tensile_strength: builtins.float = ...,
+ thermal_treatment: builtins.bool = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["tensile_strength", b"tensile_strength", "thermal_treatment", b"thermal_treatment", "yield_strength", b"yield_strength"]) -> None: ...
+
+ INFO_FIELD_NUMBER: builtins.int
+ DESCRIPTION_FIELD_NUMBER: builtins.int
+ THERMAL_FIELD_NUMBER: builtins.int
+ MECHANICAL_FIELD_NUMBER: builtins.int
+ STRENGTH_FIELD_NUMBER: builtins.int
+ DYNAMIC_FRICTION_FIELD_NUMBER: builtins.int
+ STATIC_FRICTION_FIELD_NUMBER: builtins.int
+ RESTITUTION_FIELD_NUMBER: builtins.int
+ DEFORMABLE_FIELD_NUMBER: builtins.int
+ MATTYPE_FIELD_NUMBER: builtins.int
+ description: builtins.str
+ """/ short description of physical material"""
+ dynamic_friction: builtins.float
+ """/ Frictional force for dampening - Interpolate (0-1)"""
+ static_friction: builtins.float
+ """/ Frictional force override at stop - Interpolate (0-1)"""
+ restitution: builtins.float
+ """/ Restitution of the object - Interpolate (0-1)"""
+ deformable: builtins.bool
+ """/ should this object deform when encountering large forces - TODO: This needs a proper message and equation field"""
+ matType: global___PhysicalMaterial.MaterialType.ValueType
+ """/ generic type to assign some default params"""
+ @property
+ def info(self) -> types_pb2.Info:
+ """/ Identifiable information (id, name, version, etc)"""
+
+ @property
+ def thermal(self) -> global___PhysicalMaterial.Thermal:
+ """/ Thermal Physical properties of the model OPTIONAL"""
+
+ @property
+ def mechanical(self) -> global___PhysicalMaterial.Mechanical:
+ """/ Mechanical properties of the model OPTIONAL"""
+
+ @property
+ def strength(self) -> global___PhysicalMaterial.Strength:
+ """/ Physical Strength properties of the model OPTIONAL"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ description: builtins.str = ...,
+ thermal: global___PhysicalMaterial.Thermal | None = ...,
+ mechanical: global___PhysicalMaterial.Mechanical | None = ...,
+ strength: global___PhysicalMaterial.Strength | None = ...,
+ dynamic_friction: builtins.float = ...,
+ static_friction: builtins.float = ...,
+ restitution: builtins.float = ...,
+ deformable: builtins.bool = ...,
+ matType: global___PhysicalMaterial.MaterialType.ValueType = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["info", b"info", "mechanical", b"mechanical", "strength", b"strength", "thermal", b"thermal"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["deformable", b"deformable", "description", b"description", "dynamic_friction", b"dynamic_friction", "info", b"info", "matType", b"matType", "mechanical", b"mechanical", "restitution", b"restitution", "static_friction", b"static_friction", "strength", b"strength", "thermal", b"thermal"]) -> None: ...
+
+global___PhysicalMaterial = PhysicalMaterial
diff --git a/exporter/SynthesisFusionAddin/src/Proto/motor_pb2.pyi b/exporter/SynthesisFusionAddin/src/Proto/motor_pb2.pyi
new file mode 100644
index 0000000000..6131aeb69f
--- /dev/null
+++ b/exporter/SynthesisFusionAddin/src/Proto/motor_pb2.pyi
@@ -0,0 +1,203 @@
+"""
+@generated by mypy-protobuf. Do not edit manually!
+isort:skip_file
+"""
+
+import builtins
+import google.protobuf.descriptor
+import google.protobuf.internal.enum_type_wrapper
+import google.protobuf.message
+import sys
+import types_pb2
+import typing
+
+if sys.version_info >= (3, 10):
+ import typing as typing_extensions
+else:
+ import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class _DutyCycles:
+ ValueType = typing.NewType("ValueType", builtins.int)
+ V: typing_extensions.TypeAlias = ValueType
+
+class _DutyCyclesEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_DutyCycles.ValueType], builtins.type):
+ DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+ CONTINUOUS_RUNNING: _DutyCycles.ValueType # 0
+ """/ S1"""
+ SHORT_TIME: _DutyCycles.ValueType # 1
+ """/ S2"""
+ INTERMITTENT_PERIODIC: _DutyCycles.ValueType # 2
+ """/ S3"""
+ CONTINUOUS_PERIODIC: _DutyCycles.ValueType # 3
+ """/ S6 Continuous Operation with Periodic Duty"""
+
+class DutyCycles(_DutyCycles, metaclass=_DutyCyclesEnumTypeWrapper):
+ """*
+ Duty Cycles for electric motors
+ Affects the dynamic output of the motor
+ https://www.news.benevelli-group.com/index.php/en/88-what-motor-duty-cycle.html
+ These each have associated data we are not going to use right now
+ """
+
+CONTINUOUS_RUNNING: DutyCycles.ValueType # 0
+"""/ S1"""
+SHORT_TIME: DutyCycles.ValueType # 1
+"""/ S2"""
+INTERMITTENT_PERIODIC: DutyCycles.ValueType # 2
+"""/ S3"""
+CONTINUOUS_PERIODIC: DutyCycles.ValueType # 3
+"""/ S6 Continuous Operation with Periodic Duty"""
+global___DutyCycles = DutyCycles
+
+@typing.final
+class Motor(google.protobuf.message.Message):
+ """*
+ A Motor should determine the relationship between an input and joint motion
+ Could represent something like a DC Motor relationship
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ INFO_FIELD_NUMBER: builtins.int
+ DC_MOTOR_FIELD_NUMBER: builtins.int
+ SIMPLE_MOTOR_FIELD_NUMBER: builtins.int
+ @property
+ def info(self) -> types_pb2.Info: ...
+ @property
+ def dc_motor(self) -> global___DCMotor: ...
+ @property
+ def simple_motor(self) -> global___SimpleMotor: ...
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ dc_motor: global___DCMotor | None = ...,
+ simple_motor: global___SimpleMotor | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["dc_motor", b"dc_motor", "info", b"info", "motor_type", b"motor_type", "simple_motor", b"simple_motor"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["dc_motor", b"dc_motor", "info", b"info", "motor_type", b"motor_type", "simple_motor", b"simple_motor"]) -> None: ...
+ def WhichOneof(self, oneof_group: typing.Literal["motor_type", b"motor_type"]) -> typing.Literal["dc_motor", "simple_motor"] | None: ...
+
+global___Motor = Motor
+
+@typing.final
+class SimpleMotor(google.protobuf.message.Message):
+ """*
+ SimpleMotor Configuration
+ Very easy motor used to simulate joints without specifying a real motor
+ Can set braking_constant - stall_torque - and max_velocity
+ Assumes you are solving using a velocity constraint for a joint and not a acceleration constraint
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ STALL_TORQUE_FIELD_NUMBER: builtins.int
+ MAX_VELOCITY_FIELD_NUMBER: builtins.int
+ BRAKING_CONSTANT_FIELD_NUMBER: builtins.int
+ stall_torque: builtins.float
+ """/ Torque at 0 rpm with a inverse linear relationship to max_velocity"""
+ max_velocity: builtins.float
+ """/ The target velocity in RPM, will use stall_torque relationship to reach each step"""
+ braking_constant: builtins.float
+ """/ (Optional) 0 - 1, the relationship of stall_torque used to perserve the position of this motor"""
+ def __init__(
+ self,
+ *,
+ stall_torque: builtins.float = ...,
+ max_velocity: builtins.float = ...,
+ braking_constant: builtins.float = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["braking_constant", b"braking_constant", "max_velocity", b"max_velocity", "stall_torque", b"stall_torque"]) -> None: ...
+
+global___SimpleMotor = SimpleMotor
+
+@typing.final
+class DCMotor(google.protobuf.message.Message):
+ """*
+ DCMotor Configuration
+ Parameters to simulate a DC Electric Motor
+ Still needs some more but overall they are most of the parameters we can use
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ @typing.final
+ class Advanced(google.protobuf.message.Message):
+ """/ Information usually found on datasheet"""
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ FREE_CURRENT_FIELD_NUMBER: builtins.int
+ FREE_SPEED_FIELD_NUMBER: builtins.int
+ STALL_CURRENT_FIELD_NUMBER: builtins.int
+ STALL_TORQUE_FIELD_NUMBER: builtins.int
+ INPUT_VOLTAGE_FIELD_NUMBER: builtins.int
+ RESISTANCE_VARIATION_FIELD_NUMBER: builtins.int
+ free_current: builtins.float
+ """/ measured in AMPs"""
+ free_speed: builtins.int
+ """/ measured in RPM"""
+ stall_current: builtins.float
+ """/ measure in AMPs"""
+ stall_torque: builtins.float
+ """/ measured in Nm"""
+ input_voltage: builtins.int
+ """/ measured in Volts DC"""
+ resistance_variation: builtins.float
+ """/ between (K * (N / 4)) and (K * ((N-2) / 4)) where N is number of poles - leave at 0 if unknown"""
+ def __init__(
+ self,
+ *,
+ free_current: builtins.float = ...,
+ free_speed: builtins.int = ...,
+ stall_current: builtins.float = ...,
+ stall_torque: builtins.float = ...,
+ input_voltage: builtins.int = ...,
+ resistance_variation: builtins.float = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["free_current", b"free_current", "free_speed", b"free_speed", "input_voltage", b"input_voltage", "resistance_variation", b"resistance_variation", "stall_current", b"stall_current", "stall_torque", b"stall_torque"]) -> None: ...
+
+ REFERENCE_URL_FIELD_NUMBER: builtins.int
+ TORQUE_CONSTANT_FIELD_NUMBER: builtins.int
+ EMF_CONSTANT_FIELD_NUMBER: builtins.int
+ RESISTANCE_FIELD_NUMBER: builtins.int
+ MAXIMUM_EFFECIENCY_FIELD_NUMBER: builtins.int
+ MAXIMUM_POWER_FIELD_NUMBER: builtins.int
+ DUTY_CYCLE_FIELD_NUMBER: builtins.int
+ ADVANCED_FIELD_NUMBER: builtins.int
+ reference_url: builtins.str
+ """/ Reference for purchase page or spec sheet"""
+ torque_constant: builtins.float
+ """/ m-Nm/Amp"""
+ emf_constant: builtins.float
+ """/ mV/rad/sec"""
+ resistance: builtins.float
+ """/ Resistance of Motor - Optional if other values are known"""
+ maximum_effeciency: builtins.int
+ """/ measure in percentage of 100 - generally around 60 - measured under optimal load"""
+ maximum_power: builtins.int
+ """/ measured in Watts"""
+ duty_cycle: global___DutyCycles.ValueType
+ """/ Stated Duty Cycle of motor"""
+ @property
+ def advanced(self) -> global___DCMotor.Advanced:
+ """/ Optional data that can give a better relationship to the simulation"""
+
+ def __init__(
+ self,
+ *,
+ reference_url: builtins.str = ...,
+ torque_constant: builtins.float = ...,
+ emf_constant: builtins.float = ...,
+ resistance: builtins.float = ...,
+ maximum_effeciency: builtins.int = ...,
+ maximum_power: builtins.int = ...,
+ duty_cycle: global___DutyCycles.ValueType = ...,
+ advanced: global___DCMotor.Advanced | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["advanced", b"advanced"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["advanced", b"advanced", "duty_cycle", b"duty_cycle", "emf_constant", b"emf_constant", "maximum_effeciency", b"maximum_effeciency", "maximum_power", b"maximum_power", "reference_url", b"reference_url", "resistance", b"resistance", "torque_constant", b"torque_constant"]) -> None: ...
+
+global___DCMotor = DCMotor
diff --git a/exporter/SynthesisFusionAddin/src/Proto/signal_pb2.pyi b/exporter/SynthesisFusionAddin/src/Proto/signal_pb2.pyi
new file mode 100644
index 0000000000..0befac8035
--- /dev/null
+++ b/exporter/SynthesisFusionAddin/src/Proto/signal_pb2.pyi
@@ -0,0 +1,159 @@
+"""
+@generated by mypy-protobuf. Do not edit manually!
+isort:skip_file
+"""
+
+import builtins
+import collections.abc
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.internal.enum_type_wrapper
+import google.protobuf.message
+import sys
+import types_pb2
+import typing
+
+if sys.version_info >= (3, 10):
+ import typing as typing_extensions
+else:
+ import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class _IOType:
+ ValueType = typing.NewType("ValueType", builtins.int)
+ V: typing_extensions.TypeAlias = ValueType
+
+class _IOTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_IOType.ValueType], builtins.type):
+ DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+ INPUT: _IOType.ValueType # 0
+ """/ Input Signal"""
+ OUTPUT: _IOType.ValueType # 1
+ """/ Output Signal"""
+
+class IOType(_IOType, metaclass=_IOTypeEnumTypeWrapper):
+ """*
+ IOType is a way to specify Input or Output.
+ """
+
+INPUT: IOType.ValueType # 0
+"""/ Input Signal"""
+OUTPUT: IOType.ValueType # 1
+"""/ Output Signal"""
+global___IOType = IOType
+
+class _DeviceType:
+ ValueType = typing.NewType("ValueType", builtins.int)
+ V: typing_extensions.TypeAlias = ValueType
+
+class _DeviceTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_DeviceType.ValueType], builtins.type):
+ DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+ PWM: _DeviceType.ValueType # 0
+ Digital: _DeviceType.ValueType # 1
+ Analog: _DeviceType.ValueType # 2
+ I2C: _DeviceType.ValueType # 3
+ CANBUS: _DeviceType.ValueType # 4
+ CUSTOM: _DeviceType.ValueType # 5
+
+class DeviceType(_DeviceType, metaclass=_DeviceTypeEnumTypeWrapper):
+ """*
+ DeviceType needs to be a type of device that has a supported connection
+ As well as a signal frmae but that can come later
+ """
+
+PWM: DeviceType.ValueType # 0
+Digital: DeviceType.ValueType # 1
+Analog: DeviceType.ValueType # 2
+I2C: DeviceType.ValueType # 3
+CANBUS: DeviceType.ValueType # 4
+CUSTOM: DeviceType.ValueType # 5
+global___DeviceType = DeviceType
+
+@typing.final
+class Signals(google.protobuf.message.Message):
+ """*
+ Signals is a container for all of the potential signals.
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ @typing.final
+ class SignalMapEntry(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ KEY_FIELD_NUMBER: builtins.int
+ VALUE_FIELD_NUMBER: builtins.int
+ key: builtins.str
+ @property
+ def value(self) -> global___Signal: ...
+ def __init__(
+ self,
+ *,
+ key: builtins.str = ...,
+ value: global___Signal | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ...
+
+ INFO_FIELD_NUMBER: builtins.int
+ SIGNAL_MAP_FIELD_NUMBER: builtins.int
+ @property
+ def info(self) -> types_pb2.Info:
+ """/ Has identifiable data (id, name, version)"""
+
+ @property
+ def signal_map(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Signal]:
+ """/ Contains a full collection of symbols"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ signal_map: collections.abc.Mapping[builtins.str, global___Signal] | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["info", b"info"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["info", b"info", "signal_map", b"signal_map"]) -> None: ...
+
+global___Signals = Signals
+
+@typing.final
+class Signal(google.protobuf.message.Message):
+ """*
+ Signal is a way to define a controlling signal.
+
+ TODO: Add Origin
+ TODO: Decide how this is linked to a exported object
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ INFO_FIELD_NUMBER: builtins.int
+ IO_FIELD_NUMBER: builtins.int
+ CUSTOM_TYPE_FIELD_NUMBER: builtins.int
+ SIGNAL_ID_FIELD_NUMBER: builtins.int
+ DEVICE_TYPE_FIELD_NUMBER: builtins.int
+ io: global___IOType.ValueType
+ """/ Is this a Input or Output"""
+ custom_type: builtins.str
+ """/ The name of a custom input type that is not listed as a device type"""
+ signal_id: builtins.int
+ """/ ID for a given signal that exists... PWM 2, CANBUS 4"""
+ device_type: global___DeviceType.ValueType
+ """/ Enum for device type that should always be set"""
+ @property
+ def info(self) -> types_pb2.Info:
+ """/ Has identifiable data (id, name, version)"""
+
+ def __init__(
+ self,
+ *,
+ info: types_pb2.Info | None = ...,
+ io: global___IOType.ValueType = ...,
+ custom_type: builtins.str = ...,
+ signal_id: builtins.int = ...,
+ device_type: global___DeviceType.ValueType = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["info", b"info"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["custom_type", b"custom_type", "device_type", b"device_type", "info", b"info", "io", b"io", "signal_id", b"signal_id"]) -> None: ...
+
+global___Signal = Signal
diff --git a/exporter/SynthesisFusionAddin/src/Proto/types_pb2.pyi b/exporter/SynthesisFusionAddin/src/Proto/types_pb2.pyi
new file mode 100644
index 0000000000..4463567347
--- /dev/null
+++ b/exporter/SynthesisFusionAddin/src/Proto/types_pb2.pyi
@@ -0,0 +1,315 @@
+"""
+@generated by mypy-protobuf. Do not edit manually!
+isort:skip_file
+Common data type implementations
+Intended to be re-used
+"""
+
+import builtins
+import collections.abc
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.internal.enum_type_wrapper
+import google.protobuf.message
+import sys
+import typing
+
+if sys.version_info >= (3, 10):
+ import typing as typing_extensions
+else:
+ import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class _Axis:
+ ValueType = typing.NewType("ValueType", builtins.int)
+ V: typing_extensions.TypeAlias = ValueType
+
+class _AxisEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_Axis.ValueType], builtins.type):
+ DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+ X: _Axis.ValueType # 0
+ Y: _Axis.ValueType # 1
+ Z: _Axis.ValueType # 2
+
+class Axis(_Axis, metaclass=_AxisEnumTypeWrapper):
+ """Axis Enum"""
+
+X: Axis.ValueType # 0
+Y: Axis.ValueType # 1
+Z: Axis.ValueType # 2
+global___Axis = Axis
+
+@typing.final
+class Node(google.protobuf.message.Message):
+ """Each proper object within the Graph - First one is Root"""
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ VALUE_FIELD_NUMBER: builtins.int
+ CHILDREN_FIELD_NUMBER: builtins.int
+ USER_DATA_FIELD_NUMBER: builtins.int
+ value: builtins.str
+ """/ the reference ID for whatever kind of graph this is"""
+ @property
+ def children(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Node]:
+ """/ the children for the given leaf"""
+
+ @property
+ def user_data(self) -> global___UserData:
+ """/ other associated data that can be used"""
+
+ def __init__(
+ self,
+ *,
+ value: builtins.str = ...,
+ children: collections.abc.Iterable[global___Node] | None = ...,
+ user_data: global___UserData | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["user_data", b"user_data"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["children", b"children", "user_data", b"user_data", "value", b"value"]) -> None: ...
+
+global___Node = Node
+
+@typing.final
+class GraphContainer(google.protobuf.message.Message):
+ """Top level GraphContainer
+ Contains all Graph element roots within
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ NODES_FIELD_NUMBER: builtins.int
+ @property
+ def nodes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Node]:
+ """represents the root of each seperate assembly - most of the time 1 node"""
+
+ def __init__(
+ self,
+ *,
+ nodes: collections.abc.Iterable[global___Node] | None = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["nodes", b"nodes"]) -> None: ...
+
+global___GraphContainer = GraphContainer
+
+@typing.final
+class UserData(google.protobuf.message.Message):
+ """*
+ UserData
+
+ Arbitrary data to append to a given message in map form
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ @typing.final
+ class DataEntry(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ KEY_FIELD_NUMBER: builtins.int
+ VALUE_FIELD_NUMBER: builtins.int
+ key: builtins.str
+ value: builtins.str
+ def __init__(
+ self,
+ *,
+ key: builtins.str = ...,
+ value: builtins.str = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ...
+
+ DATA_FIELD_NUMBER: builtins.int
+ @property
+ def data(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]:
+ """/ e.g. data["wheel"] = "yes" """
+
+ def __init__(
+ self,
+ *,
+ data: collections.abc.Mapping[builtins.str, builtins.str] | None = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["data", b"data"]) -> None: ...
+
+global___UserData = UserData
+
+@typing.final
+class Vector3(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ X_FIELD_NUMBER: builtins.int
+ Y_FIELD_NUMBER: builtins.int
+ Z_FIELD_NUMBER: builtins.int
+ x: builtins.float
+ y: builtins.float
+ z: builtins.float
+ def __init__(
+ self,
+ *,
+ x: builtins.float = ...,
+ y: builtins.float = ...,
+ z: builtins.float = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["x", b"x", "y", b"y", "z", b"z"]) -> None: ...
+
+global___Vector3 = Vector3
+
+@typing.final
+class PhysicalProperties(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ DENSITY_FIELD_NUMBER: builtins.int
+ MASS_FIELD_NUMBER: builtins.int
+ VOLUME_FIELD_NUMBER: builtins.int
+ AREA_FIELD_NUMBER: builtins.int
+ COM_FIELD_NUMBER: builtins.int
+ density: builtins.float
+ """/ kg per cubic cm kg/(cm^3)"""
+ mass: builtins.float
+ """/ kg"""
+ volume: builtins.float
+ """/ cm^3"""
+ area: builtins.float
+ """/ cm^2"""
+ @property
+ def com(self) -> global___Vector3:
+ """/ non-negative? Vec3"""
+
+ def __init__(
+ self,
+ *,
+ density: builtins.float = ...,
+ mass: builtins.float = ...,
+ volume: builtins.float = ...,
+ area: builtins.float = ...,
+ com: global___Vector3 | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["com", b"com"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["area", b"area", "com", b"com", "density", b"density", "mass", b"mass", "volume", b"volume"]) -> None: ...
+
+global___PhysicalProperties = PhysicalProperties
+
+@typing.final
+class Transform(google.protobuf.message.Message):
+ """*
+ Transform
+
+ Data needed to apply scale, position, and rotational changes
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ SPATIAL_MATRIX_FIELD_NUMBER: builtins.int
+ @property
+ def spatial_matrix(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]:
+ """
+ flat map of 4x4 transform matrix
+ [00][01][02][03][10][11][12][13][20][21][22][23]
+ """
+
+ def __init__(
+ self,
+ *,
+ spatial_matrix: collections.abc.Iterable[builtins.float] | None = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["spatial_matrix", b"spatial_matrix"]) -> None: ...
+
+global___Transform = Transform
+
+@typing.final
+class Color(google.protobuf.message.Message):
+ """RGBA in expanded form 0-255"""
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ R_FIELD_NUMBER: builtins.int
+ G_FIELD_NUMBER: builtins.int
+ B_FIELD_NUMBER: builtins.int
+ A_FIELD_NUMBER: builtins.int
+ R: builtins.int
+ """red"""
+ G: builtins.int
+ """green"""
+ B: builtins.int
+ """blue"""
+ A: builtins.int
+ """alpha"""
+ def __init__(
+ self,
+ *,
+ R: builtins.int = ...,
+ G: builtins.int = ...,
+ B: builtins.int = ...,
+ A: builtins.int = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["A", b"A", "B", b"B", "G", b"G", "R", b"R"]) -> None: ...
+
+global___Color = Color
+
+@typing.final
+class Info(google.protobuf.message.Message):
+ """*
+ Defines basic fields for almost all objects
+ The location where you can access the GUID for a reference
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ GUID_FIELD_NUMBER: builtins.int
+ NAME_FIELD_NUMBER: builtins.int
+ VERSION_FIELD_NUMBER: builtins.int
+ GUID: builtins.str
+ """GUID unique value - must always be defined
+ since guid's have exactly 128bits could be represented with bytes[]
+ however endian becomes an issue
+ """
+ name: builtins.str
+ """Generic readable name"""
+ version: builtins.int
+ """Version of object iteration"""
+ def __init__(
+ self,
+ *,
+ GUID: builtins.str = ...,
+ name: builtins.str = ...,
+ version: builtins.int = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["GUID", b"GUID", "name", b"name", "version", b"version"]) -> None: ...
+
+global___Info = Info
+
+@typing.final
+class Thumbnail(google.protobuf.message.Message):
+ """*
+ A basic Thumbnail to be encoded in the file
+ Most of the Time Fusion can encode the file with transparency as PNG not bitmap
+ """
+
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ WIDTH_FIELD_NUMBER: builtins.int
+ HEIGHT_FIELD_NUMBER: builtins.int
+ EXTENSION_FIELD_NUMBER: builtins.int
+ TRANSPARENT_FIELD_NUMBER: builtins.int
+ DATA_FIELD_NUMBER: builtins.int
+ width: builtins.int
+ """/ Image Width"""
+ height: builtins.int
+ """/ Image Height"""
+ extension: builtins.str
+ """/ Image Extension - ex. (.png, .bitmap, .jpeg)"""
+ transparent: builtins.bool
+ """/ Transparency - true from fusion when correctly configured"""
+ data: builtins.bytes
+ """/ Data as read from the file in bytes[] form"""
+ def __init__(
+ self,
+ *,
+ width: builtins.int = ...,
+ height: builtins.int = ...,
+ extension: builtins.str = ...,
+ transparent: builtins.bool = ...,
+ data: builtins.bytes = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["data", b"data", "extension", b"extension", "height", b"height", "transparent", b"transparent", "width", b"width"]) -> None: ...
+
+global___Thumbnail = Thumbnail
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 1aca3a5162..166bc43976 100644
--- a/exporter/SynthesisFusionAddin/src/Types.py
+++ b/exporter/SynthesisFusionAddin/src/Types.py
@@ -1,9 +1,11 @@
import os
import pathlib
import platform
-from dataclasses import dataclass, field, fields, is_dataclass
+from dataclasses import MISSING, dataclass, field, fields, is_dataclass
from enum import Enum, EnumType
-from typing import Union, get_origin
+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"])
@@ -11,24 +13,32 @@
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
class Wheel:
- jointToken: str = field(default=None)
- wheelType: WheelType = field(default=None)
- signalType: SignalType = field(default=None)
+ jointToken: str = field(default="")
+ wheelType: WheelType = field(default=WheelType.STANDARD)
+ signalType: SignalType = field(default=SignalType.PWM)
@dataclass
class Joint:
- jointToken: str = field(default=None)
- parent: JointParentType = field(default=None)
- signalType: SignalType = field(default=None)
- speed: float = field(default=None)
- force: float = field(default=None)
+ jointToken: str = field(default="")
+ parent: JointParentType = field(default=JointParentType.ROOT)
+ signalType: SignalType = field(default=SignalType.PWM)
+ speed: float = field(default=float("-inf"))
+ force: float = field(default=float("-inf"))
# Transition: AARD-1865
# Should consider changing how the parser handles wheels and joints as there is overlap between
@@ -39,9 +49,9 @@ class Joint:
@dataclass
class Gamepiece:
- occurrenceToken: str = field(default=None)
- weight: float = field(default=None)
- friction: float = field(default=None)
+ occurrenceToken: str = field(default="")
+ weight: float = field(default=float("-inf"))
+ friction: float = field(default=float("-inf"))
class PhysicalDepth(Enum):
@@ -72,26 +82,12 @@ class ModelHierarchy(Enum):
SingleMesh = 3
-class LBS(float):
- """Mass Unit in Pounds."""
-
-
-class KG(float):
- """Mass Unit in Kilograms."""
-
-
-def toLbs(kgs: float) -> LBS:
- return LBS(round(kgs * 2.2062, 2))
-
-
-def toKg(pounds: float) -> KG:
- return KG(round(pounds / 2.2062, 2))
-
-
+KG: TypeAlias = float
+LBS: TypeAlias = float
PRIMITIVES = (bool, str, int, float, type(None))
-def encodeNestedObjects(obj: any) -> any:
+def encodeNestedObjects(obj: Any) -> Any:
if isinstance(obj, Enum):
return obj.value
elif hasattr(obj, "__dict__"):
@@ -101,13 +97,13 @@ def encodeNestedObjects(obj: any) -> any:
return obj
-def makeObjectFromJson(objType: type, data: any) -> any:
+def makeObjectFromJson(objType: type, data: Any) -> Any:
if isinstance(objType, EnumType):
return objType(data)
elif isinstance(objType, PRIMITIVES) or isinstance(data, PRIMITIVES):
return data
elif get_origin(objType) is list:
- return [makeObjectFromJson(objType.__args__[0], item) for item in data]
+ return [makeObjectFromJson(get_args(objType)[0], item) for item in data]
obj = objType()
assert is_dataclass(obj) and isinstance(data, dict), "Found unsupported type to decode."
@@ -115,13 +111,13 @@ def makeObjectFromJson(objType: type, data: any) -> any:
if field.name in data:
setattr(obj, field.name, makeObjectFromJson(field.type, data[field.name]))
else:
- setattr(obj, field.name, field.default)
+ setattr(obj, field.name, field.default_factory if field.default_factory is not MISSING else field.default)
return obj
class OString:
- def __init__(self, path: object, fileName: str):
+ def __init__(self, path: str | os.PathLike[str] | list[str], fileName: str):
"""Generate a string for the operating system that matches fusion requirements
Args:
@@ -142,7 +138,7 @@ def __repr__(self) -> str:
str: OString [ - ['test', 'test2] - 'test.hell' ]
"""
# return f"OString [\n-\t[{self.literals!r} \n-\t{self.fileName}\n]"
- return f"{os.path.join(self.path, self.fileName)}"
+ return f"{os.path.join(str(self.path), self.fileName)}"
def __eq__(self, value: object) -> bool:
"""Equals operator for this class
@@ -179,7 +175,7 @@ def _os() -> str:
else:
raise OSError(2, "No Operating System Recognized", f"{osName}")
- def AssertEquals(self, comparing: object):
+ def AssertEquals(self, comparing: object) -> bool:
"""Compares the two OString objects
Args:
@@ -190,21 +186,21 @@ def AssertEquals(self, comparing: object):
"""
return comparing == self
- def getPath(self) -> Union[str, object]:
+ def getPath(self) -> str | os.PathLike[str]:
"""Returns a OSPath from literals and filename
Returns:
Path | str: OsPath that is cross platform
"""
- return os.path.join(self.path, self.fileName)
+ return os.path.join(str(self.path), self.fileName)
- def getDirectory(self) -> Union[str, object]:
+ def getDirectory(self) -> str | os.PathLike[str]:
"""Returns a OSPath from literals and filename
Returns:
Path | str: OsPath that is cross platform
"""
- return self.path
+ return self.path if not isinstance(self.path, list) else "".join(self.path)
def exists(self) -> bool:
"""Check to see if Directory and File exist in the current system
@@ -216,7 +212,7 @@ def exists(self) -> bool:
return True
return False
- def serialize(self) -> str:
+ def serialize(self) -> str | os.PathLike[str]:
"""Serialize the OString to be storred in a temp doc
Returns:
@@ -225,7 +221,7 @@ def serialize(self) -> str:
return self.getPath()
@classmethod
- def deserialize(cls, serialized) -> object:
+ def deserialize(cls, serialized: str | os.PathLike[str]) -> object:
path, file = os.path.split(serialized)
if path is None or file is None:
raise RuntimeError(f"Can not parse OString Path supplied \n {serialized}")
@@ -273,7 +269,7 @@ def AppDataPath(cls, fileName: str) -> object:
"""
if cls._os() == "Windows":
if os.getenv("APPDATA") is not None:
- path = os.path.join(os.getenv("APPDATA"), "..", "Local", "Temp")
+ path = os.path.join(os.getenv("APPDATA") or "", "..", "Local", "Temp")
return cls(path, fileName)
return None
diff --git a/exporter/SynthesisFusionAddin/src/UI/Camera.py b/exporter/SynthesisFusionAddin/src/UI/Camera.py
index 02de04083a..53168fd24d 100644
--- a/exporter/SynthesisFusionAddin/src/UI/Camera.py
+++ b/exporter/SynthesisFusionAddin/src/UI/Camera.py
@@ -2,12 +2,14 @@
import adsk.core
+from src import SUPPORT_PATH
from src.Logging import logFailure
from src.Types import OString
+from src.Util import makeDirectories
@logFailure
-def captureThumbnail(size=250):
+def captureThumbnail(size: int = 250) -> str | os.PathLike[str]:
"""
## Captures Thumbnail and saves it to a temporary path - needs to be cleared after or on startup
- Size: int (Default: 200) : (width & height)
@@ -21,9 +23,10 @@ def captureThumbnail(size=250):
) # remove whitespace from just the filename
)
- path = OString.ThumbnailPath(name)
+ path = makeDirectories(f"{SUPPORT_PATH}/Resources/Icons/")
+ path += name
- saveOptions = adsk.core.SaveImageFileOptions.create(str(path.getPath()))
+ saveOptions = adsk.core.SaveImageFileOptions.create(path)
saveOptions.height = size
saveOptions.width = size
saveOptions.isAntiAliased = True
@@ -36,7 +39,7 @@ def captureThumbnail(size=250):
app.activeViewport.saveAsImageFileWithOptions(saveOptions)
app.activeViewport.camera = originalCamera
- return str(path.getPath())
+ return path
def clearIconCache() -> None:
@@ -44,7 +47,7 @@ def clearIconCache() -> None:
This is useful for now but should be cached in the event the app is closed and re-opened.
"""
- path = OString.ThumbnailPath("Whatever.png").getDirectory()
+ path = OString.ThumbnailPath("Whatever.png").getDirectory() # type: ignore[attr-defined]
for _r, _d, f in os.walk(path):
for file in f:
diff --git a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py
index c50c7feed2..b463302c5b 100644
--- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py
+++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py
@@ -5,6 +5,7 @@
import os
import pathlib
from enum import Enum
+from typing import Any
import adsk.core
import adsk.fusion
@@ -36,7 +37,9 @@
INPUTS_ROOT = None
-def GUID(arg):
+# Transition: AARD-1765
+# This should be removed in the config command refactor. Seemingly impossible to type.
+def GUID(arg: str | adsk.core.Base) -> str | adsk.core.Base:
"""### Will return command object when given a string GUID, or the string GUID of an object (depending on arg value)
Args:
@@ -49,7 +52,7 @@ def GUID(arg):
object = gm.app.activeDocument.design.findEntityByToken(arg)[0]
return object
else: # type(obj)
- return arg.entityToken
+ return arg.entityToken # type: ignore[union-attr]
class JointMotions(Enum):
@@ -76,14 +79,13 @@ class ConfigureCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
- will be called from (@ref Events.py)
"""
- def __init__(self, configure):
+ def __init__(self, configure: Any) -> None:
super().__init__()
@logFailure(messageBox=True)
- def notify(self, args):
- exporterOptions = ExporterOptions().readFromDesign()
- eventArgs = adsk.core.CommandCreatedEventArgs.cast(args)
- cmd = eventArgs.command # adsk.core.Command
+ def notify(self, args: adsk.core.CommandCreatedEventArgs) -> None:
+ exporterOptions = ExporterOptions().readFromDesign() or ExporterOptions()
+ cmd = args.command
# Set to false so won't automatically export on switch context
cmd.isAutoExecute = False
@@ -284,16 +286,15 @@ class ConfigureCommandExecuteHandler(adsk.core.CommandEventHandler):
"""
- def __init__(self):
+ def __init__(self) -> None:
super().__init__()
self.current = SerialCommand()
@logFailure(messageBox=True)
- def notify(self, args):
- eventArgs = adsk.core.CommandEventArgs.cast(args)
+ def notify(self, args: adsk.core.CommandEventArgs) -> None:
exporterOptions = ExporterOptions().readFromDesign()
- if eventArgs.executeFailed:
+ if args.executeFailed:
logger.error("Could not execute configuration due to failure")
return
@@ -301,39 +302,43 @@ def notify(self, args):
if generalConfigTab.exportLocation == ExportLocation.DOWNLOAD:
savepath = FileDialogConfig.saveFileDialog(defaultPath=exporterOptions.fileLocation)
- if savepath == False:
+ if not savepath:
# save was canceled
return
- updatedPath = pathlib.Path(savepath).parent
- if updatedPath != self.current.filePath:
- self.current.filePath = str(updatedPath)
+ # Transition: AARD-1742
+ # With the addition of a 'release' build the fusion exporter will not have permissions within the sourced
+ # folder. Because of this we cannot use this kind of tmp path anymore. This code was already unused and
+ # should be removed.
+ # updatedPath = pathlib.Path(savepath).parent
+ # if updatedPath != self.current.filePath:
+ # self.current.filePath = str(updatedPath)
else:
savepath = processedFileName
adsk.doEvents()
+
design = gm.app.activeDocument.design
- name = design.rootComponent.name.rsplit(" ", 1)[0]
- version = design.rootComponent.name.rsplit(" ", 1)[1]
+
+ name_split: list[str] = design.rootComponent.name.split(" ")
+ if len(name_split) < 2:
+ gm.ui.messageBox("Please open the robot design you would like to export", "Synthesis: Error")
+ return
+
+ name = name_split[0]
+ version = name_split[1]
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(
- savepath,
+ str(savepath),
name,
version,
materials=0,
joints=selectedJoints,
wheels=selectedWheels,
gamepieces=selectedGamepieces,
- preferredUnits=units,
robotWeight=generalConfigTab.robotWeight,
autoCalcRobotWeight=generalConfigTab.autoCalculateWeight,
autoCalcGamepieceWeight=gamepieceConfigTab.autoCalculateWeight,
@@ -362,12 +367,12 @@ class CommandExecutePreviewHandler(adsk.core.CommandEventHandler):
adsk (CommandEventHandler): Command event handler that a client derives from to handle events triggered by a CommandEvent.
"""
- def __init__(self, cmd) -> None:
+ def __init__(self, cmd: adsk.core.Command) -> None:
super().__init__()
self.cmd = cmd
@logFailure(messageBox=True)
- def notify(self, args):
+ def notify(self, args: adsk.core.CommandEventArgs) -> None:
"""Notify member called when a command event is triggered
Args:
@@ -386,22 +391,26 @@ class MySelectHandler(adsk.core.SelectionEventHandler):
lastInputCmd = None
- def __init__(self, cmd):
+ def __init__(self, cmd: adsk.core.Command) -> None:
super().__init__()
self.cmd = cmd
- self.allWheelPreselections = [] # all child occurrences of selections
- self.allGamepiecePreselections = [] # all child gamepiece occurrences of selections
+ # Transition: AARD-1765
+ # self.allWheelPreselections = [] # all child occurrences of selections
+ # self.allGamepiecePreselections = [] # all child gamepiece occurrences of selections
self.selectedOcc = None # selected occurrence (if there is one)
self.selectedJoint = None # selected joint (if there is one)
- self.wheelJointList = []
+ # Transition: AARD-1765
+ # self.wheelJointList = []
self.algorithmicSelection = True
@logFailure(messageBox=True)
def traverseAssembly(
- self, child_occurrences: adsk.fusion.OccurrenceList, jointedOcc: dict
+ self, child_occurrences: adsk.fusion.OccurrenceList, jointedOcc: dict[adsk.fusion.Joint, adsk.fusion.Occurrence]
+ ) -> (
+ list[adsk.fusion.Joint | adsk.fusion.Occurrence] | None
): # recursive traversal to check if children are jointed
"""### Traverses the entire occurrence hierarchy to find a match (jointed occurrence) in self.occurrence
@@ -422,7 +431,7 @@ def traverseAssembly(
return None # no jointed occurrence found
@logFailure(messageBox=True)
- def wheelParent(self, occ: adsk.fusion.Occurrence):
+ def wheelParent(self, occ: adsk.fusion.Occurrence) -> list[str | adsk.fusion.Occurrence | None]:
"""### Identify an occurrence that encompasses the entire wheel component.
Process:
@@ -490,7 +499,7 @@ def wheelParent(self, occ: adsk.fusion.Occurrence):
return [None, occ] # no jointed occurrence found, return what is selected
@logFailure(messageBox=True)
- def notify(self, args: adsk.core.SelectionEventArgs):
+ def notify(self, args: adsk.core.SelectionEventArgs) -> None:
"""### Notify member is called when a selection event is triggered.
Args:
@@ -512,12 +521,12 @@ class MyPreSelectHandler(adsk.core.SelectionEventHandler):
Args: SelectionEventHandler
"""
- def __init__(self, cmd):
+ def __init__(self, cmd: adsk.core.Command) -> None:
super().__init__()
self.cmd = cmd
@logFailure(messageBox=True)
- def notify(self, args):
+ def notify(self, args: adsk.core.SelectionEventArgs) -> None:
design = adsk.fusion.Design.cast(gm.app.activeProduct)
preSelectedOcc = adsk.fusion.Occurrence.cast(args.selection.entity)
preSelectedJoint = adsk.fusion.Joint.cast(args.selection.entity)
@@ -539,12 +548,12 @@ class MyPreselectEndHandler(adsk.core.SelectionEventHandler):
Args: SelectionEventArgs
"""
- def __init__(self, cmd):
+ def __init__(self, cmd: adsk.core.Command) -> None:
super().__init__()
self.cmd = cmd
@logFailure(messageBox=True)
- def notify(self, args):
+ def notify(self, args: adsk.core.SelectionEventArgs) -> None:
design = adsk.fusion.Design.cast(gm.app.activeProduct)
preSelectedOcc = adsk.fusion.Occurrence.cast(args.selection.entity)
preSelectedJoint = adsk.fusion.Joint.cast(args.selection.entity)
@@ -560,7 +569,7 @@ class ConfigureCommandInputChanged(adsk.core.InputChangedEventHandler):
Args: InputChangedEventHandler
"""
- def __init__(self, cmd):
+ def __init__(self, cmd: adsk.core.Command) -> None:
super().__init__()
self.cmd = cmd
self.allWeights = [None, None] # [lbs, kg]
@@ -568,7 +577,7 @@ def __init__(self, cmd):
self.isLbs_f = True
@logFailure
- def reset(self):
+ def reset(self) -> None:
"""### Process:
- Reset the mouse icon to default
- Clear active selections
@@ -576,13 +585,14 @@ def reset(self):
self.cmd.setCursor("", 0, 0)
gm.ui.activeSelections.clear()
- def notify(self, args):
- generalConfigTab.handleInputChanged(args)
+ def notify(self, args: adsk.core.InputChangedEventArgs) -> None:
+ if generalConfigTab.isActive:
+ generalConfigTab.handleInputChanged(args)
- if jointConfigTab.isVisible:
+ if jointConfigTab.isVisible and jointConfigTab.isActive:
jointConfigTab.handleInputChanged(args, INPUTS_ROOT)
- if gamepieceConfigTab.isVisible:
+ if gamepieceConfigTab.isVisible and gamepieceConfigTab.isActive:
gamepieceConfigTab.handleInputChanged(args, INPUTS_ROOT)
@@ -593,11 +603,8 @@ class MyCommandDestroyHandler(adsk.core.CommandEventHandler):
Args: CommandEventHandler
"""
- def __init__(self):
- super().__init__()
-
@logFailure(messageBox=True)
- def notify(self, args):
+ def notify(self, args: adsk.core.CommandEventArgs) -> None:
jointConfigTab.reset()
gamepieceConfigTab.reset()
diff --git a/exporter/SynthesisFusionAddin/src/UI/Configuration/SerialCommand.py b/exporter/SynthesisFusionAddin/src/UI/Configuration/SerialCommand.py
index 663afe9337..61c1e30108 100644
--- a/exporter/SynthesisFusionAddin/src/UI/Configuration/SerialCommand.py
+++ b/exporter/SynthesisFusionAddin/src/UI/Configuration/SerialCommand.py
@@ -10,6 +10,8 @@
from src.Types import OString
+# Transition: AARD-1765
+# Will likely be removed later as this is no longer used. Avoiding adding typing for now.
def generateFilePath() -> str:
"""Generates a temporary file path that can be used to save the file for exporting
@@ -19,24 +21,29 @@ def generateFilePath() -> str:
Returns:
str: file path
"""
- tempPath = OString.TempPath("").getPath()
+ tempPath = OString.TempPath("").getPath() # type: ignore
return str(tempPath)
class Struct:
"""For decoding the dict values into named values"""
- def __init__(self, **entries):
+ def __init__(self, **entries): # type: ignore
self.__dict__.update(entries)
class SerialCommand:
"""All of the command inputs combined"""
- def __init__(self):
+ def __init__(self): # type: ignore
self.general = General()
self.advanced = Advanced()
- self.filePath = generateFilePath()
+
+ # Transition: AARD-1742
+ # With the addition of a 'release' build the fusion exporter will not have permissions within the sourced
+ # folder. Because of this we cannot use this kind of tmp path anymore. This code was already unused and
+ # should be removed.
+ # self.filePath = generateFilePath()
def toJSON(self) -> str:
"""Converts this class into a json object that can be written to the object data
@@ -50,7 +57,7 @@ def toJSON(self) -> str:
class General:
"""General Options"""
- def __init__(self):
+ def __init__(self): # type: ignore
# This is the overall export decision point
self.exportMode = ExportMode.standard
self.RenderType = RenderType.basic3D
@@ -64,7 +71,7 @@ def __init__(self):
class Advanced:
"""Advanced settings in the command input"""
- def __init__(self):
+ def __init__(self): # type: ignore
self.friction = BooleanInput("friction", True)
self.density = BooleanInput("density", True)
self.mass = BooleanInput("mass", True)
diff --git a/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py b/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py
index 3b6bd8e2c2..58a331a198 100644
--- a/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py
+++ b/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py
@@ -6,7 +6,7 @@
@logFailure
-def createTextGraphics(wheel: adsk.fusion.Occurrence, _wheels) -> None:
+def createTextGraphics(wheel: adsk.fusion.Occurrence, _wheels: list[adsk.fusion.Occurrence]) -> None:
design = gm.app.activeDocument.design
boundingBox = wheel.boundingBox # occurrence bounding box
diff --git a/exporter/SynthesisFusionAddin/src/UI/Events.py b/exporter/SynthesisFusionAddin/src/UI/Events.py
index 64d0f1ea29..c8da4590a6 100644
--- a/exporter/SynthesisFusionAddin/src/UI/Events.py
+++ b/exporter/SynthesisFusionAddin/src/UI/Events.py
@@ -14,16 +14,12 @@
logger = getLogger()
-def updateDocument(*argv: Sequence[str]):
- pass
+def updateDocument(*argv: Sequence[str]) -> None: ...
-def updateConnection(_) -> str:
+def updateConnection() -> str:
"""Updates the JS side connection with the Network Manager connected()
- Args:
- _ (Any): Any
-
Returns:
str: Json formatted connected: true | false
"""
@@ -61,6 +57,8 @@ def openDocument(json_data: str) -> str:
return ""
-def example(palette):
+def example(palette: adsk.core.Palette) -> None:
app = adsk.core.Application.get()
- app.userInterface(f"{Helper.getDocName()}")
+ # Transition: AARD-1765
+ # Many many things in this file can be removed, this is just the part that typing can not be added to
+ # app.userInterface(f"{Helper.getDocName()}")
diff --git a/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py b/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py
index d2465ae29b..d7708bfec6 100644
--- a/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py
+++ b/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py
@@ -1,3 +1,7 @@
+import os
+import tempfile
+from pathlib import Path
+
import adsk.core
import adsk.fusion
@@ -5,7 +9,7 @@
from src.Types import OString
-def saveFileDialog(defaultPath: str | None = None, defaultName: str | None = None) -> str | bool:
+def saveFileDialog(defaultPath: str | None = None, defaultName: str | None = None) -> str | os.PathLike[str] | None:
"""Function to generate the Save File Dialog for the Hellion Data files
Args:
@@ -13,11 +17,11 @@ def saveFileDialog(defaultPath: str | None = None, defaultName: str | None = Non
defaultName (str): default name for the saving file
Returns:
- bool: False if canceled
+ None: if canceled
str: full file path
"""
- fileDialog: adsk.core.FileDialog = gm.ui.createFileDialog()
+ fileDialog = gm.ui.createFileDialog()
fileDialog.isMultiSelectEnabled = False
fileDialog.title = "Save Export Result"
@@ -36,11 +40,29 @@ def saveFileDialog(defaultPath: str | None = None, defaultName: str | None = Non
fileDialog.filterIndex = 0
dialogResult = fileDialog.showSave()
- if dialogResult == adsk.core.DialogResults.DialogOK:
- return fileDialog.filename
- else:
+ if dialogResult != adsk.core.DialogResults.DialogOK:
+ return None
+
+ canWrite = isWriteableDirectory(Path(fileDialog.filename).parent)
+ if not canWrite:
+ gm.ui.messageBox("Synthesis does not have the required permissions to write to this directory.")
+ return saveFileDialog(defaultPath, defaultName)
+
+ return fileDialog.filename or ""
+
+
+def isWriteableDirectory(path: str | os.PathLike[str]) -> bool:
+ if not os.access(path, os.W_OK):
+ return False
+
+ try:
+ with tempfile.NamedTemporaryFile(dir=path, delete=True) as f:
+ f.write(b"test")
+ except OSError:
return False
+ return True
+
def generateFilePath() -> str:
"""Generates a temporary file path that can be used to save the file for exporting
@@ -51,7 +73,9 @@ def generateFilePath() -> str:
Returns:
str: file path
"""
- tempPath = OString.TempPath("").getPath()
+ # Transition: AARD-1765
+ # Ignoring the type for now, will revisit in the OString refactor
+ tempPath = OString.TempPath("").getPath() # type: ignore
return str(tempPath)
@@ -74,5 +98,4 @@ def generateFileName() -> str:
return "{0}_{1}.mira".format(name, version)
-def OpenFileDialog():
- pass
+def OpenFileDialog() -> None: ...
diff --git a/exporter/SynthesisFusionAddin/src/UI/GamepieceConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/GamepieceConfigTab.py
index 8eb4f80044..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(
@@ -102,22 +96,22 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: Exp
@property
def isVisible(self) -> bool:
- return self.gamepieceConfigTab.isVisible
+ return self.gamepieceConfigTab.isVisible or False
@isVisible.setter
def isVisible(self, value: bool) -> None:
self.gamepieceConfigTab.isVisible = value
@property
- def selectedUnits(self) -> PreferredUnits:
- return self.currentUnits
+ def isActive(self) -> bool:
+ return self.gamepieceConfigTab.isActive or False
@property
def autoCalculateWeight(self) -> bool:
autoCalcWeightButton: adsk.core.BoolValueCommandInput = self.gamepieceConfigTab.children.itemById(
"autoCalcGamepieceWeight"
)
- return autoCalcWeightButton.value
+ return autoCalcWeightButton.value or False
@logFailure
def weightInputs(self) -> list[adsk.core.ValueCommandInput]:
@@ -164,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)
@@ -218,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))
@@ -228,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
@@ -243,15 +216,23 @@ 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(
self, args: adsk.core.InputChangedEventArgs, globalCommandInputs: adsk.core.CommandInputs
) -> None:
+ gamepieceAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("gamepieceAddButton")
+ gamepieceTable: adsk.core.TableCommandInput = args.inputs.itemById("gamepieceTable")
+ gamepieceRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("gamepieceRemoveButton")
+ gamepieceSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById(
+ "gamepieceSelectCancelButton"
+ )
+ gamepieceSelection: adsk.core.SelectionCommandInput = self.gamepieceConfigTab.children.itemById(
+ "gamepieceSelect"
+ )
+ spacer: adsk.core.SelectionCommandInput = self.gamepieceConfigTab.children.itemById("gamepieceTabSpacer")
+
commandInput = args.input
if commandInput.id == "autoCalcGamepieceWeight":
autoCalcWeightButton = adsk.core.BoolValueCommandInput.cast(commandInput)
@@ -268,33 +249,7 @@ 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":
- gamepieceAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("gamepieceAddButton")
- gamepieceRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById(
- "gamepieceRemoveButton"
- )
- gamepieceSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById(
- "gamepieceSelectCancelButton"
- )
- gamepieceSelection: adsk.core.SelectionCommandInput = self.gamepieceConfigTab.children.itemById(
- "gamepieceSelect"
- )
- spacer: adsk.core.SelectionCommandInput = self.gamepieceConfigTab.children.itemById("gamepieceTabSpacer")
-
gamepieceSelection.isVisible = gamepieceSelection.isEnabled = True
gamepieceSelection.clearSelection()
gamepieceAddButton.isEnabled = gamepieceRemoveButton.isEnabled = False
@@ -302,9 +257,6 @@ def handleInputChanged(
spacer.isVisible = False
elif commandInput.id == "gamepieceRemoveButton":
- gamepieceAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("gamepieceAddButton")
- gamepieceTable: adsk.core.TableCommandInput = args.inputs.itemById("gamepieceTable")
-
gamepieceAddButton.isEnabled = True
if gamepieceTable.selectedRow == -1 or gamepieceTable.selectedRow == 0:
ui = adsk.core.Application.get().userInterface
@@ -313,18 +265,6 @@ def handleInputChanged(
self.removeIndexedGamepiece(gamepieceTable.selectedRow - 1) # selectedRow is 1 indexed
elif commandInput.id == "gamepieceSelectCancelButton":
- gamepieceAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("gamepieceAddButton")
- gamepieceRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById(
- "gamepieceRemoveButton"
- )
- gamepieceSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById(
- "gamepieceSelectCancelButton"
- )
- gamepieceSelection: adsk.core.SelectionCommandInput = self.gamepieceConfigTab.children.itemById(
- "gamepieceSelect"
- )
- spacer: adsk.core.SelectionCommandInput = self.gamepieceConfigTab.children.itemById("gamepieceTabSpacer")
-
gamepieceSelection.isEnabled = gamepieceSelection.isVisible = False
gamepieceSelectCancelButton.isEnabled = gamepieceSelectCancelButton.isVisible = False
gamepieceAddButton.isEnabled = gamepieceRemoveButton.isEnabled = True
diff --git a/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py
index 40a602a406..977f5d9298 100644
--- a/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py
+++ b/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py
@@ -2,17 +2,17 @@
import adsk.fusion
from src.Logging import logFailure
-from src.Parser.ExporterOptions import (
- ExporterOptions,
- ExportLocation,
- ExportMode,
- PreferredUnits,
-)
-from src.Types import KG, toKg, toLbs
-from src.UI import IconPaths
-from src.UI.CreateCommandInputsHelper import createBooleanInput, createTableInput
+from src.Parser.ExporterOptions import ExporterOptions
+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:
@@ -21,7 +21,6 @@ class GeneralConfigTab:
previousFrictionOverrideCheckboxState: bool
previousSelectedUnitDropdownIndex: int
previousSelectedModeDropdownIndex: int
- currentUnits: PreferredUnits
jointConfigTab: JointConfigTab
gamepieceConfigTab: GamepieceConfigTab
@@ -49,7 +48,7 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: Exp
"exportLocation", "Export Location", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle
)
- upload: bool = exporterOptions.exportLocation == ExportLocation.UPLOAD
+ upload = exporterOptions.exportLocation == ExportLocation.UPLOAD
dropdownExportLocation.listItems.add("Upload", upload)
dropdownExportLocation.listItems.add("Download", not upload)
dropdownExportLocation.tooltip = "Export Location"
@@ -57,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",
@@ -80,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",
@@ -157,9 +113,13 @@ 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
+ def isActive(self) -> bool:
+ return self.generalOptionsTab.isActive or False
+
@property
def exportMode(self) -> ExportMode:
exportModeDropdown: adsk.core.DropDownCommandInput = self.generalOptionsTab.children.itemById(
@@ -176,36 +136,26 @@ def compress(self) -> bool:
compressButton: adsk.core.BoolValueCommandInput = self.generalOptionsTab.children.itemById(
"compressOutputButton"
)
- return compressButton.value
+ return compressButton.value or False
@property
def exportAsPart(self) -> bool:
exportAsPartButton: adsk.core.BoolValueCommandInput = self.generalOptionsTab.children.itemById(
"exportAsPartButton"
)
- return exportAsPartButton.value
-
- @property
- def selectedUnits(self) -> PreferredUnits:
- return self.currentUnits
+ return exportAsPartButton.value or False
@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:
autoCalcWeightButton: adsk.core.BoolValueCommandInput = self.generalOptionsTab.children.itemById(
"autoCalcWeightButton"
)
- return autoCalcWeightButton.value
+ return autoCalcWeightButton.value or False
@property
def exportLocation(self) -> ExportLocation:
@@ -223,25 +173,26 @@ def overrideFriction(self) -> bool:
overrideFrictionButton: adsk.core.BoolValueCommandInput = self.generalOptionsTab.children.itemById(
"frictionOverride"
)
- return overrideFrictionButton.value
+ return overrideFrictionButton.value or False
@property
def frictionOverrideCoeff(self) -> float:
frictionSlider: adsk.core.FloatSliderCommandInput = self.generalOptionsTab.children.itemById(
"frictionCoefficient"
)
- return frictionSlider.valueOne
+ return frictionSlider.valueOne or -1.0
@logFailure
def handleInputChanged(self, args: adsk.core.InputChangedEventArgs) -> None:
+ autoCalcWeightButton: adsk.core.BoolValueCommandInput = args.inputs.itemById("autoCalcWeightButton")
+ 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")
commandInput = args.input
if commandInput.id == "exportModeDropdown":
modeDropdown = adsk.core.DropDownCommandInput.cast(commandInput)
- autoCalcWeightButton: adsk.core.BoolValueCommandInput = args.inputs.itemById("autoCalcWeightButton")
- weightTable: adsk.core.TableCommandInput = args.inputs.itemById("weightTable")
- exportAsPartButton: adsk.core.BoolValueCommandInput = args.inputs.itemById("exportAsPartButton")
- overrideFrictionButton: adsk.core.BoolValueCommandInput = args.inputs.itemById("frictionOverride")
- frictionSlider: adsk.core.FloatSliderCommandInput = args.inputs.itemById("frictionCoefficient")
+
if modeDropdown.selectedItem.index == self.previousSelectedModeDropdownIndex:
return
@@ -250,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
@@ -260,46 +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)
- weightTable: adsk.core.TableCommandInput = args.inputs.itemById("weightTable")
- weightInput: adsk.core.ValueCommandInput = weightTable.getInputAtPosition(0, 1)
- 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
- weightTable: adsk.core.TableCommandInput = args.inputs.itemById("weightTable")
- weightInput: adsk.core.ValueCommandInput = weightTable.getInputAtPosition(0, 1)
-
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,27 +232,8 @@ def handleInputChanged(self, args: adsk.core.InputChangedEventArgs) -> None:
elif commandInput.id == "frictionOverride":
frictionOverrideButton = adsk.core.BoolValueCommandInput.cast(commandInput)
- frictionSlider: adsk.core.FloatSliderCommandInput = args.inputs.itemById("frictionCoefficient")
if frictionOverrideButton.value == self.previousFrictionOverrideCheckboxState:
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 KG(round(mass, 2))
diff --git a/exporter/SynthesisFusionAddin/src/UI/HUI.py b/exporter/SynthesisFusionAddin/src/UI/HUI.py
index d1a968d642..d5be4473f7 100644
--- a/exporter/SynthesisFusionAddin/src/UI/HUI.py
+++ b/exporter/SynthesisFusionAddin/src/UI/HUI.py
@@ -1,3 +1,5 @@
+from typing import Any, Callable
+
import adsk.core
from src import INTERNAL_ID, gm
@@ -7,8 +9,8 @@
# no longer used
class HPalette:
- handlers = []
- events = []
+ handlers: list[Any] = []
+ events: list[adsk.core.Event] = []
def __init__(
self,
@@ -19,8 +21,8 @@ def __init__(
resizeable: bool,
width: int,
height: int,
- *argv,
- ):
+ *argv: Any,
+ ) -> None:
"""#### Creates a HPalette Object with a number of function pointers that correspond to a action on the js side.
Arguments:
@@ -69,9 +71,12 @@ def __init__(
self.palette.dockingState = adsk.core.PaletteDockingStates.PaletteDockStateLeft
- onHTML = Handlers.HPaletteHTMLEventHandler(self)
- self.palette.incomingFromHTML.add(onHTML)
- self.handlers.append(onHTML)
+ # Transition: AARD-1765
+ # Should be removed later as this is no longer used, would have been
+ # impossible to add typing for this block.
+ # onHTML = Handlers.HPaletteHTMLEventHandler(self)
+ # self.palette.incomingFromHTML.add(onHTML)
+ # self.handlers.append(onHTML)
self.palette.isVisible = True
@@ -86,7 +91,7 @@ def deleteMe(self) -> None:
class HButton:
- handlers = []
+ handlers: list[Any] = []
""" Keeps all handler classes alive which is essential apparently. - used in command events """
@logFailure
@@ -94,11 +99,11 @@ def __init__(
self,
name: str,
location: str,
- check_func: object,
- exec_func: object,
+ check_func: Callable[..., bool],
+ exec_func: Callable[..., Any],
description: str = "No Description",
command: bool = False,
- ):
+ ) -> None:
"""# Creates a new HButton Class.
Arguments:
@@ -170,7 +175,7 @@ def promote(self, flag: bool) -> None:
self.buttonControl.isPromotedByDefault = flag
self.buttonControl.isPromoted = flag
- def deleteMe(self):
+ def deleteMe(self) -> None:
"""## Custom deleteMe method to easily deconstruct button data.
This somehow doesn't work if I keep local references to all of these definitions.
@@ -186,7 +191,7 @@ def deleteMe(self):
if ctrl:
ctrl.deleteMe()
- def scrub(self):
+ def scrub(self) -> None:
"""### In-case I make a mistake or a crash happens early it can scrub the command.
It can only be called if the ID is not currently in the buttons list.
@@ -195,7 +200,7 @@ def scrub(self):
"""
self.deleteMe()
- def __str__(self):
+ def __str__(self) -> str:
"""### Retrieves the button unique ID and treats it as a string.
Returns:
*str* -- button unique ID.
diff --git a/exporter/SynthesisFusionAddin/src/UI/Handlers.py b/exporter/SynthesisFusionAddin/src/UI/Handlers.py
index 4529b861db..0e60ba36f7 100644
--- a/exporter/SynthesisFusionAddin/src/UI/Handlers.py
+++ b/exporter/SynthesisFusionAddin/src/UI/Handlers.py
@@ -1,3 +1,5 @@
+from typing import Any
+
import adsk.core
@@ -8,17 +10,17 @@ class HButtonCommandCreatedEvent(adsk.core.CommandCreatedEventHandler):
**adsk.core.CommandCreatedEventHandler** -- Parent abstract created event class
"""
- def __init__(self, button):
+ def __init__(self, button: Any) -> None:
super().__init__()
self.button = button
- def notify(self, args):
+ def notify(self, args: adsk.core.CommandCreatedEventArgs) -> None:
"""## Called when parent button object is created and links the execute function pointer.
Arguments:
**args** *args* -- List of arbitrary info given to fusion event handlers.
"""
- cmd = adsk.core.CommandCreatedEventArgs.cast(args).command
+ cmd = args.command
if self.button.check_func():
onExecute = HButtonCommandExecuteHandler(self.button)
@@ -33,11 +35,11 @@ class HButtonCommandExecuteHandler(adsk.core.CommandEventHandler):
**adsk.core.CommandEventHandler** -- Fusion CommandEventHandler Abstract parent to link notify to ui.
"""
- def __init__(self, button):
+ def __init__(self, button: Any) -> None:
super().__init__()
self.button = button
- def notify(self, _):
+ def notify(self, _: adsk.core.CommandEventArgs) -> None:
self.button.exec_func()
diff --git a/exporter/SynthesisFusionAddin/src/UI/Helper.py b/exporter/SynthesisFusionAddin/src/UI/Helper.py
index ba8bf9b0e9..9253af0600 100644
--- a/exporter/SynthesisFusionAddin/src/UI/Helper.py
+++ b/exporter/SynthesisFusionAddin/src/UI/Helper.py
@@ -3,52 +3,52 @@
import adsk.core
from src import APP_NAME, APP_TITLE, INTERNAL_ID, gm
+from src.Logging import logFailure
from src.UI import HUI, Events
-def getDocName() -> str or None:
+def check_solid_open() -> bool:
+ return True
+
+
+def getDocName() -> str | None:
"""### Gets the active Document Name
- If it can't find one then it will return None
"""
app = adsk.core.Application.get()
if check_solid_open():
- return app.activeDocument.design.rootComponent.name.rsplit(" ", 1)[0]
+ return app.activeDocument.design.rootComponent.name.rsplit(" ", 1)[0] or ""
else:
return None
+@logFailure(messageBox=True)
def checkAttribute() -> bool:
"""### Will process the file and look for a flag that unity is already using it."""
app = adsk.core.Application.get()
- try:
- connected = app.activeDocument.attributes.itemByName("UnityFile", "Connected")
- if connected is not None:
- return connected.value
- return False
- except:
- app.userInterface.messageBox(f"Could not access the attributes of the file \n -- {traceback.format_exc()}.")
- return False
+ connected = app.activeDocument.attributes.itemByName("UnityFile", "Connected")
+ if connected is not None:
+ return connected.value or False
+
+ return False
-def addUnityAttribute() -> bool or None:
+@logFailure
+def addUnityAttribute() -> bool | None:
"""#### Adds an attribute to the Fusion File
- Initially intended to be used to add a marker for in use untiy files
- No longer necessary
"""
app = adsk.core.Application.get()
- try:
- current = app.activeDocument.attributes.itemByName("UnityFile", "Connected")
-
- if check_solid_open and (current is None):
- val = app.activeDocument.attributes.add("UnityFile", "Connected", "True")
- return val
- elif current is not None:
- return current
- return None
+ current = app.activeDocument.attributes.itemByName("UnityFile", "Connected")
+
+ if check_solid_open() and (current is None):
+ val = app.activeDocument.attributes.add("UnityFile", "Connected", "True")
+ return val or False
+ elif current is not None:
+ return current or False
- except:
- app.userInterface.messageBox(f"Could not access the attributes of the file \n -- {traceback.format_exc()}.")
- return False
+ return None
def openPanel() -> None:
@@ -70,5 +70,3 @@ def openPanel() -> None:
func_list = [o for o in getmembers(Events, isfunction)]
palette_new = HUI.HPalette(name, APP_TITLE, True, True, False, 400, 500, func_list)
gm.elements.append(palette_new)
-
- return
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/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py
index b06ae6f2af..217d409329 100644
--- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py
+++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py
@@ -104,12 +104,16 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None:
@property
def isVisible(self) -> bool:
- return self.jointConfigTab.isVisible
+ return self.jointConfigTab.isVisible or False
@isVisible.setter
def isVisible(self, value: bool) -> None:
self.jointConfigTab.isVisible = value
+ @property
+ def isActive(self) -> bool:
+ return self.jointConfigTab.isActive or False
+
@logFailure
def addJoint(self, fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = None) -> bool:
if fusionJoint in self.selectedJointList:
@@ -264,6 +268,7 @@ def addJoint(self, fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = None
)
self.previousWheelCheckboxState.append(isWheel)
+ return True
@logFailure
def addWheel(self, joint: adsk.fusion.Joint, wheel: Wheel | None = None) -> None:
@@ -403,6 +408,14 @@ def handleInputChanged(
self, args: adsk.core.InputChangedEventArgs, globalCommandInputs: adsk.core.CommandInputs
) -> None:
commandInput = args.input
+ jointAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointAddButton")
+ jointTable: adsk.core.TableCommandInput = args.inputs.itemById("jointTable")
+ jointRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("jointRemoveButton")
+ jointSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById(
+ "jointSelectCancelButton"
+ )
+ jointSelection: adsk.core.SelectionCommandInput = globalCommandInputs.itemById("jointSelection")
+
if commandInput.id == "wheelType":
wheelTypeDropdown = adsk.core.DropDownCommandInput.cast(commandInput)
position = self.wheelConfigTable.getPosition(wheelTypeDropdown)[1]
@@ -443,22 +456,12 @@ def handleInputChanged(
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:
@@ -468,12 +471,6 @@ def handleInputChanged(
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
diff --git a/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py b/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py
index 5b90c1b671..30cb8831d7 100644
--- a/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py
+++ b/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py
@@ -1,3 +1,5 @@
+from typing import Callable
+
import adsk.core
import adsk.fusion
@@ -7,23 +9,20 @@
# global mapping list of event handlers to keep them referenced for the duration of the command
# handlers = {}
-handlers = []
-cmdDefs = []
-entities = []
-occurrencesOfComponents = {}
+handlers: list[adsk.core.CommandEventHandler] = []
+cmdDefs: list[adsk.core.CommandDefinition] = []
+entities: list[adsk.fusion.Occurrence] = []
logger = getLogger()
@logFailure(messageBox=True)
-def setupMarkingMenu(ui: adsk.core.UserInterface):
+def setupMarkingMenu(ui: adsk.core.UserInterface) -> None:
handlers.clear()
@logFailure(messageBox=True)
- def setLinearMarkingMenu(args):
- menuArgs = adsk.core.MarkingMenuEventArgs.cast(args)
-
- linearMenu = menuArgs.linearMarkingMenu
+ def setLinearMarkingMenu(args: adsk.core.MarkingMenuEventArgs) -> None:
+ linearMenu = args.linearMarkingMenu
linearMenu.controls.addSeparator("LinearSeparator")
synthDropDown = linearMenu.controls.addDropDown("Synthesis", "", "synthesis")
@@ -49,14 +48,16 @@ def setLinearMarkingMenu(args):
cmdEnableCollision = ui.commandDefinitions.itemById("EnableCollision")
synthDropDown.controls.addCommand(cmdEnableCollision)
- def setCollisionAttribute(occ: adsk.fusion.Occurrence, isEnabled: bool = True):
+ def setCollisionAttribute(occ: adsk.fusion.Occurrence, isEnabled: bool = True) -> None:
attr = occ.attributes.itemByName("synthesis", "collision_off")
if attr == None and not isEnabled:
occ.attributes.add("synthesis", "collision_off", "true")
elif attr != None and isEnabled:
attr.deleteMe()
- def applyToSelfAndAllChildren(occ: adsk.fusion.Occurrence, modFunc):
+ def applyToSelfAndAllChildren(
+ occ: adsk.fusion.Occurrence, modFunc: Callable[[adsk.fusion.Occurrence], None]
+ ) -> None:
modFunc(occ)
childLists = []
childLists.append(occ.childOccurrences)
@@ -70,22 +71,16 @@ def applyToSelfAndAllChildren(occ: adsk.fusion.Occurrence, modFunc):
childLists.append(o.childOccurrences)
class MyCommandCreatedEventHandler(adsk.core.CommandCreatedEventHandler):
- def __init__(self):
- super().__init__()
-
@logFailure(messageBox=True)
- def notify(self, args):
+ def notify(self, args: adsk.core.CommandCreatedEventArgs) -> None:
command = args.command
onCommandExcute = MyCommandExecuteHandler()
handlers.append(onCommandExcute)
command.execute.add(onCommandExcute)
class MyCommandExecuteHandler(adsk.core.CommandEventHandler):
- def __init__(self):
- super().__init__()
-
@logFailure(messageBox=True)
- def notify(self, args):
+ def notify(self, args: adsk.core.CommandEventArgs) -> None:
command = args.firingEvent.sender
cmdDef = command.parentCommandDefinition
if cmdDef:
@@ -128,15 +123,10 @@ def notify(self, args):
ui.messageBox("No CommandDefinition")
class MyMarkingMenuHandler(adsk.core.MarkingMenuEventHandler):
- def __init__(self):
- super().__init__()
-
@logFailure(messageBox=True)
- def notify(self, args):
+ def notify(self, args: adsk.core.CommandEventArgs) -> None:
setLinearMarkingMenu(args)
- global occurrencesOfComponents
-
# selected entities
global entities
entities.clear()
@@ -201,7 +191,7 @@ def notify(self, args):
@logFailure(messageBox=True)
-def stopMarkingMenu(ui: adsk.core.UserInterface):
+def stopMarkingMenu(ui: adsk.core.UserInterface) -> None:
for obj in cmdDefs:
if obj.isValid:
obj.deleteMe()
diff --git a/exporter/SynthesisFusionAddin/src/UI/OsHelper.py b/exporter/SynthesisFusionAddin/src/UI/OsHelper.py
index e338be5815..3b97a4b891 100644
--- a/exporter/SynthesisFusionAddin/src/UI/OsHelper.py
+++ b/exporter/SynthesisFusionAddin/src/UI/OsHelper.py
@@ -2,7 +2,7 @@
import platform
-def getOSPath(*argv) -> str:
+def getOSPath(*argv: str) -> str:
"""Takes n strings and constructs a OS specific path
Returns:
@@ -17,7 +17,7 @@ def getOSPath(*argv) -> str:
return path
-def getOSPathPalette(*argv) -> str:
+def getOSPathPalette(*argv: str) -> str:
"""## This is a different delimeter than the resources path."""
path = ""
for arg in argv:
@@ -25,7 +25,7 @@ def getOSPathPalette(*argv) -> str:
return path
-def getDesktop():
+def getDesktop() -> str:
"""Gets the Desktop Path.
Returns:
@@ -37,7 +37,7 @@ def getDesktop():
return os.path.join(os.path.join(os.environ["USERPROFILE"]), "Desktop/")
-def getOS():
+def getOS() -> str:
"""## Returns platform as a string
- Darwin
diff --git a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py
index 999abd1176..abf6f7df32 100644
--- a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py
+++ b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py
@@ -3,6 +3,7 @@
import traceback
import urllib.parse
import urllib.request
+from typing import Any
import adsk.core
@@ -15,10 +16,7 @@
class ShowAPSAuthCommandExecuteHandler(adsk.core.CommandEventHandler):
- def __init__(self):
- super().__init__()
-
- def notify(self, args):
+ def notify(self, args: adsk.core.CommandEventArgs) -> None:
try:
global palette
palette = gm.ui.palettes.itemById("authPalette")
@@ -60,10 +58,10 @@ def notify(self, args):
class ShowAPSAuthCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
- def __init__(self, configure):
+ def __init__(self, configure: Any) -> None:
super().__init__()
- def notify(self, args):
+ def notify(self, args: adsk.core.CommandCreatedEventArgs) -> None:
try:
command = args.command
onExecute = ShowAPSAuthCommandExecuteHandler()
@@ -77,18 +75,11 @@ def notify(self, args):
class SendInfoCommandExecuteHandler(adsk.core.CommandEventHandler):
- def __init__(self):
- super().__init__()
-
- def notify(self, args):
- pass
+ def notify(self, args: adsk.core.CommandEventArgs) -> None: ...
class SendInfoCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
- def __init__(self):
- super().__init__()
-
- def notify(self, args):
+ def notify(self, args: adsk.core.CommandCreatedEventArgs) -> None:
try:
command = args.command
onExecute = SendInfoCommandExecuteHandler()
@@ -102,10 +93,7 @@ def notify(self, args):
class MyCloseEventHandler(adsk.core.UserInterfaceGeneralEventHandler):
- def __init__(self):
- super().__init__()
-
- def notify(self, args):
+ def notify(self, args: adsk.core.EventArgs) -> None:
try:
if palette:
palette.deleteMe()
@@ -118,13 +106,9 @@ def notify(self, args):
class MyHTMLEventHandler(adsk.core.HTMLEventHandler):
- def __init__(self):
- super().__init__()
-
- def notify(self, args):
+ def notify(self, args: adsk.core.HTMLEventArgs) -> None:
try:
- htmlArgs = adsk.core.HTMLEventArgs.cast(args)
- data = json.loads(htmlArgs.data)
+ data = json.loads(args.data)
# gm.ui.messageBox(msg)
convertAuthToken(data["code"])
diff --git a/exporter/SynthesisFusionAddin/src/UI/ShowWebsiteCommand.py b/exporter/SynthesisFusionAddin/src/UI/ShowWebsiteCommand.py
index cc99cffb07..7441a071b4 100644
--- a/exporter/SynthesisFusionAddin/src/UI/ShowWebsiteCommand.py
+++ b/exporter/SynthesisFusionAddin/src/UI/ShowWebsiteCommand.py
@@ -1,34 +1,31 @@
-import traceback
import webbrowser
+from typing import Any
import adsk.core
from src import gm
+from src.Logging import logFailure
class ShowWebsiteCommandExecuteHandler(adsk.core.CommandEventHandler):
def __init__(self) -> None:
super().__init__()
- def notify(self, args):
- try:
- url = "https://synthesis.autodesk.com/tutorials.html"
- res = webbrowser.open(url, new=2)
- if not res:
- gm.ui.messageBox("Failed\n{}".format(traceback.format_exc()))
- except:
- gm.ui.messageBox("Failed\n{}".format(traceback.format_exc()))
+ @logFailure
+ def notify(self, args: adsk.core.CommandEventArgs) -> None:
+ url = "https://synthesis.autodesk.com/tutorials.html"
+ res = webbrowser.open(url, new=2)
+ if not res:
+ raise BaseException("Could not open webbrowser")
class ShowWebsiteCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
- def __init__(self, configure) -> None:
+ def __init__(self, configure: Any) -> None:
super().__init__()
- def notify(self, args):
- try:
- command = args.command
- onExecute = ShowWebsiteCommandExecuteHandler()
- command.execute.add(onExecute)
- gm.handlers.append(onExecute)
- except:
- gm.ui.messageBox("Failed\n{}".format(traceback.format_exc()))
+ @logFailure
+ def notify(self, args: adsk.core.CommandCreatedEventArgs) -> None:
+ command = args.command
+ onExecute = ShowWebsiteCommandExecuteHandler()
+ command.execute.add(onExecute)
+ gm.handlers.append(onExecute)
diff --git a/exporter/SynthesisFusionAddin/src/UI/Toolbar.py b/exporter/SynthesisFusionAddin/src/UI/Toolbar.py
index f9d150cc3c..93e6173502 100644
--- a/exporter/SynthesisFusionAddin/src/UI/Toolbar.py
+++ b/exporter/SynthesisFusionAddin/src/UI/Toolbar.py
@@ -1,3 +1,5 @@
+import adsk.core
+
from src import INTERNAL_ID, gm
from src.Logging import logFailure
@@ -8,27 +10,23 @@ class Toolbar:
- holds handlers
"""
- uid = None
- tab = None
- panels = []
- controls = []
+ uid: str
+ tab: adsk.core.ToolbarTab
+ panels: list[str] = []
+ controls: list[str] = []
@logFailure
def __init__(self, name: str):
self.uid = f"{name}_{INTERNAL_ID}_toolbar"
self.name = name
- designWorkspace = gm.ui.workspaces.itemById("FusionSolidEnvironment")
-
- if designWorkspace:
- allDesignTabs = designWorkspace.toolbarTabs
-
- self.tab = allDesignTabs.itemById(self.uid)
-
- if self.tab is None:
- self.tab = allDesignTabs.add(self.uid, name)
+ designWorkspace = gm.ui.workspaces.itemById("FusionSolidEnvironment") or adsk.core.Workspace()
+ allDesignTabs = designWorkspace.toolbarTabs
+ self.tab = allDesignTabs.itemById(self.uid)
+ if self.tab is None:
+ self.tab = allDesignTabs.add(self.uid, name)
- self.tab.activate()
+ self.tab.activate()
def getPanel(self, name: str, visibility: bool = True) -> str | None:
"""# Gets a control for a panel to the tabbed toolbar
diff --git a/exporter/SynthesisFusionAddin/src/Util.py b/exporter/SynthesisFusionAddin/src/Util.py
new file mode 100644
index 0000000000..c5f91ef635
--- /dev/null
+++ b/exporter/SynthesisFusionAddin/src/Util.py
@@ -0,0 +1,48 @@
+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."""
+ os.makedirs(directory, exist_ok=True)
+ return directory
diff --git a/exporter/SynthesisFusionAddin/src/__init__.py b/exporter/SynthesisFusionAddin/src/__init__.py
index 1e426279bb..b53fb6b045 100644
--- a/exporter/SynthesisFusionAddin/src/__init__.py
+++ b/exporter/SynthesisFusionAddin/src/__init__.py
@@ -1,17 +1,34 @@
import os
import platform
+from pathlib import Path
from src.GlobalManager import GlobalManager
+from src.Util import makeDirectories
APP_NAME = "Synthesis"
APP_TITLE = "Synthesis Robot Exporter"
DESCRIPTION = "Exports files from Fusion into the Synthesis Format"
INTERNAL_ID = "Synthesis"
ADDIN_PATH = os.path.dirname(os.path.realpath(__file__))
+IS_RELEASE = str(Path(os.path.abspath(__file__)).parent.parent.parent.parent).split(os.sep)[-1] == "ApplicationPlugins"
SYSTEM = platform.system()
-assert SYSTEM != "Linux"
+if SYSTEM == "Windows":
+ SUPPORT_PATH = makeDirectories(os.path.expandvars(r"%appdata%\Autodesk\Synthesis"))
+else:
+ assert SYSTEM == "Darwin"
+ SUPPORT_PATH = makeDirectories(f"{os.path.expanduser('~')}/.config/Autodesk/Synthesis")
gm = GlobalManager()
-__all__ = ["APP_NAME", "APP_TITLE", "DESCRIPTION", "INTERNAL_ID", "ADDIN_PATH", "SYSTEM", "gm"]
+__all__ = [
+ "APP_NAME",
+ "APP_TITLE",
+ "DESCRIPTION",
+ "INTERNAL_ID",
+ "ADDIN_PATH",
+ "IS_RELEASE",
+ "SYSTEM",
+ "SUPPORT_PATH",
+ "gm",
+]
diff --git a/fission/README.md b/fission/README.md
index fc4098ecce..6d8a493e87 100644
--- a/fission/README.md
+++ b/fission/README.md
@@ -1,14 +1,31 @@
-# Fission: Synthesis' web-based robot simulator
+# Fission
-## Gettings Started
+Fission is Synthesis' web-based robotics simulator. This app is hosted [on our website](https://synthesis.github.com/fission/), in addition to a closed, in-development version [here](https://synthesis.autodesk.com/beta/).
+## Setup & Building
### Requirements
1. NPM (v10.2.4 recommended)
+ - Yarn, Bun, or any other package managers work just as well.
2. NodeJS (v20.10.0 recommended)
-3. TypeScript (v4.8.4 recommended) _Unknown if this is actually required_
+ - Needed for running the development server.
-### Assets
+### Setup
+
+You can either run the `init` command or run the following commands details in "Specific Steps":
+
+```bash
+npm i && npm init
+```
+
+
+Specific Steps
+
+To install all dependencies:
+
+```bash
+npm i
+```
For the asset pack that will be available in production, download the asset pack [here](https://synthesis.autodesk.com/Downloadables/assetpack.zip) and unzip it.
Make sure that the Downloadables directory is placed inside of the public directory like so:
@@ -19,50 +36,63 @@ Make sure that the Downloadables directory is placed inside of the public direct
This can be accomplished with the `assetpack` npm script:
-```
+```bash
npm run assetpack
```
-### Building
-
-To build, install all dependencies:
+We use [Playwright](https://playwright.dev/) for testing consistency. The package is installed with the rest of the dependencies; however, be sure to install the playwright browsers with the following command:
```bash
-npm i
+npx playwright install
+```
+or
+```bash
+npm run playwright:install
```
-### NPM Scripts
-
-| Script | Description |
-| -------------- | ----------------------------------------------------------------------------------------------------------------------- |
-| `dev` | Starts the development server used for testing. Supports hotloading (though finicky with WASM module loading). |
-| `test` | Runs the unit tests via vitest. |
-| `build` | Builds the project into it's packaged form. Uses root base path. |
-| `build:prod` | Builds the project into it's packaged form. Uses the `/fission/` base path. |
-| `preview` | Runs the built project for preview locally before deploying. |
-| `lint` | Runs eslint on the project. |
-| `lint:fix` | Attempts to fix issues found with eslint. |
-| `prettier` | Runs prettier on the project as a check. |
-| `prettier:fix` | Runs prettier on the project to fix any issues with formating. **DO NOT USE**, I don't like the current format it uses. |
-| `format` | Runs `prettier:fix` and `lint:fix`. **Do not use** for the same reasons as `prettier:fix`. |
-| `assetpack` | Downloads the assetpack and unzips/installs it in the correct location. |
-| `playwright:install` | Downloads the playwright browsers. |
+
-### Unit Testing
+### Environment Configuration
-We use [Playwright](https://playwright.dev/) for testing consistency. The package is installed with the rest of the dependencies; however, be sure to install the playwright browsers with the following command:
+In `vite.config.ts` you'll find a number of constants that can be used to tune the project to match your development environment.
+## Running & Testing
+
+### Development Server
+
+You can use the `dev` command to run the development server. This will open a server on port 3000 and open your default browser at the hosted endpoint.
+
+```bash
+npm run dev
```
-npx playwright install
+
+### Unit Testing
+
+We use a combination of Vitest and Playwright for running our unit tests. A number of the unit tests rely on the asset pack data and may time out due to download speeds. By default, the unit test command uses a Chromium browser.
+
+```bash
+npm run test
```
-or
+
+## Packaging
+
+We have two packaging commands: one for compiling dev for attachment to the in-development endpoint, and another for the release endpoint.
+
+Release:
+```bash
+npm run build:prod
```
-npm run playwright:install
+
+In-development:
+```bash
+npm run build:dev
```
-### Autodesk Platform Services
+You can alternatively run the default build command for your own hosting:
-To test/enable the use of Autodesk Platform Services (APS), please follow instructions for development web server (Closed Source).
+```bash
+npm run build
+```
## Core Systems
@@ -75,16 +105,18 @@ The world serves as a hub for all of the core systems. It is a static class that
### Scene Renderer
-The Scene Renderer is our interface with rendering within the Canvas. This is primarily done via ThreeJS, however can be extended later on.
+The Scene Renderer is our interface for rendering within the Canvas. This is primarily done via ThreeJS, however it can be extended later on.
### Physics System
-This Physics System is our interface with Jolt, ensuring objects are properly handled and provides utility functions that are more custom fit to our purposes.
+This Physics System is our interface with Jolt, ensuring objects are properly handled and providing utility functions that are more custom-fit to our purposes.
[Jolt Physics Architecture](https://jrouwe.github.io/JoltPhysics/)
### Input System
+The Input System listens for and records key presses and joystick positions to be used by robots. It also maps robot behaviors (e.g. an arcade drivetrain or an arm) to specific keys through customizable input schemes.
+
### UI System
## Additional Systems
@@ -93,11 +125,11 @@ These systems will extend off of the core systems to build out features in Synth
### Simulation System
-The Simulation System articulates dynamic elements of the scene via the Physics System. At it's core there are 3 main components:
+The Simulation System articulates dynamic elements of the scene via the Physics System. At its core there are 3 main components:
#### Driver
-Drivers are mostly write-only. They take in values to know how to articulate the physics objects and contraints.
+Drivers are mostly write-only. They take in values to know how to articulate the physics objects and constraints.
#### Stimulus
@@ -107,6 +139,22 @@ Stimuli are mostly read-only. They read values from given physics objects and co
Brains are the controllers of the mechanisms. They use a combination of Drivers and Stimuli to control a given mechanism.
-For basic user control of the mechanisms, we'll have a Synthesis Brain. By the end of Summer 2024, I hope to have an additional brain, the WPIBrain for facilitating WPILib code control over the mechanisms inside of Synthesis.
-
-### Mode System
+For basic user control of the mechanisms, we'll have a Synthesis Brain. We hope to have an additional brain by the end of Summer 2024: the WPIBrain for facilitating WPILib code control over the mechanisms inside of Synthesis.
+
+## NPM Scripts
+
+| Script | Description |
+| -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
+| `init` | Runs the initialization commands to install all dependencies, assets, and unit testing browsers. |
+| `dev` | Starts the development server used for testing. Supports hot-reloading (though finicky with WASM module loading). |
+| `test` | Runs the unit tests via Vitest. |
+| `build` | Builds the project into its packaged form. Uses the root base path. |
+| `build:prod` | Builds the project into its packaged form. Uses the `/fission/` base path. |
+| `preview` | Runs the built project for preview locally before deploying. |
+| `lint` | Runs ESLint on the project. |
+| `lint:fix` | Attempts to fix issues found with ESLint. |
+| `prettier` | Runs Prettier on the project as a check. |
+| `prettier:fix` | Runs Prettier on the project to fix any issues with formatting. |
+| `format` | Runs `prettier:fix` and `lint:fix`. |
+| `assetpack` | Downloads the assetpack and unzips/installs it in the correct location. |
+| `playwright:install` | Downloads the Playwright browsers. |
diff --git a/fission/index.html b/fission/index.html
index 5916fa6750..b3a5d0fd1c 100644
--- a/fission/index.html
+++ b/fission/index.html
@@ -12,7 +12,11 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
/>
-
+
+
Fission | Synthesis
diff --git a/fission/package.json b/fission/package.json
index 0643c80a52..3b5df05cdc 100644
--- a/fission/package.json
+++ b/fission/package.json
@@ -4,6 +4,7 @@
"version": "0.0.1",
"type": "module",
"scripts": {
+ "init": "(bun run assetpack && bun run playwright:install) || (npm run assetpack && npm run playwright:install)",
"dev": "vite --open",
"build": "tsc && vite build",
"build:prod": "tsc && vite build --base=/fission/ --outDir dist/prod",
@@ -45,6 +46,9 @@
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.6",
+ "@testing-library/dom": "^10.4.0",
+ "@testing-library/react": "^16.0.0",
+ "@testing-library/user-event": "^14.5.2",
"@types/node": "^20.4.4",
"@types/pako": "^2.0.3",
"@types/react": "^18.2.47",
@@ -74,5 +78,9 @@
"vite-plugin-glsl": "^1.3.0",
"vite-plugin-singlefile": "^0.13.5",
"vitest": "^1.5.3"
+ },
+ "license": "Apache-2.0",
+ "repository": {
+ "url": "https://github.com/Autodesk/synthesis.git"
}
}
diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx
index c1d33806ca..badb33a281 100644
--- a/fission/src/Synthesis.tsx
+++ b/fission/src/Synthesis.tsx
@@ -36,18 +36,15 @@ 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"
import World from "@/systems/World.ts"
-import { AddRobotsModal, AddFieldsModal, SpawningModal } from "@/modals/spawning/SpawningModals.tsx"
import ImportLocalMirabufModal from "@/modals/mirabuf/ImportLocalMirabufModal.tsx"
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"
@@ -61,7 +58,6 @@ import NewInputSchemeModal from "./ui/modals/configuring/theme-editor/NewInputSc
import AssignNewSchemeModal from "./ui/modals/configuring/theme-editor/AssignNewSchemeModal.tsx"
import AnalyticsConsent from "./ui/components/AnalyticsConsent.tsx"
import PreferencesSystem from "./systems/preferences/PreferencesSystem.ts"
-import ResetAllInputsModal from "./ui/modals/configuring/inputs/ResetAllInputsModal.tsx"
import APSManagementModal from "./ui/modals/APSManagementModal.tsx"
import ConfigurePanel from "./ui/panels/configuring/assembly-config/ConfigurePanel.tsx"
@@ -166,7 +162,7 @@ function Synthesis() {
closeAllPanels={closeAllPanels}
>
-
+
{panelElements.length > 0 && panelElements}
@@ -193,9 +189,6 @@ function Synthesis() {
const initialModals = [
,
- ,
- ,
- ,
,
,
,
@@ -221,7 +214,6 @@ const initialModals = [
,
,
,
- ,
,
]
@@ -232,17 +224,10 @@ const initialPanels: ReactElement[] = [
,
,
,
- ,
,
,
,
,
- ,
]
export default Synthesis
diff --git a/fission/src/main.tsx b/fission/src/main.tsx
index 3b761d8ab3..167538c689 100644
--- a/fission/src/main.tsx
+++ b/fission/src/main.tsx
@@ -11,16 +11,16 @@ const defaultColors: Theme = {
above: [],
},
InteractiveElementLeft: {
- color: { r: 224, g: 130, b: 65, a: 1 },
+ color: { r: 207, g: 114, b: 57, a: 1 },
above: ["Background", "BackgroundSecondary"],
},
InteractiveElementRight: {
- color: { r: 218, g: 102, b: 89, a: 1 },
+ color: { r: 212, g: 75, b: 62, a: 1 },
above: ["Background", "BackgroundSecondary"],
},
Background: { color: { r: 0, g: 0, b: 0, a: 1 }, above: [] },
- BackgroundSecondary: { color: { r: 30, g: 30, b: 30, a: 1 }, above: [] },
- InteractiveBackground: { color: { r: 52, g: 58, b: 64, a: 1 }, above: [] },
+ BackgroundSecondary: { color: { r: 18, g: 18, b: 18, a: 1 }, above: [] },
+ InteractiveBackground: { color: { r: 40, g: 44, b: 47, a: 1 }, above: [] },
MainText: {
color: { r: 255, g: 255, b: 255, a: 1 },
above: [
@@ -33,8 +33,8 @@ const defaultColors: Theme = {
],
},
Scrollbar: { color: { r: 170, g: 170, b: 170, a: 1 }, above: [] },
- AcceptButton: { color: { r: 71, g: 138, b: 226, a: 1 }, above: [] },
- CancelButton: { color: { r: 231, g: 85, b: 81, a: 1 }, above: [] },
+ AcceptButton: { color: { r: 33, g: 137, b: 228, a: 1 }, above: [] },
+ CancelButton: { color: { r: 248, g: 78, b: 78, a: 1 }, above: [] },
InteractiveElementText: {
color: { r: 255, g: 255, b: 255, a: 1 },
above: [],
@@ -63,8 +63,8 @@ const defaultColors: Theme = {
SkyboxTop: { color: { r: 255, g: 255, b: 255, a: 1 }, above: [] },
SkyboxBottom: { color: { r: 255, g: 255, b: 255, a: 1 }, above: [] },
FloorGrid: { color: { r: 93, g: 93, b: 93, a: 1 }, above: [] },
- MatchRedAlliance: { color: { r: 255, g: 0, b: 0, a: 1 }, above: [] },
- MatchBlueAlliance: { color: { r: 0, g: 0, b: 255, a: 1 }, above: [] },
+ MatchRedAlliance: { color: { r: 180, g: 20, b: 20, a: 1 }, above: [] },
+ MatchBlueAlliance: { color: { r: 20, g: 20, b: 180, a: 1 }, above: [] },
ToastInfo: { color: { r: 126, g: 34, b: 206, a: 1 }, above: [] },
ToastWarning: { color: { r: 234, g: 179, b: 8, a: 1 }, above: [] },
ToastError: { color: { r: 239, g: 68, b: 68, a: 1 }, above: [] },
diff --git a/fission/src/mirabuf/IntakeSensorSceneObject.ts b/fission/src/mirabuf/IntakeSensorSceneObject.ts
index 517ab86423..bd8986afbe 100644
--- a/fission/src/mirabuf/IntakeSensorSceneObject.ts
+++ b/fission/src/mirabuf/IntakeSensorSceneObject.ts
@@ -20,14 +20,10 @@ class IntakeSensorSceneObject extends SceneObject {
private _deltaTransformation?: THREE.Matrix4
private _joltBodyId?: Jolt.BodyID
- private _mesh?: THREE.Mesh
private _collision?: (e: OnContactPersistedEvent) => void
public constructor(parentAssembly: MirabufSceneObject) {
super()
-
- console.debug("Trying to create intake sensor...")
-
this._parentAssembly = parentAssembly
}
@@ -47,12 +43,6 @@ class IntakeSensorSceneObject extends SceneObject {
return
}
- this._mesh = World.SceneRenderer.CreateSphere(
- this._parentAssembly.intakePreferences.zoneDiameter / 2.0,
- World.SceneRenderer.CreateToonMaterial(0x5eeb67)
- )
- World.SceneRenderer.scene.add(this._mesh)
-
this._collision = (event: OnContactPersistedEvent) => {
const brain = this._parentAssembly.brain
const brainIndex = brain instanceof SynthesisBrain ? brain.brainIndex ?? -1 : -1
@@ -71,8 +61,6 @@ class IntakeSensorSceneObject extends SceneObject {
}
OnContactPersistedEvent.AddListener(this._collision)
-
- console.debug("Intake sensor created successfully!")
}
}
@@ -88,25 +76,12 @@ class IntakeSensorSceneObject extends SceneObject {
World.PhysicsSystem.SetBodyPosition(this._joltBodyId, ThreeVector3_JoltVec3(position))
World.PhysicsSystem.SetBodyRotation(this._joltBodyId, ThreeQuaternion_JoltQuat(rotation))
-
- if (this._mesh) {
- this._mesh.position.setFromMatrixPosition(bodyTransform)
- this._mesh.rotation.setFromRotationMatrix(bodyTransform)
- }
}
}
public Dispose(): void {
- console.debug("Destroying intake sensor")
-
if (this._joltBodyId) {
World.PhysicsSystem.DestroyBodyIds(this._joltBodyId)
-
- if (this._mesh) {
- this._mesh.geometry.dispose()
- ;(this._mesh.material as THREE.Material).dispose()
- World.SceneRenderer.scene.remove(this._mesh)
- }
}
if (this._collision) OnContactPersistedEvent.RemoveListener(this._collision)
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/systems/input/DefaultInputs.ts b/fission/src/systems/input/DefaultInputs.ts
index e15fe82d44..4a6a87a687 100644
--- a/fission/src/systems/input/DefaultInputs.ts
+++ b/fission/src/systems/input/DefaultInputs.ts
@@ -1,6 +1,7 @@
import { InputScheme } from "./InputSchemeManager"
import { AxisInput, ButtonInput, EmptyModifierState } from "./InputSystem"
+/** The purpose of this class is to store any defaults related to the input system. */
class DefaultInputs {
static ernie = () => {
return {
@@ -133,6 +134,7 @@ class DefaultInputs {
}
}
+ /** We like this guy */
public static hunter = () => {
return {
schemeName: "Hunter",
@@ -187,7 +189,8 @@ class DefaultInputs {
}
}
- public static get defaultInputCopies() {
+ /** @returns {InputScheme[]} New copies of the default input schemes without reference to any others. */
+ public static get defaultInputCopies(): InputScheme[] {
return [
DefaultInputs.ernie(),
DefaultInputs.luna(),
@@ -197,6 +200,7 @@ class DefaultInputs {
]
}
+ /** @returns {InputScheme} A new blank input scheme with no control bound. */
public static get newBlankScheme(): InputScheme {
return {
schemeName: "",
diff --git a/fission/src/systems/input/InputSchemeManager.ts b/fission/src/systems/input/InputSchemeManager.ts
index 23a5ede69d..4e2d66d73c 100644
--- a/fission/src/systems/input/InputSchemeManager.ts
+++ b/fission/src/systems/input/InputSchemeManager.ts
@@ -129,20 +129,6 @@ class InputSchemeManager {
PreferencesSystem.setGlobalPreference("InputSchemes", customizedSchemes)
PreferencesSystem.savePreferences()
}
-
- /** Returns a copy of a scheme without references to the original in any way */
- public static copyScheme(scheme: InputScheme): InputScheme {
- const copiedInputs: Input[] = []
- scheme.inputs.forEach(i => copiedInputs.push(i.getCopy()))
-
- return {
- schemeName: scheme.schemeName,
- descriptiveName: scheme.descriptiveName,
- customized: scheme.customized,
- usesGamepad: scheme.usesGamepad,
- inputs: copiedInputs,
- }
- }
}
export default InputSchemeManager
diff --git a/fission/src/systems/input/InputSystem.ts b/fission/src/systems/input/InputSystem.ts
index d56aadd692..509f46760e 100644
--- a/fission/src/systems/input/InputSystem.ts
+++ b/fission/src/systems/input/InputSystem.ts
@@ -9,28 +9,34 @@ export type ModifierState = {
}
export const EmptyModifierState: ModifierState = { ctrl: false, alt: false, shift: false, meta: false }
-// Represents any input
+/** Represents any user input */
abstract class Input {
public inputName: string
+ /** @param {string} inputName - The name given to this input to identify it's function. */
constructor(inputName: string) {
this.inputName = inputName
}
- // Returns the current value of the input. Range depends on input type
+ /** @returns {number} a number between -1 and 1 for this input. */
abstract getValue(useGamepad: boolean): number
-
- // Creates a copy to avoid modifying the default inputs by reference
- abstract getCopy(): Input
}
-// A single button
+/** Represents any user input that is a single true/false button. */
class ButtonInput extends Input {
public keyCode: string
public keyModifiers: ModifierState
public gamepadButton: number
+ /**
+ * All optional params will remain unassigned if not value is given. This can be assigned later by the user through the configuration panel.
+ *
+ * @param {string} inputName - The name given to this input to identify it's function.
+ * @param {string} [keyCode] - The keyboard button for this input if a gamepad is not used.
+ * @param {number} [gamepadButton] - The gamepad button for this input if a gamepad is used.
+ * @param {ModifierState} [keyModifiers] - The key modifier state for the keyboard input.
+ */
public constructor(inputName: string, keyCode?: string, gamepadButton?: number, keyModifiers?: ModifierState) {
super(inputName)
this.keyCode = keyCode ?? ""
@@ -38,7 +44,10 @@ class ButtonInput extends Input {
this.gamepadButton = gamepadButton ?? -1
}
- // Returns 1 if pressed and 0 if not pressed
+ /**
+ * @param useGamepad Looks at the gamepad if true and the keyboard if false.
+ * @returns 1 if pressed, 0 if not pressed or not found.
+ */
getValue(useGamepad: boolean): number {
// Gamepad button input
if (useGamepad) {
@@ -48,13 +57,9 @@ class ButtonInput extends Input {
// Keyboard button input
return InputSystem.isKeyPressed(this.keyCode, this.keyModifiers) ? 1 : 0
}
-
- getCopy(): Input {
- return new ButtonInput(this.inputName, this.keyCode, this.gamepadButton, this.keyModifiers)
- }
}
-// An axis between two buttons (-1 to 1)
+/** Represents any user input that is an axis between -1 and 1. Can be a gamepad axis, two gamepad buttons, or two keyboard buttons. */
class AxisInput extends Input {
public posKeyCode: string
public posKeyModifiers: ModifierState
@@ -67,6 +72,20 @@ class AxisInput extends Input {
public posGamepadButton: number
public negGamepadButton: number
+ /**
+ * All optional params will remain unassigned if not value is given. This can be assigned later by the user through the configuration panel.
+ *
+ * @param {string} inputName - The name given to this input to identify it's function.
+ * @param {string} [posKeyCode] - The keyboard input that corresponds to a positive input value (1).
+ * @param {string} [negKeyCode] - The keyboard input that corresponds to a negative input value (-1).
+ * @param {number} [gamepadAxisNumber] - The gamepad axis that this input looks at if the scheme is set to use a gamepad.
+ * @param {boolean} [joystickInverted] - Inverts the input if a gamepad axis is used.
+ * @param {boolean} [useGamepadButtons] - If this is true and the scheme is set to use a gamepad, this axis will be between two buttons on the controller.
+ * @param {number} [posGamepadButton] - The gamepad button that corresponds to a positive input value (1).
+ * @param {number} [negGamepadButton] - The gamepad button that corresponds to a negative input value (-1).
+ * @param {ModifierState} [posKeyModifiers] - The key modifier state for the positive keyboard input.
+ * @param {ModifierState} [negKeyModifiers] - The key modifier state for the negative keyboard input.
+ */
public constructor(
inputName: string,
posKeyCode?: string,
@@ -94,11 +113,14 @@ class AxisInput extends Input {
this.negGamepadButton = negGamepadButton ?? -1
}
- // For keyboard: returns 1 if positive pressed, -1 if negative pressed, or 0 if none or both are pressed
- // For gamepad axis: returns a range between -1 and 1 with a deadband in the middle
+ /**
+ * @param useGamepad Looks at the gamepad if true and the keyboard if false.
+ * @returns {number} KEYBOARD: 1 if positive pressed, -1 if negative pressed, or 0 if none or both are pressed.
+ * @returns {number} GAMEPAD: a number between -1 and 1 with a deadband in the middle.
+ */
getValue(useGamepad: boolean): number {
- // Gamepad joystick axis
if (useGamepad) {
+ // Gamepad joystick axis
if (!this.useGamepadButtons)
return InputSystem.getGamepadAxis(this.gamepadAxisNumber) * (this.joystickInverted ? -1 : 1)
@@ -115,38 +137,28 @@ class AxisInput extends Input {
(InputSystem.isKeyPressed(this.negKeyCode, this.negKeyModifiers) ? 1 : 0)
)
}
-
- getCopy(): Input {
- return new AxisInput(
- this.inputName,
- this.posKeyCode,
- this.negKeyCode,
- this.gamepadAxisNumber,
- this.joystickInverted,
- this.useGamepadButtons,
- this.posGamepadButton,
- this.negGamepadButton,
- this.posKeyModifiers,
- this.negKeyModifiers
- )
- }
}
+/**
+ * The input system listens for and records key presses and joystick positions to be used by robots.
+ * It also maps robot behaviors (such as an arcade drivetrain or an arm) to specific keys through customizable input schemes.
+ */
class InputSystem extends WorldSystem {
public static currentModifierState: ModifierState
- // A list of keys currently being pressed
+ /** The keys currently being pressed. */
private static _keysPressed: { [key: string]: boolean } = {}
private static _gpIndex: number | null
public static gamepad: Gamepad | null
- // Maps a brain index to a certain input scheme
+ /** Maps a brain index to an input scheme. */
public static brainIndexSchemeMap: Map = new Map()
constructor() {
super()
+ // Initialize input events
this.handleKeyDown = this.handleKeyDown.bind(this)
document.addEventListener("keydown", this.handleKeyDown)
@@ -159,19 +171,31 @@ class InputSystem extends WorldSystem {
this.gamepadDisconnected = this.gamepadDisconnected.bind(this)
window.addEventListener("gamepaddisconnected", this.gamepadDisconnected)
+ // Initialize an event that's triggered when the user exits/enters the page
document.addEventListener("visibilitychange", () => {
if (document.hidden) this.clearKeyData()
})
+
+ // Disable gesture inputs on track pad to zoom into UI
+ window.addEventListener(
+ "wheel",
+ function (e) {
+ if (e.ctrlKey) {
+ e.preventDefault() // Prevent the zoom
+ }
+ },
+ { passive: false }
+ )
}
public Update(_: number): void {
- InputSystem
// Fetch current gamepad information
if (InputSystem._gpIndex == null) InputSystem.gamepad = null
else InputSystem.gamepad = navigator.getGamepads()[InputSystem._gpIndex]
if (!document.hasFocus()) this.clearKeyData()
+ // Update the current modifier state to be checked against target stats when getting input values
InputSystem.currentModifierState = {
ctrl: InputSystem.isKeyPressed("ControlLeft") || InputSystem.isKeyPressed("ControlRight"),
alt: InputSystem.isKeyPressed("AltLeft") || InputSystem.isKeyPressed("AltRight"),
@@ -187,21 +211,22 @@ class InputSystem extends WorldSystem {
window.removeEventListener("gamepaddisconnected", this.gamepadDisconnected)
}
- // Called when any key is first pressed
+ /** Called when any key is first pressed */
private handleKeyDown(event: KeyboardEvent) {
InputSystem._keysPressed[event.code] = true
}
- // Called when any key is released
+ /* Called when any key is released */
private handleKeyUp(event: KeyboardEvent) {
InputSystem._keysPressed[event.code] = false
}
+ /** Clears all stored key data when the user leaves the page. */
private clearKeyData() {
for (const keyCode in InputSystem._keysPressed) delete InputSystem._keysPressed[keyCode]
}
- // Called once when a gamepad is first connected
+ /* Called once when a gamepad is first connected */
private gamepadConnected(event: GamepadEvent) {
console.log(
"Gamepad connected at index %d: %s. %d buttons, %d axes.",
@@ -214,14 +239,18 @@ class InputSystem extends WorldSystem {
InputSystem._gpIndex = event.gamepad.index
}
- // Called once when a gamepad is first disconnected
+ /* Called once when a gamepad is first disconnected */
private gamepadDisconnected(event: GamepadEvent) {
console.log("Gamepad disconnected from index %d: %s", event.gamepad.index, event.gamepad.id)
InputSystem._gpIndex = null
}
- // Returns true if the given key is currently down
+ /**
+ * @param {string} key - The keycode of the target key.
+ * @param {ModifierState} modifiers - The target modifier state. Assumed to be no modifiers if undefined.
+ * @returns {boolean} True if the key is pressed or false otherwise.
+ */
public static isKeyPressed(key: string, modifiers?: ModifierState): boolean {
if (modifiers != null && !InputSystem.compareModifiers(InputSystem.currentModifierState, modifiers))
return false
@@ -229,7 +258,11 @@ class InputSystem extends WorldSystem {
return !!InputSystem._keysPressed[key]
}
- // If an input exists, return it's value
+ /**
+ * @param {string} inputName The name of the function of the input.
+ * @param {number} brainIndex The robot brain index for this input. Used to map to a control scheme.
+ * @returns {number} A number between -1 and 1 based on the current state of the input.
+ */
public static getInput(inputName: string, brainIndex: number): number {
const targetScheme = InputSystem.brainIndexSchemeMap.get(brainIndex)
@@ -240,8 +273,12 @@ class InputSystem extends WorldSystem {
return targetInput.getValue(targetScheme.usesGamepad)
}
- // Returns true if two modifier states are identical
- private static compareModifiers(state1: ModifierState, state2: ModifierState): boolean {
+ /**
+ * @param {ModifierState} state1 Any key modifier state.
+ * @param {ModifierState} state2 Any key modifier state.
+ * @returns {boolean} True if the modifier states are identical and false otherwise.
+ */
+ public static compareModifiers(state1: ModifierState, state2: ModifierState): boolean {
if (!state1 || !state2) return false
return (
@@ -252,7 +289,10 @@ class InputSystem extends WorldSystem {
)
}
- // Returns a number between -1 and 1 with a deadband
+ /**
+ * @param {number} axisNumber The joystick axis index. Must be an integer.
+ * @returns {number} A number between -1 and 1 based on the position of this axis or 0 if no gamepad is connected or the axis is not found.
+ */
public static getGamepadAxis(axisNumber: number): number {
if (InputSystem.gamepad == null) return 0
@@ -264,7 +304,11 @@ class InputSystem extends WorldSystem {
return Math.abs(value) < 0.15 ? 0 : value
}
- // Returns true if a gamepad is connected and a certain button is pressed
+ /**
+ *
+ * @param {number} buttonNumber - The gamepad button index. Must be an integer.
+ * @returns {boolean} True if the button is pressed, false if not, a gamepad isn't connected, or the button can't be found.
+ */
public static isGamepadButtonPressed(buttonNumber: number): boolean {
if (InputSystem.gamepad == null) return false
diff --git a/fission/src/systems/preferences/PreferenceTypes.ts b/fission/src/systems/preferences/PreferenceTypes.ts
index 64ee580142..8c99e60d19 100644
--- a/fission/src/systems/preferences/PreferenceTypes.ts
+++ b/fission/src/systems/preferences/PreferenceTypes.ts
@@ -1,6 +1,7 @@
import { InputScheme } from "../input/InputSchemeManager"
import { Vector3Tuple } from "three"
+/** Names of all global preferences. */
export type GlobalPreference =
| "QualitySettings"
| "ZoomSensitivity"
@@ -17,6 +18,10 @@ export type GlobalPreference =
export const RobotPreferencesKey: string = "Robots"
export const FieldPreferencesKey: string = "Fields"
+/**
+ * Default values for GlobalPreferences as a fallback if they are not configured by the user.
+ * Every global preference should have a default value.
+ */
export const DefaultGlobalPreferences: { [key: string]: unknown } = {
QualitySettings: "High" as QualitySetting,
ZoomSensitivity: 15,
@@ -45,8 +50,10 @@ export type EjectorPreferences = {
parentNode: string | undefined
}
+/** The behavior types that can be sequenced. */
export type BehaviorType = "Elevator" | "Arm"
+/** Data for sequencing and inverting elevator and behaviors. */
export type SequentialBehaviorPreferences = {
jointIndex: number
parentJointIndex: number | undefined
@@ -54,6 +61,7 @@ export type SequentialBehaviorPreferences = {
inverted: boolean
}
+/** Default preferences for a joint with not parent specified and inverted set to false. */
export function DefaultSequentialConfig(index: number, type: BehaviorType): SequentialBehaviorPreferences {
return {
jointIndex: index,
@@ -93,6 +101,7 @@ export type ScoringZonePreferences = {
}
export type FieldPreferences = {
+ // TODO: implement this
defaultSpawnLocation: Vector3Tuple
scoringZones: ScoringZonePreferences[]
}
diff --git a/fission/src/systems/preferences/PreferencesSystem.ts b/fission/src/systems/preferences/PreferencesSystem.ts
index 09b8c3961e..74a7d24607 100644
--- a/fission/src/systems/preferences/PreferencesSystem.ts
+++ b/fission/src/systems/preferences/PreferencesSystem.ts
@@ -9,10 +9,15 @@ import {
RobotPreferencesKey,
} from "./PreferenceTypes"
+/** An event that's triggered when a preference is changed. */
export class PreferenceEvent extends Event {
public prefName: GlobalPreference
public prefValue: unknown
+ /**
+ * @param {GlobalPreference} prefName - The name of the preference that has just been updated.
+ * @param {unknown} prefValue - The new value this preference was set to.
+ */
constructor(prefName: GlobalPreference, prefValue: unknown) {
super("preferenceChanged")
this.prefName = prefName
@@ -20,6 +25,7 @@ export class PreferenceEvent extends Event {
}
}
+/** The preference system handles loading, saving, and updating all user managed data saved in local storage. */
class PreferencesSystem {
private static _preferences: { [key: string]: unknown }
private static _localStorageKey = "Preferences"
@@ -36,7 +42,12 @@ class PreferencesSystem {
return this._preferences[key] as T
}
- /** Gets a global preference, or it's default value if it does not exist in the preferences map */
+ /**
+ * Gets a global preference, or it's default value if it does not exist in the preferences map
+ *
+ * @param {GlobalPreference} key - The name of the preference to get.
+ * @returns {T} The value of this preference casted to type T.
+ */
public static getGlobalPreference(key: GlobalPreference): T {
const customPref = this.getPreference(key)
if (customPref != undefined) return customPref
@@ -47,7 +58,12 @@ class PreferencesSystem {
throw new Error("Preference '" + key + "' is not assigned a default!")
}
- /** Sets a global preference to be a value of a specific type */
+ /**
+ * Sets a global preference to be a value of a specific type
+ *
+ * @param {GlobalPreference} key - The name of the preference to set.
+ * @param {T} value - The value to set the preference to.
+ */
public static setGlobalPreference(key: GlobalPreference, value: T) {
if (this._preferences == undefined) this.loadPreferences()
@@ -55,7 +71,10 @@ class PreferencesSystem {
this._preferences[key] = value
}
- /** Gets a RobotPreferences object for a robot of a specific mira name */
+ /**
+ * @param {string} miraName - The name of the robot assembly to get preference for.
+ * @returns {RobotPreferences} Robot preferences found for the given robot, or default robot preferences if none are found.
+ */
public static getRobotPreferences(miraName: string): RobotPreferences {
const allRoboPrefs = this.getAllRobotPreferences()
@@ -74,7 +93,7 @@ class PreferencesSystem {
allRoboPrefs[miraName] = value
}
- /** Gets preferences for every robot in local storage */
+ /** @returns Preferences for every robot that was found in local storage. */
public static getAllRobotPreferences(): { [key: string]: RobotPreferences } {
let allRoboPrefs = this.getPreference<{ [key: string]: RobotPreferences }>(RobotPreferencesKey)
@@ -86,7 +105,10 @@ class PreferencesSystem {
return allRoboPrefs
}
- /** Gets a FieldPreferences object for a robot of a specific mira name */
+ /**
+ * @param {string} miraName - The name of the field assembly to get preference for.
+ * @returns {FieldPreferences} Field preferences found for the given field, or default field preferences if none are found.
+ */
public static getFieldPreferences(miraName: string): FieldPreferences {
const allFieldPrefs = this.getAllFieldPreferences()
@@ -99,7 +121,7 @@ class PreferencesSystem {
return allFieldPrefs[miraName]
}
- /** Gets preferences for every robot in local storage */
+ /** @returns Preferences for every field that was found in local storage. */
public static getAllFieldPreferences(): { [key: string]: FieldPreferences } {
let allFieldPrefs = this.getPreference<{ [key: string]: FieldPreferences }>(FieldPreferencesKey)
@@ -111,7 +133,7 @@ class PreferencesSystem {
return allFieldPrefs
}
- /** Load all preferences from local storage */
+ /** Loads all preferences from local storage. */
public static loadPreferences() {
const loadedPrefs = window.localStorage.getItem(this._localStorageKey)
@@ -128,7 +150,7 @@ class PreferencesSystem {
}
}
- /** Save all preferences to local storage */
+ /** Saves all preferences to local storage. */
public static savePreferences() {
if (this._preferences == undefined) {
console.log("Preferences not loaded!")
@@ -145,7 +167,7 @@ class PreferencesSystem {
window.localStorage.setItem(this._localStorageKey, prefsString)
}
- /** Remove all preferences from local storage */
+ /** Removes all preferences from local storage. */
public static clearPreferences() {
window.localStorage.removeItem(this._localStorageKey)
this._preferences = {}
diff --git a/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts b/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts
index 03db0cdf2c..a58c1bdb3a 100644
--- a/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts
+++ b/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts
@@ -40,6 +40,7 @@ class SynthesisBrain extends Brain {
// Tracks the number of each specific mira file spawned
public static numberRobotsSpawned: { [key: string]: number } = {}
+ /** @returns {string} The name of the input scheme attached to this brain. */
public get inputSchemeName(): string {
const scheme = InputSystem.brainIndexSchemeMap.get(this._brainIndex)
if (scheme == undefined) return "Not Configured"
@@ -47,10 +48,15 @@ class SynthesisBrain extends Brain {
return scheme.schemeName
}
+ /** @returns {number} The unique index used to identify this brain. */
public get brainIndex(): number {
return this._brainIndex
}
+ /**
+ * @param mechanism The mechanism this brain will control.
+ * @param assemblyName The name of the assembly that corresponds to the mechanism used for identification.
+ */
public constructor(mechanism: Mechanism, assemblyName: string) {
super(mechanism)
@@ -89,7 +95,7 @@ class SynthesisBrain extends Brain {
InputSystem.brainIndexSchemeMap.delete(this._brainIndex)
}
- // Creates an instance of ArcadeDriveBehavior and automatically configures it
+ /** Creates an instance of ArcadeDriveBehavior and automatically configures it. */
private configureArcadeDriveBehavior() {
const wheelDrivers: WheelDriver[] = this._simLayer.drivers.filter(
driver => driver instanceof WheelDriver
@@ -134,7 +140,7 @@ class SynthesisBrain extends Brain {
)
}
- // Creates instances of ArmBehavior and automatically configures them
+ /** Creates instances of ArmBehavior and automatically configures them. */
private configureArmBehaviors() {
const hingeDrivers: HingeDriver[] = this._simLayer.drivers.filter(
driver => driver instanceof HingeDriver
@@ -171,7 +177,7 @@ class SynthesisBrain extends Brain {
}
}
- // Creates instances of ElevatorBehavior and automatically configures them
+ /** Creates instances of ElevatorBehavior and automatically configures them. */
private configureElevatorBehaviors() {
const sliderDrivers: SliderDriver[] = this._simLayer.drivers.filter(
driver => driver instanceof SliderDriver
@@ -208,6 +214,7 @@ class SynthesisBrain extends Brain {
}
}
+ /** Gets field preferences and handles any field specific configuration. */
private configureField() {
PreferencesSystem.getFieldPreferences(this._assemblyName)
diff --git a/fission/src/test/InputSystem.test.ts b/fission/src/test/InputSystem.test.ts
new file mode 100644
index 0000000000..fc63f5e829
--- /dev/null
+++ b/fission/src/test/InputSystem.test.ts
@@ -0,0 +1,79 @@
+import { test, describe, assert, expect } from "vitest"
+import InputSystem, { EmptyModifierState, ModifierState } from "@/systems/input/InputSystem"
+import InputSchemeManager from "@/systems/input/InputSchemeManager"
+import DefaultInputs from "@/systems/input/DefaultInputs"
+
+describe("Input Scheme Manager Checks", () => {
+ test("Available Schemes", () => {
+ assert(InputSchemeManager.availableInputSchemes[0].schemeName == DefaultInputs.ernie().schemeName)
+ assert(InputSchemeManager.defaultInputSchemes.length >= 1)
+
+ const startingLength = InputSchemeManager.availableInputSchemes.length
+ InputSchemeManager.addCustomScheme(DefaultInputs.newBlankScheme)
+
+ expect(InputSchemeManager.availableInputSchemes.length).toBe(startingLength + 1)
+ })
+ test("Add a Custom Scheme", () => {
+ const startingLength = InputSchemeManager.availableInputSchemes.length
+ InputSchemeManager.addCustomScheme(DefaultInputs.newBlankScheme)
+
+ assert((InputSchemeManager.availableInputSchemes.length = startingLength + 1))
+ })
+ test("Get Random Names", () => {
+ const names: string[] = []
+ for (let i = 0; i < 20; i++) {
+ const name = InputSchemeManager.randomAvailableName
+ expect(names.includes(name)).toBe(false)
+ assert(name != undefined)
+ expect(name.length).toBeGreaterThan(0)
+
+ const scheme = DefaultInputs.newBlankScheme
+ scheme.schemeName = name
+
+ InputSchemeManager.addCustomScheme(scheme)
+
+ names.push(name)
+ }
+ })
+})
+
+describe("Input System Checks", () => {
+ const inputSystem = new InputSystem()
+
+ test("Brain Map Exists?", () => {
+ assert(InputSystem.brainIndexSchemeMap != undefined)
+ })
+
+ test("Inputs are Zero", () => {
+ expect(InputSystem.getInput("arcadeDrive", 0)).toBe(0)
+ expect(InputSystem.getGamepadAxis(0)).toBe(0)
+ expect(InputSystem.getInput("randomInputThatDoesNotExist", 1273)).toBe(0)
+ expect(InputSystem.isKeyPressed("keyA")).toBe(false)
+ expect(InputSystem.isKeyPressed("ajhsekff")).toBe(false)
+ expect(InputSystem.isGamepadButtonPressed(1)).toBe(false)
+ })
+
+ test("Modifier State Comparison", () => {
+ const allFalse: ModifierState = {
+ alt: false,
+ ctrl: false,
+ shift: false,
+ meta: false,
+ }
+
+ const differentState: ModifierState = {
+ alt: false,
+ ctrl: true,
+ shift: false,
+ meta: true,
+ }
+
+ inputSystem.Update(-1)
+
+ expect(InputSystem.compareModifiers(allFalse, EmptyModifierState)).toBe(true)
+ expect(InputSystem.compareModifiers(allFalse, InputSystem.currentModifierState)).toBe(true)
+ expect(InputSystem.compareModifiers(differentState, InputSystem.currentModifierState)).toBe(false)
+ expect(InputSystem.compareModifiers(differentState, differentState)).toBe(true)
+ expect(InputSystem.compareModifiers(differentState, allFalse)).toBe(false)
+ })
+})
diff --git a/fission/src/test/PreferencesSystem.test.ts b/fission/src/test/PreferencesSystem.test.ts
new file mode 100644
index 0000000000..22828937d7
--- /dev/null
+++ b/fission/src/test/PreferencesSystem.test.ts
@@ -0,0 +1,34 @@
+import PreferencesSystem from "@/systems/preferences/PreferencesSystem"
+import { test, describe, expect } from "vitest"
+
+describe("Preferences System", () => {
+ test("Setting without saving", () => {
+ PreferencesSystem.setGlobalPreference("ZoomSensitivity", 15)
+ PreferencesSystem.setGlobalPreference("RenderSceneTags", true)
+ PreferencesSystem.setGlobalPreference("RenderScoreboard", false)
+
+ expect(PreferencesSystem.getGlobalPreference("ZoomSensitivity")).toBe(15)
+ expect(PreferencesSystem.getGlobalPreference("RenderSceneTags")).toBe(true)
+ expect(PreferencesSystem.getGlobalPreference("RenderScoreboard")).toBe(false)
+ })
+ test("Reset to default if undefined", () => {
+ PreferencesSystem.setGlobalPreference("ZoomSensitivity", undefined)
+ PreferencesSystem.setGlobalPreference("RenderSceneTags", undefined)
+ PreferencesSystem.setGlobalPreference("RenderScoreboard", undefined)
+
+ expect(PreferencesSystem.getGlobalPreference("ZoomSensitivity")).toBe(15)
+ expect(PreferencesSystem.getGlobalPreference("RenderSceneTags")).toBe(true)
+ expect(PreferencesSystem.getGlobalPreference("RenderScoreboard")).toBe(true)
+ })
+ test("Setting then saving", () => {
+ PreferencesSystem.setGlobalPreference("ZoomSensitivity", 13)
+ PreferencesSystem.setGlobalPreference("RenderSceneTags", true)
+ PreferencesSystem.setGlobalPreference("RenderScoreboard", false)
+
+ PreferencesSystem.savePreferences()
+
+ expect(PreferencesSystem.getGlobalPreference("ZoomSensitivity")).toBe(13)
+ expect(PreferencesSystem.getGlobalPreference("RenderSceneTags")).toBe(true)
+ expect(PreferencesSystem.getGlobalPreference("RenderScoreboard")).toBe(false)
+ })
+})
diff --git a/fission/src/test/ui/Button.test.tsx b/fission/src/test/ui/Button.test.tsx
new file mode 100644
index 0000000000..f911494d09
--- /dev/null
+++ b/fission/src/test/ui/Button.test.tsx
@@ -0,0 +1,29 @@
+import { render, fireEvent, getByText } from "@testing-library/react"
+import { assert, describe, expect, test } from "vitest"
+import Button from "@/ui/components/Button"
+
+describe("Button", () => {
+ test("Click Enabled Button", () => {
+ let buttonClicked = false
+ const container = render(