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..92e739da77 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -36,13 +36,13 @@ 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) + wheels: list[Wheel] = field(default_factory=list) + joints: list[Joint] = field(default_factory=list) + gamepieces: list[Gamepiece] = field(default_factory=list) preferredUnits: PreferredUnits = field(default=PreferredUnits.IMPERIAL) # Always stored in kg regardless of 'preferredUnits' 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/Types.py b/exporter/SynthesisFusionAddin/src/Types.py index 1aca3a5162..5e2d7c967f 100644 --- a/exporter/SynthesisFusionAddin/src/Types.py +++ b/exporter/SynthesisFusionAddin/src/Types.py @@ -1,9 +1,9 @@ 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 # Not 100% sure what this is for - Brandon JointParentType = Enum("JointParentType", ["ROOT", "END"]) @@ -17,18 +17,18 @@ @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 +39,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 +72,22 @@ class ModelHierarchy(Enum): SingleMesh = 3 -class LBS(float): - """Mass Unit in Pounds.""" +LBS: TypeAlias = float +KG: TypeAlias = float -class KG(float): - """Mass Unit in Kilograms.""" - - -def toLbs(kgs: float) -> LBS: +def toLbs(kgs: KG) -> LBS: return LBS(round(kgs * 2.2062, 2)) -def toKg(pounds: float) -> KG: +def toKg(pounds: LBS) -> KG: return KG(round(pounds / 2.2062, 2)) 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..cbdd5f1881 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,13 +302,17 @@ 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 @@ -326,7 +331,7 @@ def notify(self, args): units = gamepieceConfigTab.selectedUnits exporterOptions = ExporterOptions( - savepath, + str(savepath), name, version, materials=0, @@ -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..d3e26f1f99 100644 --- a/exporter/SynthesisFusionAddin/src/UI/GamepieceConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/GamepieceConfigTab.py @@ -102,12 +102,16 @@ 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 isActive(self) -> bool: + return self.gamepieceConfigTab.isActive or False + @property def selectedUnits(self) -> PreferredUnits: return self.currentUnits @@ -117,7 +121,7 @@ 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]: @@ -252,6 +256,17 @@ def calcGamepieceWeights(self) -> None: 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) @@ -283,18 +298,6 @@ def handleInputChanged( 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 +305,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 +313,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..673e56054b 100644 --- a/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py @@ -2,13 +2,8 @@ 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.Parser.ExporterOptions import ExporterOptions +from src.Types import KG, ExportLocation, ExportMode, PreferredUnits, toKg, toLbs from src.UI import IconPaths from src.UI.CreateCommandInputsHelper import createBooleanInput, createTableInput from src.UI.GamepieceConfigTab import GamepieceConfigTab @@ -49,7 +44,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" @@ -160,6 +155,10 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: Exp weightInput.isVisible = weightTableInput.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,14 +175,14 @@ 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 + return exportAsPartButton.value or False @property def selectedUnits(self) -> PreferredUnits: @@ -205,7 +204,7 @@ 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 +222,27 @@ 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") + weightTable: adsk.core.TableCommandInput = args.inputs.itemById("weightTable") + weightInput: adsk.core.ValueCommandInput = weightTable.getInputAtPosition(0, 1) + 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 @@ -268,8 +269,6 @@ def handleInputChanged(self, args: adsk.core.InputChangedEventArgs) -> None: 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 @@ -294,9 +293,6 @@ def handleInputChanged(self, args: adsk.core.InputChangedEventArgs) -> None: 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) @@ -308,7 +304,6 @@ 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 @@ -331,4 +326,4 @@ def designMassCalculation() -> KG: physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) mass += physical.mass - return KG(round(mass, 2)) + return 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/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..9916cbb853 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Util.py @@ -0,0 +1,7 @@ +import os + + +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 812e2255df..428df4a422 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", @@ -74,5 +75,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..386da3280d 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -41,7 +41,6 @@ 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" @@ -166,7 +165,7 @@ function Synthesis() { closeAllPanels={closeAllPanels} > - + {panelElements.length > 0 && panelElements} @@ -193,9 +192,6 @@ function Synthesis() { const initialModals = [ , - , - , - , , , , 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/systems/input/InputSystem.ts b/fission/src/systems/input/InputSystem.ts index d56aadd692..cf7148552c 100644 --- a/fission/src/systems/input/InputSystem.ts +++ b/fission/src/systems/input/InputSystem.ts @@ -159,9 +159,21 @@ class InputSystem extends WorldSystem { this.gamepadDisconnected = this.gamepadDisconnected.bind(this) window.addEventListener("gamepaddisconnected", this.gamepadDisconnected) + // Detect when the user leaves the page to clear inputs 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 { diff --git a/fission/src/ui/components/Button.tsx b/fission/src/ui/components/Button.tsx index 997a2081af..c5fe2e115e 100644 --- a/fission/src/ui/components/Button.tsx +++ b/fission/src/ui/components/Button.tsx @@ -42,13 +42,23 @@ const Button: React.FC = ({ value, colorOverrideClass, sizeOverride return ( {value} diff --git a/fission/src/ui/components/Checkbox.tsx b/fission/src/ui/components/Checkbox.tsx index 6ebdd578f8..48cbcddf31 100644 --- a/fission/src/ui/components/Checkbox.tsx +++ b/fission/src/ui/components/Checkbox.tsx @@ -1,7 +1,8 @@ import React, { useState } from "react" -import Stack, { StackDirection } from "./Stack" import Label, { LabelSize } from "./Label" import { Switch } from "@mui/base/Switch" +import { Box } from "@mui/material" +import { LabelWithTooltip } from "./StyledComponents" type CheckboxProps = { label: string @@ -10,13 +11,30 @@ type CheckboxProps = { stateOverride?: boolean hideLabel?: boolean onClick?: (checked: boolean) => void + tooltipText?: string } -const Checkbox: React.FC = ({ label, className, defaultState, stateOverride, hideLabel, onClick }) => { +const Checkbox: React.FC = ({ + label, + className, + defaultState, + stateOverride, + hideLabel, + onClick, + tooltipText, +}) => { const [state] = useState(defaultState) return ( - - {hideLabel ? null : ( + + {hideLabel ? null : tooltipText ? ( + LabelWithTooltip(label, tooltipText) + ) : ( @@ -25,14 +43,14 @@ const Checkbox: React.FC = ({ label, className, defaultState, sta onChange={(e: React.ChangeEvent) => onClick && onClick(e.target.checked)} slotProps={{ root: { - className: `group relative inline-block w-[24px] h-[24px] m-2.5 cursor-pointer`, + className: `group relative inline-block w-[24px] h-[24px] m-2.5 cursor-pointer transform transition-transform hover:scale-[1.03] active:scale-[1.06]`, }, input: { className: `cursor-inherit absolute w-full h-full top-0 left-0 opacity-0 z-10 border-none`, }, track: ownerState => { return { - className: `absolute block w-full h-full transition rounded-full border border-solid outline-none border-interactive-element-right dark:border-interactive-element-right group-[.base--focusVisible]:shadow-outline-switch ${ownerState.checked ? "bg-gradient-to-br from-interactive-element-left to-interactive-element-right" : "bg-background-secondary"}`, + className: `absolute block w-full h-full transition rounded-full border border-solid outline-none border-interactive-element-right dark:border-interactive-element-right group-[.base--focusVisible]:shadow-outline-switch ${ownerState.checked ? "bg-gradient-to-br from-interactive-element-left to-interactive-element-right" : "bg-background-secondary"} transform transition-transform group-hover:scale-[1.03] group-active:scale-[1.06]`, } }, thumb: { @@ -41,7 +59,7 @@ const Checkbox: React.FC = ({ label, className, defaultState, sta }} defaultChecked={stateOverride != null ? undefined : state} /> - + ) } diff --git a/fission/src/ui/components/Dropdown.tsx b/fission/src/ui/components/Dropdown.tsx index d5ba480505..378a4aa040 100644 --- a/fission/src/ui/components/Dropdown.tsx +++ b/fission/src/ui/components/Dropdown.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from "react" -import { styled } from "@mui/system" +import { alpha, styled } from "@mui/system" import { Menu, MenuItem, Button, Tooltip } from "@mui/material" import { colorNameToVar } from "../ThemeContext" @@ -35,6 +35,9 @@ const CustomButton = styled(Button)({ border: "0 !important", backgroundColor: colorNameToVar("BackgroundSecondary"), }, + "& .MuiTouchRipple-root": { + color: "#ffffff30", + }, }) const CustomMenu = styled(Menu)({ @@ -45,9 +48,17 @@ const CustomMenu = styled(Menu)({ minWidth: "unset", }, "& .MuiMenuItem-root": { - "transition": "background-color 0.3s ease, color 0.3s ease", + "transition": "background-color 0.3s ease, color 0.3s ease, transform 0.2s ease", + "transform": "scale(1.06)", "&:hover": { color: "#da6659", + transform: "scale(1.05)", + }, + "&:active": { + transform: "scale(1.03)", + }, + "& .MuiTouchRipple-root": { + color: alpha("#d44a3e", 0.3), }, }, }) @@ -102,7 +113,11 @@ const Dropdown: React.FC = ({ options, defaultValue, onSelect, la )}
- + {selectedValue || "Select an option"}
diff --git a/fission/src/ui/components/Label.tsx b/fission/src/ui/components/Label.tsx index 1aa786aacd..96d569411f 100644 --- a/fission/src/ui/components/Label.tsx +++ b/fission/src/ui/components/Label.tsx @@ -29,7 +29,12 @@ type LabelProps = { } const Label: React.FC = ({ children, size, className }) => ( - {children} + + {children} + ) export default Label diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index e42c0f00c9..517af3e30c 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react" -import { BiMenuAltLeft } from "react-icons/bi" import { FaXmark } from "react-icons/fa6" import { useModalControlContext } from "@/ui/ModalContext" import { usePanelControlContext } from "@/ui/PanelContext" @@ -8,8 +7,9 @@ import logo from "@/assets/autodesk_logo.png" import { ToastType, useToastContext } from "@/ui/ToastContext" import APS, { APS_USER_INFO_UPDATE_EVENT } from "@/aps/APS" import { UserIcon } from "./UserIcon" -import { Button } from "@mui/base/Button" import { ButtonIcon, SynthesisIcons } from "./StyledComponents" +import { Button } from "@mui/base" +import { Box } from "@mui/material" type ButtonProps = { value: string @@ -23,11 +23,23 @@ const MainHUDButton: React.FC = ({ value, icon, onClick, larger }) return ( ) } @@ -58,12 +70,36 @@ const MainHUD: React.FC = () => { return ( <> {!isOpen && ( - + + + setIsOpen(!isOpen)} + value={SynthesisIcons.OpenHudIcon} + className="" + /> + + + )} { className="fixed flex flex-col gap-2 bg-gradient-to-b from-interactive-element-right to-interactive-element-left w-min p-4 rounded-3xl ml-4 top-1/2 -translate-y-1/2" >
- + } + value={} onClick={() => setIsOpen(false)} />
@@ -84,13 +129,17 @@ const MainHUD: React.FC = () => { larger={true} onClick={() => openPanel("import-mirabuf")} /> -
+ openModal("settings")} /> - {/* openModal("view")} @@ -101,18 +150,18 @@ const MainHUD: React.FC = () => { onClick={() => openPanel("subsystem-config")} /> openPanel("configure")} /> { openPanel("debug") }} /> -
+ {userInfo ? ( = ({ {name && ( )}
{children}
- + {(cancelEnabled || middleEnabled || acceptEnabled) && ( + + )} ) diff --git a/fission/src/ui/components/Panel.tsx b/fission/src/ui/components/Panel.tsx index b8faee4977..bed0ec4fa6 100644 --- a/fission/src/ui/components/Panel.tsx +++ b/fission/src/ui/components/Panel.tsx @@ -132,7 +132,17 @@ const Panel: React.FC = ({ {name && ( )}
= ({ }} className={`${ cancelBlocked ? "bg-interactive-background" : "bg-cancel-button" - } rounded-md cursor-pointer px-4 py-1 font-bold duration-100 hover:brightness-90`} + } rounded-md cursor-pointer px-4 py-1 font-bold duration-100 hover:brightness-90 + transform transition-transform hover:scale-[1.03] active:scale-[1.06]`} + style={{ fontWeight: "bold" }} /> )} {middleEnabled && ( @@ -170,7 +182,9 @@ const Panel: React.FC = ({ }} className={`${ middleBlocked ? "bg-interactive-background" : "bg-accept-button" - } rounded-md cursor-pointer px-4 py-1 font-bold duration-100 hover:brightness-90`} + } rounded-md cursor-pointer px-4 py-1 font-bold duration-100 hover:brightness-90 + transform transition-transform hover:scale-[1.03] active:scale-[1.06]`} + style={{ fontWeight: "bold" }} /> )} {acceptEnabled && ( @@ -183,7 +197,9 @@ const Panel: React.FC = ({ }} className={`${ acceptBlocked ? "bg-interactive-background" : "bg-accept-button" - } rounded-md cursor-pointer px-4 py-1 font-bold duration-100 hover:brightness-90`} + } rounded-md cursor-pointer px-4 py-1 font-bold duration-100 hover:brightness-90 + transform transition-transform hover:scale-[1.03] active:scale-[1.06]`} + style={{ fontWeight: "bold" }} /> )}
diff --git a/fission/src/ui/components/SelectButton.tsx b/fission/src/ui/components/SelectButton.tsx index 3d7e018669..65e277dbb5 100644 --- a/fission/src/ui/components/SelectButton.tsx +++ b/fission/src/ui/components/SelectButton.tsx @@ -1,10 +1,10 @@ import React, { useCallback, useEffect, useRef, useState } from "react" import Button, { ButtonSize } from "./Button" -import Label, { LabelSize } from "./Label" import Stack, { StackDirection } from "./Stack" import World from "@/systems/World" import { ThreeVector3_JoltVec3 } from "@/util/TypeConversions" import Jolt from "@barclah/jolt-physics" +import { LabelWithTooltip } from "./StyledComponents" // raycasting constants const RAY_MAX_LENGTH = 20.0 @@ -70,7 +70,10 @@ const SelectButton: React.FC = ({ colorClass, size, value, pl return ( - + {LabelWithTooltip( + "Select parent node", + "Select the parent node for this object to follow. Click the button below, then click a part of the robot or field." + )}