diff --git a/.gitattributes b/.gitattributes index 176a458f94..ebbf058172 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,6 @@ * text=auto + +# Workaround for a bug in GitHub's language detection, +# previously half of our code was being counted as JavaScript.. for some reason... +# and we certainly can't have that. - Brandon +*.js linguist-detectable=false diff --git a/.github/workflows/FissionUnitTest.yml b/.github/workflows/FissionUnitTest.yml index 1a0389244b..b3dc4e6be2 100644 --- a/.github/workflows/FissionUnitTest.yml +++ b/.github/workflows/FissionUnitTest.yml @@ -44,7 +44,7 @@ jobs: with: path: | ~/.cache/ms-playwright/ - key: ${{ runner.os }}-assets-playwright-${{ env.PLAYWRIGHT_VERSION }} + key: ${{ runner.os }}-assets-playwright-${{ env.PLAYWRIGHT_VERSION }}-v2 - name: Install Dependencies run: | 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/.gitmodules b/.gitmodules index 44da10b0bf..8f7b277463 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "mirabuf"] path = mirabuf url = https://github.com/HiceS/mirabuf.git +[submodule "jolt"] + path = jolt + url = https://github.com/HunterBarclay/JoltPhysics.js.git diff --git a/README.md b/README.md index b9c8e56aa0..cd19a6d963 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ If you're a developer who wants to contribute to Synthesis, you're in the right - [Fission (Core Web App)](/fission/README.md) - [Fusion Exporter (Fusion exporter to Mirabuf file format)](/exporter/SynthesisFusionAddin/README.md) -- [Installers](/installer/) +- [Fusion Exporter Installer](/installer/) Follow the above links to the respective READMEs on how to build and run each component. ### Compatibility Notes -As Fusion is not supported on linux, the linux installer does not come with the Fusion Addin for exporting robots and fields. +As Fusion is not officially supported on Linux, we do not provide an installer for the Fusion Exporter on Linux. ## Contributing @@ -60,6 +60,10 @@ All code is under a configured formatting utility. See each component for more d Mirabuf is a file format we use to store physical data from Fusion to load into the Synthesis simulator (Fission). This is a separate project that is a submodule of Synthesis. [See Mirabuf](https://github.com/HiceS/mirabuf/) +### Jolt Physics + +Jolt is the core physics engine for our web biased simulator. [See JoltPhysics.js](https://github.com/HunterBarclay/JoltPhysics.js) for more information. + ### Tutorials Our source code for the tutorials featured on our [Tutorials Page](https://synthesis.autodesk.com/tutorials.html). diff --git a/exporter/SynthesisFusionAddin/README.md b/exporter/SynthesisFusionAddin/README.md index 4fc4933373..1e61d0735c 100644 --- a/exporter/SynthesisFusionAddin/README.md +++ b/exporter/SynthesisFusionAddin/README.md @@ -33,15 +33,14 @@ We use `VSCode` Primarily, download it to interact with our code or use your own ### How to Build + Run -1. See root [`README`](/README.md) on how to run `init` script -2. Open `Autodesk Fusion` -3. Select `UTILITIES` from the top bar -4. Click `ADD-INS` Button -5. Click `Add-Ins` tab at the top of Scripts and Add-Ins dialog -6. Press + Button under **My Add-Ins** -7. Navigate to the containing folder for this Addin and click open at bottom - _clone-directory_/synthesis/exporters/SynthesisFusionAddin -8. Synthesis should be an option - select it and click run at the bottom of the dialog -9. There should now be a button that says Synthesis in your utilities menu +1. Open `Autodesk Fusion` +2. Select `UTILITIES` from the top bar +3. Click `ADD-INS` Button +4. Click `Add-Ins` tab at the top of Scripts and Add-Ins dialog +5. Press + Button under **My Add-Ins** +6. Navigate to the containing folder for this Addin and click open at bottom - _clone-directory_/synthesis/exporters/SynthesisFusionAddin +7. Synthesis should be an option - select it and click run at the bottom of the dialog +8. There should now be a button that says Synthesis in your utilities menu - If there is no button there may be a problem - see below for [checking log file](#debug-non-start) --- 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 478c6c9aa2..109460f808 100644 --- a/exporter/SynthesisFusionAddin/Synthesis.py +++ b/exporter/SynthesisFusionAddin/Synthesis.py @@ -1,38 +1,55 @@ -# DO NOT CHANGE ORDER, OR ADD IMPORTS BEFORE UNTIL END COMMENT - import os -from shutil import rmtree +import sys +from typing import Any import adsk.core -from .src.general_imports import APP_NAME, DESCRIPTION, INTERNAL_ID, gm -from .src.Logging import getLogger, logFailure, setupLogger -from .src.UI import ( +# Required for absolute imports. +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from src.Dependencies import resolveDependencies +from src.Logging import logFailure, setupLogger + +logger = setupLogger() + +try: + # Attempt to import required pip dependencies to verify their installation. + import requests + + from src.Proto import ( + assembly_pb2, + joint_pb2, + material_pb2, + motor_pb2, + signal_pb2, + types_pb2, + ) +except (ImportError, ModuleNotFoundError, BaseException) as error: # BaseException required to catch proto.VersionError + logger.warn(f"Running resolve dependencies with error of:\n{error}") + result = resolveDependencies() + if result: + adsk.core.Application.get().userInterface.messageBox("Installed required dependencies.\nPlease restart Fusion.") + + +from src import APP_NAME, DESCRIPTION, INTERNAL_ID, gm +from src.UI import ( HUI, Camera, ConfigCommand, - Handlers, - Helper, MarkingMenu, ShowAPSAuthCommand, + ShowWebsiteCommand, ) -from .src.UI.Toolbar import Toolbar - -# END OF RESTRICTION - -# Transition: AARD-1721 -# Should attempt to fix this ordering scheme within AARD-1741 -from .src.APS import APS # isort:skip +from src.UI.Toolbar import Toolbar @logFailure -def run(_): +def run(_context: dict[str, Any]) -> None: """## Entry point to application from Fusion. Arguments: **context** *context* -- Fusion context to derive app and UI. """ - setupLogger() # Remove all items prior to start just to make sure unregister_all() @@ -47,7 +64,7 @@ def run(_): @logFailure -def stop(_): +def stop(_context: dict[str, Any]) -> None: """## Fusion exit point - deconstructs buttons and handlers Arguments: @@ -62,28 +79,8 @@ def stop(_): # nm.deleteMe() - logger = getLogger(INTERNAL_ID) logger.cleanupHandlers() - - for file in gm.files: - try: - os.remove(file) - except OSError: - pass - - # removes path so that proto files don't get confused - - import sys - - path = os.path.abspath(os.path.dirname(__file__)) - - path_proto_files = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "proto", "proto_out")) - - if path in sys.path: - sys.path.remove(path) - - if path_proto_files in sys.path: - sys.path.remove(path_proto_files) + gm.clear() @logFailure @@ -124,8 +121,18 @@ def register_ui() -> None: work_panel, lambda *_: True, # TODO: Should be redone with various refactors. ShowAPSAuthCommand.ShowAPSAuthCommandCreatedHandler, - description=f"APS", + description=f"Login to your Autodesk account", command=True, ) gm.elements.append(apsButton) + + websiteButton = HUI.HButton( + "Synthesis Website", + work_panel, + lambda *_: True, + ShowWebsiteCommand.ShowWebsiteCommandCreatedHandler, + description=f"Open our tutorials page", + command=True, + ) + gm.elements.append(websiteButton) 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/proto/deps.py b/exporter/SynthesisFusionAddin/proto/deps.py deleted file mode 100644 index 10ec7757b3..0000000000 --- a/exporter/SynthesisFusionAddin/proto/deps.py +++ /dev/null @@ -1,191 +0,0 @@ -import os -import platform -from pathlib import Path - -import adsk.core -import adsk.fusion - -from src.Logging import getLogger, logFailure - -system = platform.system() -logger = getLogger() - - -@logFailure(messageBox=True) -def getPythonFolder() -> str: - """Retreives the folder that contains the Autodesk python executable - - Raises: - ImportError: Unrecognized Platform - - Returns: - str: The path that contains the Autodesk python executable - """ - - # Thank you Kris Kaplan - import importlib.machinery - import sys - - osPath = importlib.machinery.PathFinder.find_spec("os", sys.path).origin - - # The location of the python executable is found relative to the location of the os module in each operating system - if system == "Windows": - pythonFolder = Path(osPath).parents[1] - elif system == "Darwin": - pythonFolder = f"{Path(osPath).parents[2]}/bin" - else: - raise ImportError("Unsupported platform! This add-in only supports windows and macos") - - logger.debug(f"Python Folder -> {pythonFolder}") - return pythonFolder - - -def executeCommand(command: tuple) -> int: - """Abstracts the execution of commands to account for platform differences - - Args: - command (tuple): Tuple starting with command, and each indice having the arguments for said command - - Returns: - int: Exit code of the process - """ - - joinedCommand = str.join(" ", command) - logger.debug(f"Command -> {joinedCommand}") - executionResult = os.system(joinedCommand) - - return executionResult - - -@logFailure(messageBox=True) -def installCross(pipDeps: list) -> bool: - """Attempts to fetch pip script and resolve dependencies with less user interaction - - Args: - pipDeps (list): List of all string imports - - Returns: - bool: Success - - Notes: - Liam originally came up with this style after realizing accessing the python dir was too unreliable. - """ - app = adsk.core.Application.get() - ui = app.userInterface - - if app.isOffLine: - ui.messageBox( - "Unable to install dependencies when Fusion is offline. Please connect to the internet and try again!" - ) - return False - - progressBar = ui.createProgressDialog() - progressBar.isCancelButtonShown = False - progressBar.reset() - progressBar.show("Synthesis", f"Installing dependencies...", 0, len(pipDeps), 0) - - # this is important to reduce the chance of hang on startup - adsk.doEvents() - - try: - pythonFolder = getPythonFolder() - except ImportError as e: - logger.error(f"Failed to download dependencies: {e.msg}") - return False - - if system == "Darwin": # macos - # if nothing has previously fetched it - if not os.path.exists(f"{pythonFolder}/get-pip.py"): - executeCommand( - [ - "curl", - "https://bootstrap.pypa.io/get-pip.py", - "-o", - f'"{pythonFolder}/get-pip.py"', - ] - ) - - executeCommand([f'"{pythonFolder}/python"', f'"{pythonFolder}/get-pip.py"']) - - pythonExecutable = "python" - if system == "Windows": - pythonExecutable = "python.exe" - - for depName in pipDeps: - progressBar.progressValue += 1 - progressBar.message = f"Installing {depName}..." - adsk.doEvents() - - # os.path.join needed for varying system path separators - installResult = executeCommand( - [ - f'"{os.path.join(pythonFolder, pythonExecutable)}"', - "-m", - "pip", - "install", - depName, - ] - ) - if installResult != 0: - logger.warn(f'Dep installation "{depName}" exited with code "{installResult}"') - - if system == "Darwin": - pipAntiDeps = ["dataclasses", "typing"] - progressBar.progressValue = 0 - progressBar.maximumValue = len(pipAntiDeps) - for depName in pipAntiDeps: - progressBar.message = f"Uninstalling {depName}..." - progressBar.progressValue += 1 - adsk.doEvents() - uninstallResult = executeCommand( - [ - f'"{os.path.join(pythonFolder, pythonExecutable)}"', - "-m", - "pip", - "uninstall", - f"{depName}", - "-y", - ] - ) - if uninstallResult != 0: - logger.warn(f'AntiDep uninstallation "{depName}" exited with code "{uninstallResult}"') - - progressBar.hide() - - if _checkDeps(): - return True - else: - # Will be caught and logged to a message box & log file from `@logFailure` - raise RuntimeError("Failed to install dependencies needed") - - -def _checkDeps() -> bool: - try: - from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 - - return True - except ImportError: - return False - - -""" -Checks for, and installs if need be, the dependencies needed by the Synthesis Exporter. Will error if it cannot install the dependencies -correctly. This should crash the exporter, since most of the exporter needs these dependencies to function in -the first place. -""" - - -def installDependencies(): - try: - import logging.handlers - - import google.protobuf - import pkg_resources - from requests import get, post - - from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 - except ImportError or ModuleNotFoundError: - installCross(["protobuf==4.23.3", "requests==2.32.3"]) - from requests import get, post - - from .proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 diff --git a/exporter/SynthesisFusionAddin/pyproject.toml b/exporter/SynthesisFusionAddin/pyproject.toml index 01222be485..92f6c6ec1d 100644 --- a/exporter/SynthesisFusionAddin/pyproject.toml +++ b/exporter/SynthesisFusionAddin/pyproject.toml @@ -13,6 +13,7 @@ skip = [ ".vscode/", "/dist/", "proto/proto_out", + "src/Proto", ] [tool.black] @@ -30,6 +31,7 @@ exclude = ''' | .vscode | dist | proto_out + | src/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 a65728ec23..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 @@ -10,13 +11,13 @@ import requests -from ..general_imports import INTERNAL_ID, gm, my_addin_path -from ..Logging import getLogger +from src import ADDIN_PATH, gm +from src.Logging import getLogger logger = getLogger() CLIENT_ID = "GCxaewcLjsYlK8ud7Ka9AKf9dPwMR3e4GlybyfhAK2zvl3tU" -auth_path = os.path.abspath(os.path.join(my_addin_path, "..", ".aps_auth")) +auth_path = os.path.abspath(os.path.join(ADDIN_PATH, "..", ".aps_auth")) APS_AUTH = None APS_USER_INFO = None @@ -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 new file mode 100644 index 0000000000..284a8ad942 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Dependencies.py @@ -0,0 +1,129 @@ +import importlib.machinery +import os +import subprocess +import sys +from pathlib import Path + +import adsk.core +import adsk.fusion + +from src import SYSTEM +from src.Logging import getLogger, logFailure + +logger = getLogger() + +# Since the Fusion python runtime is separate from the system python runtime we need to do some funky things +# in order to download and install python packages separate from the standard library. +PIP_DEPENDENCY_VERSION_MAP: dict[str, str] = { + "protobuf": "5.27.2", + "requests": "2.32.3", +} + + +@logFailure +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. + 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": + folder = f"{Path(pythonStandardLibraryModulePath).parents[1]}" + else: + assert SYSTEM == "Darwin" + folder = f"{Path(pythonStandardLibraryModulePath).parents[2]}/bin" + + return folder + + +def executeCommand(*args: str) -> subprocess.CompletedProcess[str]: + logger.debug(f"Running Command -> {' '.join(args)}") + try: + 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}") + return result + + except subprocess.CalledProcessError as error: + logger.error(f"Exit code: {error.returncode}") + logger.error(f"Output:\n{error.stderr}") + raise error + + +def getInstalledPipPackages(pythonExecutablePath: str) -> dict[str, str]: + result: str = executeCommand(pythonExecutablePath, "-m", "pip", "freeze").stdout + # We don't need to check against packages with a specific hash as those are not required by Synthesis. + return {x.split("==")[0]: x.split("==")[1] for x in result.splitlines() if "==" in x} + + +def packagesOutOfDate(installedPackages: dict[str, str]) -> bool: + for package, installedVersion in installedPackages.items(): + expectedVersion = PIP_DEPENDENCY_VERSION_MAP.get(package) + if expectedVersion and expectedVersion != installedVersion: + return True + + return False + + +@logFailure +def resolveDependencies() -> bool | None: + app = adsk.core.Application.get() + ui = app.userInterface + if app.isOffLine: + # If we have gotten this far that means that an import error was thrown for possible missing + # dependencies... And we can't try to download them because we have no internet... ¯\_(ツ)_/¯ + ui.messageBox("Unable to resolve dependencies while not connected to the internet.") + return False + + # This is important to reduce the chance of hang on startup. + adsk.doEvents() + + pythonFolder = getInternalFusionPythonInstillationFolder() + pythonExecutableFile = "python.exe" if SYSTEM == "Windows" else "python" # Confirming 110% everything is fine. + pythonExecutablePath = os.path.join(pythonFolder, pythonExecutableFile) + + progressBar = ui.createProgressDialog() + progressBar.isCancelButtonShown = False + progressBar.reset() + progressBar.show("Synthesis", f"Installing dependencies...", 0, len(PIP_DEPENDENCY_VERSION_MAP) * 2 + 2, 0) + + # Install pip manually on macos as it is not included by default? Really? + if SYSTEM == "Darwin" and not os.path.exists(os.path.join(pythonFolder, "pip")): + pipInstallScriptPath = os.path.join(pythonFolder, "get-pip.py") + if not os.path.exists(pipInstallScriptPath): + executeCommand("curl", "https://bootstrap.pypa.io/get-pip.py", "-o", pipInstallScriptPath) + progressBar.message = "Downloading PIP Installer..." + + progressBar.progressValue += 1 + progressBar.message = "Installing PIP..." + executeCommand(pythonExecutablePath, pipInstallScriptPath) + progressBar.progressValue += 1 + + installedPackages = getInstalledPipPackages(pythonExecutablePath) + if packagesOutOfDate(installedPackages): + # Uninstall and reinstall everything to confirm updated versions. + progressBar.message = "Uninstalling out-of-date Dependencies..." + + for dep in PIP_DEPENDENCY_VERSION_MAP.keys(): + progressBar.progressValue += 1 + executeCommand(pythonExecutablePath, "-m", "pip", "uninstall", "-y", dep) + else: + progressBar.progressValue += len(PIP_DEPENDENCY_VERSION_MAP) + + progressBar.message = "Installing Dependencies..." + for dep, version in PIP_DEPENDENCY_VERSION_MAP.items(): + progressBar.progressValue += 1 + progressBar.message = f"Installing {dep}..." + adsk.doEvents() + + result = executeCommand(pythonExecutablePath, "-m", "pip", "install", f"{dep}=={version}").returncode + if result: + logger.warn(f'Dep installation "{dep}" exited with code "{result}"') + + progressBar.hide() + return True diff --git a/exporter/SynthesisFusionAddin/src/GlobalManager.py b/exporter/SynthesisFusionAddin/src/GlobalManager.py index a31688d119..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. """ -import logging +from typing import Any import adsk.core import adsk.fusion -from .general_imports import * -from .strings import * +class GlobalManager: + def __init__(self) -> None: + self.app = adsk.core.Application.get() -class GlobalManager(object): - """Global Manager instance""" + if self.app: + self.ui = self.app.userInterface - class __GlobalManager: - def __init__(self): - self.app = adsk.core.Application.get() + self.connected = False + """ Is unity currently connected """ - if self.app: - self.ui = self.app.userInterface + self.uniqueIds: list[str] = [] # type of HButton + """ Collection of unique ID values to not overlap """ - self.connected = False - """ Is unity currently connected """ + self.elements: list[Any] = [] + """ Unique constructed buttons to delete """ - self.uniqueIds = [] - """ Collection of unique ID values to not overlap """ + # 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 """ - self.elements = [] - """ Unique constructed buttons to delete """ + self.handlers: list[adsk.core.EventHandler] = [] + """ Object to store all event handlers to custom events like saving. """ - self.palettes = [] - """ Unique constructed palettes to delete """ + self.tabs: list[adsk.core.ToolbarPanel] = [] + """ Set of Tab objects to keep track of. """ - self.handlers = [] - """ Object to store all event handlers to custom events like saving. """ + # 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 + """ - self.tabs = [] - """ Set of Tab objects to keep track of. """ + # Transition: AARD-1765 + # Will likely be removed later as this is no longer used. Avoiding adding typing for now. + self.files = [] # type: ignore - self.queue = [] - """ This will eventually implement the Python SimpleQueue synchronized workflow - - this is the list of objects being sent - """ + def __str__(self) -> str: + return "GlobalManager" - self.files = [] - - def __str__(self): - return "GlobalManager" - - instance = None - - def __new__(cls): - if not GlobalManager.instance: - GlobalManager.instance = GlobalManager.__GlobalManager() - - return GlobalManager.instance - - def __getattr__(self, name): - return getattr(self.instance, name) - - 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 2bf9ce191b..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 .strings import INTERNAL_ID -from .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: @@ -27,17 +27,20 @@ def cleanupHandlers(self) -> None: handler.close() -def setupLogger() -> 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,26 +49,28 @@ def setupLogger() -> None: logger = getLogger(INTERNAL_ID) logger.setLevel(10) # Debug logger.addHandler(logHandler) + 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__"): @@ -87,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 52ccd255bf..306f839cd4 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py +++ b/exporter/SynthesisFusionAddin/src/Parser/ExporterOptions.py @@ -11,9 +11,9 @@ import adsk.core from adsk.fusion import CalculationAccuracy, TriangleMeshQualityOptions -from ..Logging import logFailure, timed -from ..strings import INTERNAL_ID -from ..Types import ( +from src import INTERNAL_ID +from src.Logging import logFailure, timed +from src.Types import ( KG, ExportLocation, ExportMode, @@ -21,7 +21,6 @@ Joint, ModelHierarchy, PhysicalDepth, - PreferredUnits, Wheel, encodeNestedObjects, makeObjectFromJson, @@ -36,25 +35,25 @@ class ExporterOptions: fileLocation: str | None = field( default=(os.getenv("HOME") if platform.system() == "Windows" else os.path.expanduser("~")) ) - name: str = field(default=None) - version: str = field(default=None) + name: str | None = field(default=None) + version: str | None = field(default=None) materials: int = field(default=0) exportMode: ExportMode = field(default=ExportMode.ROBOT) - wheels: list[Wheel] = field(default=None) - joints: list[Joint] = field(default=None) - gamepieces: list[Gamepiece] = field(default=None) - preferredUnits: PreferredUnits = field(default=PreferredUnits.IMPERIAL) - - # Always stored in kg regardless of 'preferredUnits' - robotWeight: KG = field(default=0.0) + wheels: list[Wheel] = field(default_factory=list) + joints: list[Joint] = field(default_factory=list) + gamepieces: list[Gamepiece] = field(default_factory=list) + robotWeight: KG = field(default=KG(0.0)) + autoCalcRobotWeight: bool = field(default=False) + autoCalcGamepieceWeight: bool = field(default=False) frictionOverride: bool = field(default=False) - frictionOverrideCoeff: float | None = field(default=None) + frictionOverrideCoeff: float = field(default=0.5) compressOutput: bool = field(default=True) exportAsPart: bool = field(default=False) exportLocation: ExportLocation = field(default=ExportLocation.UPLOAD) + openSynthesisUponExport: bool = field(default=False) hierarchy: ModelHierarchy = field(default=ModelHierarchy.FusionAssembly) visualQuality: TriangleMeshQualityOptions = field(default=TriangleMeshQualityOptions.LowQualityTriangleMesh) @@ -68,9 +67,10 @@ def readFromDesign(self) -> "ExporterOptions": for field in fields(self): attribute = designAttributes.itemByName(INTERNAL_ID, field.name) if attribute: - attrJsonData = makeObjectFromJson(field.type, json.loads(attribute.value)) + attrJsonData = makeObjectFromJson(type(field.type), json.loads(attribute.value)) setattr(self, field.name, attrJsonData) + self.visualQuality = TriangleMeshQualityOptions.LowQualityTriangleMesh return self @logFailure diff --git a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py index aa77cac500..6a78ad7cdf 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Components.py @@ -1,20 +1,18 @@ # Contains all of the logic for mapping the Components / Occurrences -import logging -import traceback -import uuid -from typing import * - import adsk.core import adsk.fusion -from proto.proto_out import assembly_pb2, joint_pb2, material_pb2, types_pb2 - -from ...Logging import logFailure, timed -from ...Types import ExportMode -from ..ExporterOptions import ExporterOptions -from . import PhysicalProperties -from .PDMessage import PDMessage -from .Utilities import * +from src.Logging import logFailure +from src.Parser.ExporterOptions import ExporterOptions +from src.Parser.SynthesisParser import PhysicalProperties +from src.Parser.SynthesisParser.PDMessage import PDMessage +from src.Parser.SynthesisParser.Utilities import ( + fill_info, + guid_component, + guid_occurrence, +) +from src.Proto import assembly_pb2, joint_pb2, material_pb2, types_pb2 +from src.Types import ExportMode # TODO: Impelement Material overrides @@ -47,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: @@ -79,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) @@ -110,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: @@ -174,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 @@ -187,10 +185,15 @@ def _ParseBRep( body: adsk.fusion.BRepBody, options: ExporterOptions, trimesh: assembly_pb2.TriangleMesh, -) -> any: +) -> None: meshManager = body.meshManager calc = meshManager.createMeshCalculator() - calc.setQuality(options.visualQuality) + # Disabling for now. We need the user to be able to adjust this, otherwise it gets locked + # into whatever the default was at the time it first creates the export options. + # calc.setQuality(options.visualQuality) + calc.setQuality(adsk.fusion.TriangleMeshQualityOptions.LowQualityTriangleMesh) + # calc.maxNormalDeviation = 3.14159 * (1.0 / 6.0) + # calc.surfaceTolerance = 0.5 mesh = calc.calculate() fill_info(trimesh, body) @@ -209,7 +212,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 53dd3d10fa..0b5be182c2 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/JointHierarchy.py @@ -1,18 +1,15 @@ import enum -import logging -import traceback -from typing import * +from typing import Any, Iterator, cast import adsk.core import adsk.fusion -from proto.proto_out import joint_pb2, types_pb2 - -from ...general_imports import * -from ...Logging import getLogger, logFailure -from ..ExporterOptions import ExporterOptions -from .PDMessage import PDMessage -from .Utilities import guid_component, guid_occurrence +from src import gm +from src.Logging import getLogger, logFailure +from src.Parser.ExporterOptions import ExporterOptions +from src.Parser.SynthesisParser.PDMessage import PDMessage +from src.Parser.SynthesisParser.Utilities import guid_component, guid_occurrence +from src.Proto import joint_pb2, types_pb2 logger = getLogger() @@ -21,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: @@ -40,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: @@ -62,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 + + print(f"Edge Containing {name} -> {self.node}") - def __iter__(self): + def __iter__(self) -> Iterator["GraphEdge"]: """Iterator for Edges within this edge Yields: @@ -74,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: @@ -112,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: @@ -126,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 _______________________ @@ -146,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 @@ -159,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 @@ -214,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 @@ -246,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: @@ -289,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() ] @@ -312,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 @@ -325,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: @@ -342,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) @@ -366,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: @@ -394,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) @@ -406,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: @@ -445,7 +454,7 @@ def BuildJointPartHierarchy( joints: joint_pb2.Joints, options: ExporterOptions, progressDialog: PDMessage, -): +) -> None: try: progressDialog.currentMessage = f"Constructing Simulation Hierarchy" progressDialog.update() @@ -469,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") @@ -497,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") @@ -534,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 c79a0240d0..0577da7a41 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Joints.py @@ -24,22 +24,29 @@ import traceback import uuid -from typing import Union +from typing import Any, Callable, Union import adsk.core import adsk.fusion -from proto.proto_out import assembly_pb2, joint_pb2, motor_pb2, signal_pb2, types_pb2 - -from ...general_imports import * -from ...Logging import getLogger -from ...Types import JointParentType, SignalType -from ..ExporterOptions import ExporterOptions -from .PDMessage import PDMessage -from .Utilities import construct_info, fill_info, guid_occurrence +from src.Logging import getLogger +from src.Parser.ExporterOptions import ExporterOptions +from src.Parser.SynthesisParser.PDMessage import PDMessage +from src.Parser.SynthesisParser.Utilities import ( + construct_info, + fill_info, + guid_occurrence, +) +from src.Proto import assembly_pb2, joint_pb2, signal_pb2, types_pb2 +from src.Types import Joint, JointParentType, SignalType, Wheel logger = getLogger() +AcceptedJointTypes = [ + adsk.fusion.JointTypes.RevoluteJointType, + adsk.fusion.JointTypes.SliderJointType, + adsk.fusion.JointTypes.BallJointType, +] # Need to take in a graphcontainer # Need to create a new base node for each Joint Instance @@ -69,7 +76,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 @@ -97,60 +104,57 @@ def populateJoints( _addRigidGroup(joint, assembly) continue - # for now if it's not a revolute or slider joint ignore it - if joint.jointMotion.jointType != 1 and joint.jointMotion.jointType != 2: - continue - - try: - # Fusion has no instances of joints but lets roll with it anyway + if joint.jointMotion.jointType in AcceptedJointTypes: + try: + # Fusion has no instances of joints but lets roll with it anyway - # progressDialog.message = f"Exporting Joint configuration {joint.name}" - progressDialog.addJoint(joint.name) + # progressDialog.message = f"Exporting Joint configuration {joint.name}" + progressDialog.addJoint(joint.name) - # create the definition - joint_definition = joints.joint_definitions[joint.entityToken] - _addJoint(joint, joint_definition) + # create the definition + joint_definition = joints.joint_definitions[joint.entityToken] + _addJoint(joint, joint_definition) - # create the instance of the single definition - joint_instance = joints.joint_instances[joint.entityToken] + # create the instance of the single definition + joint_instance = joints.joint_instances[joint.entityToken] - for parse_joints in options.joints: - if parse_joints.jointToken == joint.entityToken: - guid = str(uuid.uuid4()) - signal = signals.signal_map[guid] - construct_info(joint.name, signal, GUID=guid) - signal.io = signal_pb2.IOType.OUTPUT + for parse_joints in options.joints: + if parse_joints.jointToken == joint.entityToken: + guid = str(uuid.uuid4()) + signal = signals.signal_map[guid] + construct_info(joint.name, signal, GUID=guid) + signal.io = signal_pb2.IOType.OUTPUT - # really could just map the enum to a friggin string - if parse_joints.signalType != SignalType.PASSIVE and assembly.dynamic: - if parse_joints.signalType == SignalType.CAN: - signal.device_type = signal_pb2.DeviceType.CANBUS - elif parse_joints.signalType == SignalType.PWM: - signal.device_type = signal_pb2.DeviceType.PWM + # really could just map the enum to a friggin string + if parse_joints.signalType != SignalType.PASSIVE and assembly.dynamic: + if parse_joints.signalType == SignalType.CAN: + signal.device_type = signal_pb2.DeviceType.CANBUS + elif parse_joints.signalType == SignalType.PWM: + signal.device_type = signal_pb2.DeviceType.PWM - motor = joints.motor_definitions[joint.entityToken] - fill_info(motor, joint) - simple_motor = motor.simple_motor - simple_motor.stall_torque = parse_joints.force - simple_motor.max_velocity = parse_joints.speed - simple_motor.braking_constant = 0.8 # Default for now - joint_definition.motor_reference = joint.entityToken + motor = joints.motor_definitions[joint.entityToken] + fill_info(motor, joint) + simple_motor = motor.simple_motor + simple_motor.stall_torque = parse_joints.force + simple_motor.max_velocity = parse_joints.speed + simple_motor.braking_constant = 0.8 # Default for now + joint_definition.motor_reference = joint.entityToken - joint_instance.signal_reference = signal.info.GUID - # else: - # signals.signal_map.remove(guid) + joint_instance.signal_reference = signal.info.GUID + # else: + # signals.signal_map.remove(guid) - _addJointInstance(joint, joint_instance, joint_definition, signals, options) + _addJointInstance(joint, joint_instance, joint_definition, signals, options) - # adds information for joint motion and limits - _motionFromJoint(joint.jointMotion, joint_definition) + # adds information for joint motion and limits + _motionFromJoint(joint.jointMotion, joint_definition) - except: - logger.error("Failed:\n{}".format(traceback.format_exc())) - continue + except: + logger.error("Failed:\n{}".format(traceback.format_exc())) + 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) @@ -175,7 +179,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 @@ -230,7 +234,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 ): @@ -247,22 +251,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, + 4: notImplementedPlaceholder, # TODO: Implement + 5: notImplementedPlaceholder, # TODO: Implement + 6: fillBallJointMotion, } - 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: @@ -336,9 +340,84 @@ 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 fillBallJointMotion(ballMotion: adsk.fusion.BallJointMotion, proto_joint: joint_pb2.Joint) -> None: + """#### Fill Protobuf ball joint motion data + + Args: + ballMotion (adsk.fusion.BallJointMotion): Fusion Ball Joint Data + protoJoint (joint_pb2.Joint): Protobuf joint that is being modified + """ + + # proto_joint.joint_motion_type = joint_pb2.JointMotion.REVOLUTE + proto_joint.joint_motion_type = joint_pb2.JointMotion.BALL + customDofs = proto_joint.custom + + pitchDof = joint_pb2.DOF() + pitchDof.name = "pitch" + pitchDof.axis.x = ballMotion.pitchDirectionVector.x + pitchDof.axis.y = ballMotion.pitchDirectionVector.y + pitchDof.axis.z = ballMotion.pitchDirectionVector.z + if ballMotion.pitchLimits.isMaximumValueEnabled or ballMotion.pitchLimits.isMinimumValueEnabled: + pitchDof.limits.lower = ballMotion.pitchLimits.minimumValue + pitchDof.limits.upper = ballMotion.pitchLimits.maximumValue + pitchDof.value = ballMotion.pitchValue + customDofs.dofs.append(pitchDof) + + yawDof = joint_pb2.DOF() + yawDof.name = "yaw" + yawDof.axis.x = ballMotion.yawDirectionVector.x + yawDof.axis.y = ballMotion.yawDirectionVector.y + yawDof.axis.z = ballMotion.yawDirectionVector.z + if ballMotion.yawLimits.isMaximumValueEnabled or ballMotion.yawLimits.isMinimumValueEnabled: + yawDof.limits.lower = ballMotion.yawLimits.minimumValue + yawDof.limits.upper = ballMotion.yawLimits.maximumValue + yawDof.value = ballMotion.yawValue + customDofs.dofs.append(yawDof) + + rollDof = joint_pb2.DOF() + rollDof.name = "roll" + rollDof.axis.x = ballMotion.rollDirectionVector.x + rollDof.axis.y = ballMotion.rollDirectionVector.y + rollDof.axis.z = ballMotion.rollDirectionVector.z + if ballMotion.rollLimits.isMaximumValueEnabled or ballMotion.rollLimits.isMinimumValueEnabled: + rollDof.limits.lower = ballMotion.rollLimits.minimumValue + rollDof.limits.upper = ballMotion.rollLimits.maximumValue + rollDof.value = ballMotion.rollValue + customDofs.dofs.append(rollDof) + + # ballMotion. + + # dof = proto_joint.rotational.rotational_freedom + + # # name + # # axis + # # pivot + # # dynamics + # # limits + # # current value + + # dof.name = "Rotational Joint" + + # dof.value = revoluteMotion.rotationValue + + # if revoluteMotion.rotationLimits: + # dof.limits.lower = revoluteMotion.rotationLimits.minimumValue + # dof.limits.upper = revoluteMotion.rotationLimits.maximumValue + + # rotationAxisVector = revoluteMotion.rotationAxisVector + # if rotationAxisVector: + # + # else: + # rotationAxis = revoluteMotion.rotationAxis + # # don't handle 4 for now + # # There is a bug here https://jira.autodesk.com/browse/FUS-80533 + # # I have 0 memory of why this is necessary + # dof.axis.x = int(rotationAxis == 0) + # dof.axis.y = int(rotationAxis == 2) + # dof.axis.z = int(rotationAxis == 1) + + +def notImplementedPlaceholder(*argv: Any) -> None: ... def _searchForGrounded( @@ -429,9 +508,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" @@ -440,44 +519,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 97d8d47f57..08c5e17bb3 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Materials.py @@ -1,18 +1,10 @@ -# Should contain Physical and Apperance materials ? -import json -import logging -import math -import traceback +import adsk.core -import adsk - -from proto.proto_out import material_pb2 - -from ...general_imports import * -from ...Logging import logFailure, timed -from ..ExporterOptions import ExporterOptions -from .PDMessage import PDMessage -from .Utilities import * +from src.Logging import logFailure +from src.Parser.ExporterOptions import ExporterOptions +from src.Parser.SynthesisParser.PDMessage import PDMessage +from src.Parser.SynthesisParser.Utilities import construct_info, fill_info +from src.Proto import material_pb2 OPACITY_RAMPING_CONSTANT = 14.0 @@ -35,7 +27,7 @@ def _MapAllPhysicalMaterials( - physicalMaterials: list, + physicalMaterials: list[material_pb2.PhysicalMaterial], materials: material_pb2.Materials, options: ExporterOptions, progressDialog: PDMessage, @@ -52,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: @@ -77,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 @@ -146,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, @@ -213,6 +207,10 @@ def getMaterialAppearance( properties = fusionAppearance.appearanceProperties + roughnessProp = properties.itemById("surface_roughness") + if roughnessProp: + appearance.roughness = roughnessProp.value + # Thank Liam for this. modelItem = properties.itemById("interior_model") if modelItem: @@ -220,11 +218,15 @@ def getMaterialAppearance( baseColor = None if matModelType == 0: + reflectanceProp = properties.itemById("opaque_f0") + if reflectanceProp: + appearance.metallic = reflectanceProp.value baseColor = properties.itemById("opaque_albedo").value if baseColor: baseColor.opacity = 255 elif matModelType == 1: baseColor = properties.itemById("metal_f0").value + appearance.metallic = 0.8 if baseColor: baseColor.opacity = 255 elif matModelType == 2: 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 b59f391f0b..b085b48d32 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Parser.py @@ -5,16 +5,21 @@ import adsk.fusion from google.protobuf.json_format import MessageToJson -from proto.proto_out import assembly_pb2, types_pb2 - -from ...APS.APS import getAuth, upload_mirabuf -from ...general_imports import * -from ...Logging import getLogger, logFailure, timed -from ...Types import ExportLocation, ExportMode -from ...UI.Camera import captureThumbnail, clearIconCache -from ..ExporterOptions import ExporterOptions -from . import Components, JointHierarchy, Joints, Materials, PDMessage -from .Utilities import * +from src import gm +from src.APS.APS import getAuth, upload_mirabuf +from src.Logging import getLogger, logFailure, timed +from src.Parser.ExporterOptions import ExporterOptions +from src.Parser.SynthesisParser import ( + Components, + JointHierarchy, + Joints, + Materials, + PDMessage, +) +from src.Parser.SynthesisParser.Utilities import fill_info +from src.Proto import assembly_pb2, types_pb2 +from src.Types import ExportLocation, ExportMode +from src.UI.Camera import captureThumbnail, clearIconCache logger = getLogger() @@ -34,7 +39,7 @@ def export(self) -> None: app = adsk.core.Application.get() design: adsk.fusion.Design = app.activeDocument.design - if not getAuth(): + if self.exporterOptions.exportLocation == ExportLocation.UPLOAD and not getAuth(): app.userInterface.messageBox("APS Login Required for Uploading.", "APS Login") return @@ -174,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" @@ -184,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 db488c115a..53af57dfe5 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/PhysicalProperties.py @@ -16,24 +16,20 @@ """ -import logging -import traceback from typing import Union import adsk -from proto.proto_out import types_pb2 - -from ...general_imports import INTERNAL_ID -from ...Logging import logFailure +from src.Logging import logFailure +from src.Proto import types_pb2 @logFailure 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 362a2a6e72..a5e389beda 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/RigidGroup.py @@ -12,20 +12,22 @@ - Success """ -from typing import * +from typing import Union import adsk.core import adsk.fusion -from proto.proto_out import assembly_pb2 - -from ...Logging import logFailure +from src.Logging import logFailure +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 f508f32117..d8f38d921f 100644 --- a/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py +++ b/exporter/SynthesisFusionAddin/src/Parser/SynthesisParser/Utilities.py @@ -1,17 +1,17 @@ import math import uuid -from adsk.core import Base, Vector3D -from adsk.fusion import Component, Occurrence +import adsk.core +import adsk.fusion -# from proto.proto_out import types_pb2 +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)}" @@ -19,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: @@ -43,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 @@ -77,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: @@ -89,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)) @@ -129,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: @@ -138,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 @@ -196,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/__init__.py b/exporter/SynthesisFusionAddin/src/Proto/__init__.py new file mode 100644 index 0000000000..087dab646e --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Proto/__init__.py @@ -0,0 +1,4 @@ +import os +import sys + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) diff --git a/exporter/SynthesisFusionAddin/src/Proto/assembly_pb2.py b/exporter/SynthesisFusionAddin/src/Proto/assembly_pb2.py new file mode 100644 index 0000000000..20803d3601 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Proto/assembly_pb2.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: assembly.proto +# Protobuf Python Version: 5.27.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 1, + '', + 'assembly.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import joint_pb2 as joint__pb2 +import material_pb2 as material__pb2 +import signal_pb2 as signal__pb2 +import types_pb2 as types__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x61ssembly.proto\x12\x07mirabuf\x1a\x0btypes.proto\x1a\x0bjoint.proto\x1a\x0ematerial.proto\x1a\x0csignal.proto\"\xc4\x02\n\x08\x41ssembly\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12#\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x15.mirabuf.AssemblyData\x12\x0f\n\x07\x64ynamic\x18\x03 \x01(\x08\x12\x32\n\rphysical_data\x18\x04 \x01(\x0b\x32\x1b.mirabuf.PhysicalProperties\x12\x31\n\x10\x64\x65sign_hierarchy\x18\x05 \x01(\x0b\x32\x17.mirabuf.GraphContainer\x12\x30\n\x0fjoint_hierarchy\x18\x06 \x01(\x0b\x32\x17.mirabuf.GraphContainer\x12%\n\ttransform\x18\x07 \x01(\x0b\x32\x12.mirabuf.Transform\x12%\n\tthumbnail\x18\x08 \x01(\x0b\x32\x12.mirabuf.Thumbnail\"\xae\x01\n\x0c\x41ssemblyData\x12\x1d\n\x05parts\x18\x01 \x01(\x0b\x32\x0e.mirabuf.Parts\x12%\n\x06joints\x18\x02 \x01(\x0b\x32\x15.mirabuf.joint.Joints\x12.\n\tmaterials\x18\x03 \x01(\x0b\x32\x1b.mirabuf.material.Materials\x12(\n\x07signals\x18\x04 \x01(\x0b\x32\x17.mirabuf.signal.Signals\"\xe2\x02\n\x05Parts\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12=\n\x10part_definitions\x18\x02 \x03(\x0b\x32#.mirabuf.Parts.PartDefinitionsEntry\x12\x39\n\x0epart_instances\x18\x03 \x03(\x0b\x32!.mirabuf.Parts.PartInstancesEntry\x12$\n\tuser_data\x18\x04 \x01(\x0b\x32\x11.mirabuf.UserData\x1aO\n\x14PartDefinitionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12&\n\x05value\x18\x02 \x01(\x0b\x32\x17.mirabuf.PartDefinition:\x02\x38\x01\x1aK\n\x12PartInstancesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.mirabuf.PartInstance:\x02\x38\x01\"\xef\x01\n\x0ePartDefinition\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12\x32\n\rphysical_data\x18\x02 \x01(\x0b\x32\x1b.mirabuf.PhysicalProperties\x12*\n\x0e\x62\x61se_transform\x18\x03 \x01(\x0b\x32\x12.mirabuf.Transform\x12\x1d\n\x06\x62odies\x18\x04 \x03(\x0b\x32\r.mirabuf.Body\x12\x0f\n\x07\x64ynamic\x18\x05 \x01(\x08\x12\x19\n\x11\x66riction_override\x18\x06 \x01(\x02\x12\x15\n\rmass_override\x18\x07 \x01(\x02\"\xf9\x01\n\x0cPartInstance\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12!\n\x19part_definition_reference\x18\x02 \x01(\t\x12%\n\ttransform\x18\x03 \x01(\x0b\x32\x12.mirabuf.Transform\x12,\n\x10global_transform\x18\x04 \x01(\x0b\x32\x12.mirabuf.Transform\x12\x0e\n\x06joints\x18\x05 \x03(\t\x12\x12\n\nappearance\x18\x06 \x01(\t\x12\x19\n\x11physical_material\x18\x07 \x01(\t\x12\x15\n\rskip_collider\x18\x08 \x01(\x08\"|\n\x04\x42ody\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12\x0c\n\x04part\x18\x02 \x01(\t\x12,\n\rtriangle_mesh\x18\x03 \x01(\x0b\x32\x15.mirabuf.TriangleMesh\x12\x1b\n\x13\x61ppearance_override\x18\x04 \x01(\t\"\xad\x01\n\x0cTriangleMesh\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12\x12\n\nhas_volume\x18\x02 \x01(\x08\x12\x1a\n\x12material_reference\x18\x03 \x01(\t\x12\x1d\n\x04mesh\x18\x04 \x01(\x0b\x32\r.mirabuf.MeshH\x00\x12$\n\x05\x62mesh\x18\x05 \x01(\x0b\x32\x13.mirabuf.BinaryMeshH\x00\x42\x0b\n\tmesh_type\"C\n\x04Mesh\x12\r\n\x05verts\x18\x01 \x03(\x02\x12\x0f\n\x07normals\x18\x02 \x03(\x02\x12\n\n\x02uv\x18\x03 \x03(\x02\x12\x0f\n\x07indices\x18\x04 \x03(\x05\"\x1a\n\nBinaryMesh\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x42\x02H\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'assembly_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'H\001' + _globals['_PARTS_PARTDEFINITIONSENTRY']._loaded_options = None + _globals['_PARTS_PARTDEFINITIONSENTRY']._serialized_options = b'8\001' + _globals['_PARTS_PARTINSTANCESENTRY']._loaded_options = None + _globals['_PARTS_PARTINSTANCESENTRY']._serialized_options = b'8\001' + _globals['_ASSEMBLY']._serialized_start=84 + _globals['_ASSEMBLY']._serialized_end=408 + _globals['_ASSEMBLYDATA']._serialized_start=411 + _globals['_ASSEMBLYDATA']._serialized_end=585 + _globals['_PARTS']._serialized_start=588 + _globals['_PARTS']._serialized_end=942 + _globals['_PARTS_PARTDEFINITIONSENTRY']._serialized_start=786 + _globals['_PARTS_PARTDEFINITIONSENTRY']._serialized_end=865 + _globals['_PARTS_PARTINSTANCESENTRY']._serialized_start=867 + _globals['_PARTS_PARTINSTANCESENTRY']._serialized_end=942 + _globals['_PARTDEFINITION']._serialized_start=945 + _globals['_PARTDEFINITION']._serialized_end=1184 + _globals['_PARTINSTANCE']._serialized_start=1187 + _globals['_PARTINSTANCE']._serialized_end=1436 + _globals['_BODY']._serialized_start=1438 + _globals['_BODY']._serialized_end=1562 + _globals['_TRIANGLEMESH']._serialized_start=1565 + _globals['_TRIANGLEMESH']._serialized_end=1738 + _globals['_MESH']._serialized_start=1740 + _globals['_MESH']._serialized_end=1807 + _globals['_BINARYMESH']._serialized_start=1809 + _globals['_BINARYMESH']._serialized_end=1835 +# @@protoc_insertion_point(module_scope) 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.py b/exporter/SynthesisFusionAddin/src/Proto/joint_pb2.py new file mode 100644 index 0000000000..6a14bfadaa --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Proto/joint_pb2.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: joint.proto +# Protobuf Python Version: 5.27.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 1, + '', + 'joint.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import motor_pb2 as motor__pb2 +import types_pb2 as types__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bjoint.proto\x12\rmirabuf.joint\x1a\x0btypes.proto\x1a\x0bmotor.proto\"\x9d\x04\n\x06Joints\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12\x46\n\x11joint_definitions\x18\x02 \x03(\x0b\x32+.mirabuf.joint.Joints.JointDefinitionsEntry\x12\x42\n\x0fjoint_instances\x18\x03 \x03(\x0b\x32).mirabuf.joint.Joints.JointInstancesEntry\x12/\n\x0crigid_groups\x18\x04 \x03(\x0b\x32\x19.mirabuf.joint.RigidGroup\x12\x46\n\x11motor_definitions\x18\x05 \x03(\x0b\x32+.mirabuf.joint.Joints.MotorDefinitionsEntry\x1aM\n\x15JointDefinitionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.mirabuf.joint.Joint:\x02\x38\x01\x1aS\n\x13JointInstancesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.mirabuf.joint.JointInstance:\x02\x38\x01\x1aM\n\x15MotorDefinitionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.mirabuf.motor.Motor:\x02\x38\x01\"\x99\x02\n\rJointInstance\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12\x15\n\risEndEffector\x18\x02 \x01(\x08\x12\x13\n\x0bparent_part\x18\x03 \x01(\t\x12\x12\n\nchild_part\x18\x04 \x01(\t\x12\x17\n\x0fjoint_reference\x18\x05 \x01(\t\x12 \n\x06offset\x18\x06 \x01(\x0b\x32\x10.mirabuf.Vector3\x12&\n\x05parts\x18\x07 \x01(\x0b\x32\x17.mirabuf.GraphContainer\x12\x18\n\x10signal_reference\x18\x08 \x01(\t\x12.\n\x0bmotion_link\x18\t \x03(\x0b\x32\x19.mirabuf.joint.MotionLink\"E\n\nMotionLink\x12\x16\n\x0ejoint_instance\x18\x01 \x01(\t\x12\r\n\x05ratio\x18\x02 \x01(\x02\x12\x10\n\x08reversed\x18\x03 \x01(\x08\"\xfc\x02\n\x05Joint\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12 \n\x06origin\x18\x02 \x01(\x0b\x32\x10.mirabuf.Vector3\x12\x35\n\x11joint_motion_type\x18\x03 \x01(\x0e\x32\x1a.mirabuf.joint.JointMotion\x12\x17\n\x0f\x62reak_magnitude\x18\x04 \x01(\x02\x12\x34\n\nrotational\x18\x05 \x01(\x0b\x32\x1e.mirabuf.joint.RotationalJointH\x00\x12\x32\n\tprismatic\x18\x06 \x01(\x0b\x32\x1d.mirabuf.joint.PrismaticJointH\x00\x12,\n\x06\x63ustom\x18\x07 \x01(\x0b\x32\x1a.mirabuf.joint.CustomJointH\x00\x12$\n\tuser_data\x18\x08 \x01(\x0b\x32\x11.mirabuf.UserData\x12\x17\n\x0fmotor_reference\x18\t \x01(\tB\r\n\x0bJointMotion\"-\n\x08\x44ynamics\x12\x0f\n\x07\x64\x61mping\x18\x01 \x01(\x02\x12\x10\n\x08\x66riction\x18\x02 \x01(\x02\"H\n\x06Limits\x12\r\n\x05lower\x18\x01 \x01(\x02\x12\r\n\x05upper\x18\x02 \x01(\x02\x12\x10\n\x08velocity\x18\x03 \x01(\x02\x12\x0e\n\x06\x65\x66\x66ort\x18\x04 \x01(\x02\"Z\n\x06Safety\x12\x13\n\x0blower_limit\x18\x01 \x01(\x02\x12\x13\n\x0bupper_limit\x18\x02 \x01(\x02\x12\x12\n\nk_position\x18\x03 \x01(\x02\x12\x12\n\nk_velocity\x18\x04 \x01(\x02\"\xbb\x01\n\x03\x44OF\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x1e\n\x04\x61xis\x18\x02 \x01(\x0b\x32\x10.mirabuf.Vector3\x12%\n\x0epivotDirection\x18\x03 \x01(\x0e\x32\r.mirabuf.Axis\x12)\n\x08\x64ynamics\x18\x04 \x01(\x0b\x32\x17.mirabuf.joint.Dynamics\x12%\n\x06limits\x18\x05 \x01(\x0b\x32\x15.mirabuf.joint.Limits\x12\r\n\x05value\x18\x06 \x01(\x02\"/\n\x0b\x43ustomJoint\x12 \n\x04\x64ofs\x18\x01 \x03(\x0b\x32\x12.mirabuf.joint.DOF\"A\n\x0fRotationalJoint\x12.\n\x12rotational_freedom\x18\x01 \x01(\x0b\x32\x12.mirabuf.joint.DOF\"u\n\tBallJoint\x12\x1f\n\x03yaw\x18\x01 \x01(\x0b\x32\x12.mirabuf.joint.DOF\x12!\n\x05pitch\x18\x02 \x01(\x0b\x32\x12.mirabuf.joint.DOF\x12$\n\x08rotation\x18\x03 \x01(\x0b\x32\x12.mirabuf.joint.DOF\"?\n\x0ePrismaticJoint\x12-\n\x11prismatic_freedom\x18\x01 \x01(\x0b\x32\x12.mirabuf.joint.DOF\"/\n\nRigidGroup\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0boccurrences\x18\x02 \x03(\t*r\n\x0bJointMotion\x12\t\n\x05RIGID\x10\x00\x12\x0c\n\x08REVOLUTE\x10\x01\x12\n\n\x06SLIDER\x10\x02\x12\x0f\n\x0b\x43YLINDRICAL\x10\x03\x12\x0b\n\x07PINSLOT\x10\x04\x12\n\n\x06PLANAR\x10\x05\x12\x08\n\x04\x42\x41LL\x10\x06\x12\n\n\x06\x43USTOM\x10\x07\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'joint_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_JOINTS_JOINTDEFINITIONSENTRY']._loaded_options = None + _globals['_JOINTS_JOINTDEFINITIONSENTRY']._serialized_options = b'8\001' + _globals['_JOINTS_JOINTINSTANCESENTRY']._loaded_options = None + _globals['_JOINTS_JOINTINSTANCESENTRY']._serialized_options = b'8\001' + _globals['_JOINTS_MOTORDEFINITIONSENTRY']._loaded_options = None + _globals['_JOINTS_MOTORDEFINITIONSENTRY']._serialized_options = b'8\001' + _globals['_JOINTMOTION']._serialized_start=2090 + _globals['_JOINTMOTION']._serialized_end=2204 + _globals['_JOINTS']._serialized_start=57 + _globals['_JOINTS']._serialized_end=598 + _globals['_JOINTS_JOINTDEFINITIONSENTRY']._serialized_start=357 + _globals['_JOINTS_JOINTDEFINITIONSENTRY']._serialized_end=434 + _globals['_JOINTS_JOINTINSTANCESENTRY']._serialized_start=436 + _globals['_JOINTS_JOINTINSTANCESENTRY']._serialized_end=519 + _globals['_JOINTS_MOTORDEFINITIONSENTRY']._serialized_start=521 + _globals['_JOINTS_MOTORDEFINITIONSENTRY']._serialized_end=598 + _globals['_JOINTINSTANCE']._serialized_start=601 + _globals['_JOINTINSTANCE']._serialized_end=882 + _globals['_MOTIONLINK']._serialized_start=884 + _globals['_MOTIONLINK']._serialized_end=953 + _globals['_JOINT']._serialized_start=956 + _globals['_JOINT']._serialized_end=1336 + _globals['_DYNAMICS']._serialized_start=1338 + _globals['_DYNAMICS']._serialized_end=1383 + _globals['_LIMITS']._serialized_start=1385 + _globals['_LIMITS']._serialized_end=1457 + _globals['_SAFETY']._serialized_start=1459 + _globals['_SAFETY']._serialized_end=1549 + _globals['_DOF']._serialized_start=1552 + _globals['_DOF']._serialized_end=1739 + _globals['_CUSTOMJOINT']._serialized_start=1741 + _globals['_CUSTOMJOINT']._serialized_end=1788 + _globals['_ROTATIONALJOINT']._serialized_start=1790 + _globals['_ROTATIONALJOINT']._serialized_end=1855 + _globals['_BALLJOINT']._serialized_start=1857 + _globals['_BALLJOINT']._serialized_end=1974 + _globals['_PRISMATICJOINT']._serialized_start=1976 + _globals['_PRISMATICJOINT']._serialized_end=2039 + _globals['_RIGIDGROUP']._serialized_start=2041 + _globals['_RIGIDGROUP']._serialized_end=2088 +# @@protoc_insertion_point(module_scope) 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.py b/exporter/SynthesisFusionAddin/src/Proto/material_pb2.py new file mode 100644 index 0000000000..3c54e0c3dc --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Proto/material_pb2.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: material.proto +# Protobuf Python Version: 5.27.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 1, + '', + 'material.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import types_pb2 as types__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ematerial.proto\x12\x10mirabuf.material\x1a\x0btypes.proto\"\xea\x02\n\tMaterials\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12M\n\x11physicalMaterials\x18\x02 \x03(\x0b\x32\x32.mirabuf.material.Materials.PhysicalMaterialsEntry\x12\x41\n\x0b\x61ppearances\x18\x03 \x03(\x0b\x32,.mirabuf.material.Materials.AppearancesEntry\x1a\\\n\x16PhysicalMaterialsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x31\n\x05value\x18\x02 \x01(\x0b\x32\".mirabuf.material.PhysicalMaterial:\x02\x38\x01\x1aP\n\x10\x41ppearancesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.mirabuf.material.Appearance:\x02\x38\x01\"\x80\x01\n\nAppearance\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12\x1e\n\x06\x61lbedo\x18\x02 \x01(\x0b\x32\x0e.mirabuf.Color\x12\x11\n\troughness\x18\x03 \x01(\x01\x12\x10\n\x08metallic\x18\x04 \x01(\x01\x12\x10\n\x08specular\x18\x05 \x01(\x01\"\x82\x06\n\x10PhysicalMaterial\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12;\n\x07thermal\x18\x03 \x01(\x0b\x32*.mirabuf.material.PhysicalMaterial.Thermal\x12\x41\n\nmechanical\x18\x04 \x01(\x0b\x32-.mirabuf.material.PhysicalMaterial.Mechanical\x12=\n\x08strength\x18\x05 \x01(\x0b\x32+.mirabuf.material.PhysicalMaterial.Strength\x12\x18\n\x10\x64ynamic_friction\x18\x06 \x01(\x02\x12\x17\n\x0fstatic_friction\x18\x07 \x01(\x02\x12\x13\n\x0brestitution\x18\x08 \x01(\x02\x12\x12\n\ndeformable\x18\t \x01(\x08\x12@\n\x07matType\x18\n \x01(\x0e\x32/.mirabuf.material.PhysicalMaterial.MaterialType\x1a\x65\n\x07Thermal\x12\x1c\n\x14thermal_conductivity\x18\x01 \x01(\x02\x12\x15\n\rspecific_heat\x18\x02 \x01(\x02\x12%\n\x1dthermal_expansion_coefficient\x18\x03 \x01(\x02\x1aw\n\nMechanical\x12\x11\n\tyoung_mod\x18\x01 \x01(\x02\x12\x15\n\rpoisson_ratio\x18\x02 \x01(\x02\x12\x11\n\tshear_mod\x18\x03 \x01(\x02\x12\x0f\n\x07\x64\x65nsity\x18\x04 \x01(\x02\x12\x1b\n\x13\x64\x61mping_coefficient\x18\x05 \x01(\x02\x1aW\n\x08Strength\x12\x16\n\x0eyield_strength\x18\x01 \x01(\x02\x12\x18\n\x10tensile_strength\x18\x02 \x01(\x02\x12\x19\n\x11thermal_treatment\x18\x03 \x01(\x08\"&\n\x0cMaterialType\x12\t\n\x05METAL\x10\x00\x12\x0b\n\x07PLASTIC\x10\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'material_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_MATERIALS_PHYSICALMATERIALSENTRY']._loaded_options = None + _globals['_MATERIALS_PHYSICALMATERIALSENTRY']._serialized_options = b'8\001' + _globals['_MATERIALS_APPEARANCESENTRY']._loaded_options = None + _globals['_MATERIALS_APPEARANCESENTRY']._serialized_options = b'8\001' + _globals['_MATERIALS']._serialized_start=50 + _globals['_MATERIALS']._serialized_end=412 + _globals['_MATERIALS_PHYSICALMATERIALSENTRY']._serialized_start=238 + _globals['_MATERIALS_PHYSICALMATERIALSENTRY']._serialized_end=330 + _globals['_MATERIALS_APPEARANCESENTRY']._serialized_start=332 + _globals['_MATERIALS_APPEARANCESENTRY']._serialized_end=412 + _globals['_APPEARANCE']._serialized_start=415 + _globals['_APPEARANCE']._serialized_end=543 + _globals['_PHYSICALMATERIAL']._serialized_start=546 + _globals['_PHYSICALMATERIAL']._serialized_end=1316 + _globals['_PHYSICALMATERIAL_THERMAL']._serialized_start=965 + _globals['_PHYSICALMATERIAL_THERMAL']._serialized_end=1066 + _globals['_PHYSICALMATERIAL_MECHANICAL']._serialized_start=1068 + _globals['_PHYSICALMATERIAL_MECHANICAL']._serialized_end=1187 + _globals['_PHYSICALMATERIAL_STRENGTH']._serialized_start=1189 + _globals['_PHYSICALMATERIAL_STRENGTH']._serialized_end=1276 + _globals['_PHYSICALMATERIAL_MATERIALTYPE']._serialized_start=1278 + _globals['_PHYSICALMATERIAL_MATERIALTYPE']._serialized_end=1316 +# @@protoc_insertion_point(module_scope) 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.py b/exporter/SynthesisFusionAddin/src/Proto/motor_pb2.py new file mode 100644 index 0000000000..7371eb693f --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Proto/motor_pb2.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: motor.proto +# Protobuf Python Version: 5.27.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 1, + '', + 'motor.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import types_pb2 as types__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bmotor.proto\x12\rmirabuf.motor\x1a\x0btypes.proto\"\x98\x01\n\x05Motor\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12*\n\x08\x64\x63_motor\x18\x02 \x01(\x0b\x32\x16.mirabuf.motor.DCMotorH\x00\x12\x32\n\x0csimple_motor\x18\x03 \x01(\x0b\x32\x1a.mirabuf.motor.SimpleMotorH\x00\x42\x0c\n\nmotor_typeJ\x04\x08\x04\x10\x06\"S\n\x0bSimpleMotor\x12\x14\n\x0cstall_torque\x18\x01 \x01(\x02\x12\x14\n\x0cmax_velocity\x18\x02 \x01(\x02\x12\x18\n\x10\x62raking_constant\x18\x03 \x01(\x02\"\x9d\x03\n\x07\x44\x43Motor\x12\x15\n\rreference_url\x18\x02 \x01(\t\x12\x17\n\x0ftorque_constant\x18\x03 \x01(\x02\x12\x14\n\x0c\x65mf_constant\x18\x04 \x01(\x02\x12\x12\n\nresistance\x18\x05 \x01(\x02\x12\x1a\n\x12maximum_effeciency\x18\x06 \x01(\r\x12\x15\n\rmaximum_power\x18\x07 \x01(\r\x12-\n\nduty_cycle\x18\x08 \x01(\x0e\x32\x19.mirabuf.motor.DutyCycles\x12\x31\n\x08\x61\x64vanced\x18\x10 \x01(\x0b\x32\x1f.mirabuf.motor.DCMotor.Advanced\x1a\x96\x01\n\x08\x41\x64vanced\x12\x14\n\x0c\x66ree_current\x18\x01 \x01(\x02\x12\x12\n\nfree_speed\x18\x02 \x01(\r\x12\x15\n\rstall_current\x18\x03 \x01(\x02\x12\x14\n\x0cstall_torque\x18\x04 \x01(\x02\x12\x15\n\rinput_voltage\x18\x05 \x01(\r\x12\x1c\n\x14resistance_variation\x18\x07 \x01(\x02J\x04\x08\x01\x10\x02J\x04\x08\t\x10\x10*h\n\nDutyCycles\x12\x16\n\x12\x43ONTINUOUS_RUNNING\x10\x00\x12\x0e\n\nSHORT_TIME\x10\x01\x12\x19\n\x15INTERMITTENT_PERIODIC\x10\x02\x12\x17\n\x13\x43ONTINUOUS_PERIODIC\x10\x03\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'motor_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_DUTYCYCLES']._serialized_start=699 + _globals['_DUTYCYCLES']._serialized_end=803 + _globals['_MOTOR']._serialized_start=44 + _globals['_MOTOR']._serialized_end=196 + _globals['_SIMPLEMOTOR']._serialized_start=198 + _globals['_SIMPLEMOTOR']._serialized_end=281 + _globals['_DCMOTOR']._serialized_start=284 + _globals['_DCMOTOR']._serialized_end=697 + _globals['_DCMOTOR_ADVANCED']._serialized_start=535 + _globals['_DCMOTOR_ADVANCED']._serialized_end=685 +# @@protoc_insertion_point(module_scope) 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/readme.md b/exporter/SynthesisFusionAddin/src/Proto/readme.md new file mode 100644 index 0000000000..3f4ac2a4c6 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Proto/readme.md @@ -0,0 +1 @@ +These files are autogenerated by protobuf. For more information visit [`/exporter/proto/`](../../proto/) diff --git a/exporter/SynthesisFusionAddin/src/Proto/signal_pb2.py b/exporter/SynthesisFusionAddin/src/Proto/signal_pb2.py new file mode 100644 index 0000000000..c981f82f04 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Proto/signal_pb2.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: signal.proto +# Protobuf Python Version: 5.27.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 1, + '', + 'signal.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import types_pb2 as types__pb2 + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0csignal.proto\x12\x0emirabuf.signal\x1a\x0btypes.proto\"\xac\x01\n\x07Signals\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12:\n\nsignal_map\x18\x02 \x03(\x0b\x32&.mirabuf.signal.Signals.SignalMapEntry\x1aH\n\x0eSignalMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.mirabuf.signal.Signal:\x02\x38\x01\"\xa2\x01\n\x06Signal\x12\x1b\n\x04info\x18\x01 \x01(\x0b\x32\r.mirabuf.Info\x12\"\n\x02io\x18\x02 \x01(\x0e\x32\x16.mirabuf.signal.IOType\x12\x13\n\x0b\x63ustom_type\x18\x03 \x01(\t\x12\x11\n\tsignal_id\x18\x04 \x01(\r\x12/\n\x0b\x64\x65vice_type\x18\x05 \x01(\x0e\x32\x1a.mirabuf.signal.DeviceType*\x1f\n\x06IOType\x12\t\n\x05INPUT\x10\x00\x12\n\n\x06OUTPUT\x10\x01*O\n\nDeviceType\x12\x07\n\x03PWM\x10\x00\x12\x0b\n\x07\x44igital\x10\x01\x12\n\n\x06\x41nalog\x10\x02\x12\x07\n\x03I2C\x10\x03\x12\n\n\x06\x43\x41NBUS\x10\x04\x12\n\n\x06\x43USTOM\x10\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'signal_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_SIGNALS_SIGNALMAPENTRY']._loaded_options = None + _globals['_SIGNALS_SIGNALMAPENTRY']._serialized_options = b'8\001' + _globals['_IOTYPE']._serialized_start=385 + _globals['_IOTYPE']._serialized_end=416 + _globals['_DEVICETYPE']._serialized_start=418 + _globals['_DEVICETYPE']._serialized_end=497 + _globals['_SIGNALS']._serialized_start=46 + _globals['_SIGNALS']._serialized_end=218 + _globals['_SIGNALS_SIGNALMAPENTRY']._serialized_start=146 + _globals['_SIGNALS_SIGNALMAPENTRY']._serialized_end=218 + _globals['_SIGNAL']._serialized_start=221 + _globals['_SIGNAL']._serialized_end=383 +# @@protoc_insertion_point(module_scope) 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.py b/exporter/SynthesisFusionAddin/src/Proto/types_pb2.py new file mode 100644 index 0000000000..658fabbb7d --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Proto/types_pb2.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: types.proto +# Protobuf Python Version: 5.27.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 1, + '', + 'types.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0btypes.proto\x12\x07mirabuf\"\\\n\x04Node\x12\r\n\x05value\x18\x01 \x01(\t\x12\x1f\n\x08\x63hildren\x18\x02 \x03(\x0b\x32\r.mirabuf.Node\x12$\n\tuser_data\x18\x03 \x01(\x0b\x32\x11.mirabuf.UserData\".\n\x0eGraphContainer\x12\x1c\n\x05nodes\x18\x01 \x03(\x0b\x32\r.mirabuf.Node\"b\n\x08UserData\x12)\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x1b.mirabuf.UserData.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"*\n\x07Vector3\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\x12\t\n\x01z\x18\x03 \x01(\x02\"p\n\x12PhysicalProperties\x12\x0f\n\x07\x64\x65nsity\x18\x01 \x01(\x01\x12\x0c\n\x04mass\x18\x02 \x01(\x01\x12\x0e\n\x06volume\x18\x03 \x01(\x01\x12\x0c\n\x04\x61rea\x18\x04 \x01(\x01\x12\x1d\n\x03\x63om\x18\x05 \x01(\x0b\x32\x10.mirabuf.Vector3\"#\n\tTransform\x12\x16\n\x0espatial_matrix\x18\x01 \x03(\x02\"3\n\x05\x43olor\x12\t\n\x01R\x18\x01 \x01(\x05\x12\t\n\x01G\x18\x02 \x01(\x05\x12\t\n\x01\x42\x18\x03 \x01(\x05\x12\t\n\x01\x41\x18\x04 \x01(\x05\"3\n\x04Info\x12\x0c\n\x04GUID\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\r\"`\n\tThumbnail\x12\r\n\x05width\x18\x01 \x01(\x05\x12\x0e\n\x06height\x18\x02 \x01(\x05\x12\x11\n\textension\x18\x03 \x01(\t\x12\x13\n\x0btransparent\x18\x04 \x01(\x08\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c*\x1b\n\x04\x41xis\x12\x05\n\x01X\x10\x00\x12\x05\n\x01Y\x10\x01\x12\x05\n\x01Z\x10\x02\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'types_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_USERDATA_DATAENTRY']._loaded_options = None + _globals['_USERDATA_DATAENTRY']._serialized_options = b'8\001' + _globals['_AXIS']._serialized_start=665 + _globals['_AXIS']._serialized_end=692 + _globals['_NODE']._serialized_start=24 + _globals['_NODE']._serialized_end=116 + _globals['_GRAPHCONTAINER']._serialized_start=118 + _globals['_GRAPHCONTAINER']._serialized_end=164 + _globals['_USERDATA']._serialized_start=166 + _globals['_USERDATA']._serialized_end=264 + _globals['_USERDATA_DATAENTRY']._serialized_start=221 + _globals['_USERDATA_DATAENTRY']._serialized_end=264 + _globals['_VECTOR3']._serialized_start=266 + _globals['_VECTOR3']._serialized_end=308 + _globals['_PHYSICALPROPERTIES']._serialized_start=310 + _globals['_PHYSICALPROPERTIES']._serialized_end=422 + _globals['_TRANSFORM']._serialized_start=424 + _globals['_TRANSFORM']._serialized_end=459 + _globals['_COLOR']._serialized_start=461 + _globals['_COLOR']._serialized_end=512 + _globals['_INFO']._serialized_start=514 + _globals['_INFO']._serialized_end=565 + _globals['_THUMBNAIL']._serialized_start=567 + _globals['_THUMBNAIL']._serialized_end=663 +# @@protoc_insertion_point(module_scope) diff --git a/exporter/SynthesisFusionAddin/src/Proto/types_pb2.pyi b/exporter/SynthesisFusionAddin/src/Proto/types_pb2.pyi new file mode 100644 index 0000000000..4463567347 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Proto/types_pb2.pyi @@ -0,0 +1,315 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +Common data type implementations +Intended to be re-used +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _Axis: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _AxisEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_Axis.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + X: _Axis.ValueType # 0 + Y: _Axis.ValueType # 1 + Z: _Axis.ValueType # 2 + +class Axis(_Axis, metaclass=_AxisEnumTypeWrapper): + """Axis Enum""" + +X: Axis.ValueType # 0 +Y: Axis.ValueType # 1 +Z: Axis.ValueType # 2 +global___Axis = Axis + +@typing.final +class Node(google.protobuf.message.Message): + """Each proper object within the Graph - First one is Root""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUE_FIELD_NUMBER: builtins.int + CHILDREN_FIELD_NUMBER: builtins.int + USER_DATA_FIELD_NUMBER: builtins.int + value: builtins.str + """/ the reference ID for whatever kind of graph this is""" + @property + def children(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Node]: + """/ the children for the given leaf""" + + @property + def user_data(self) -> global___UserData: + """/ other associated data that can be used""" + + def __init__( + self, + *, + value: builtins.str = ..., + children: collections.abc.Iterable[global___Node] | None = ..., + user_data: global___UserData | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["user_data", b"user_data"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["children", b"children", "user_data", b"user_data", "value", b"value"]) -> None: ... + +global___Node = Node + +@typing.final +class GraphContainer(google.protobuf.message.Message): + """Top level GraphContainer + Contains all Graph element roots within + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NODES_FIELD_NUMBER: builtins.int + @property + def nodes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Node]: + """represents the root of each seperate assembly - most of the time 1 node""" + + def __init__( + self, + *, + nodes: collections.abc.Iterable[global___Node] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["nodes", b"nodes"]) -> None: ... + +global___GraphContainer = GraphContainer + +@typing.final +class UserData(google.protobuf.message.Message): + """* + UserData + + Arbitrary data to append to a given message in map form + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class DataEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["key", b"key", "value", b"value"]) -> None: ... + + DATA_FIELD_NUMBER: builtins.int + @property + def data(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """/ e.g. data["wheel"] = "yes" """ + + def __init__( + self, + *, + data: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["data", b"data"]) -> None: ... + +global___UserData = UserData + +@typing.final +class Vector3(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + X_FIELD_NUMBER: builtins.int + Y_FIELD_NUMBER: builtins.int + Z_FIELD_NUMBER: builtins.int + x: builtins.float + y: builtins.float + z: builtins.float + def __init__( + self, + *, + x: builtins.float = ..., + y: builtins.float = ..., + z: builtins.float = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["x", b"x", "y", b"y", "z", b"z"]) -> None: ... + +global___Vector3 = Vector3 + +@typing.final +class PhysicalProperties(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DENSITY_FIELD_NUMBER: builtins.int + MASS_FIELD_NUMBER: builtins.int + VOLUME_FIELD_NUMBER: builtins.int + AREA_FIELD_NUMBER: builtins.int + COM_FIELD_NUMBER: builtins.int + density: builtins.float + """/ kg per cubic cm kg/(cm^3)""" + mass: builtins.float + """/ kg""" + volume: builtins.float + """/ cm^3""" + area: builtins.float + """/ cm^2""" + @property + def com(self) -> global___Vector3: + """/ non-negative? Vec3""" + + def __init__( + self, + *, + density: builtins.float = ..., + mass: builtins.float = ..., + volume: builtins.float = ..., + area: builtins.float = ..., + com: global___Vector3 | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["com", b"com"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["area", b"area", "com", b"com", "density", b"density", "mass", b"mass", "volume", b"volume"]) -> None: ... + +global___PhysicalProperties = PhysicalProperties + +@typing.final +class Transform(google.protobuf.message.Message): + """* + Transform + + Data needed to apply scale, position, and rotational changes + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPATIAL_MATRIX_FIELD_NUMBER: builtins.int + @property + def spatial_matrix(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: + """ + flat map of 4x4 transform matrix + [00][01][02][03][10][11][12][13][20][21][22][23] + """ + + def __init__( + self, + *, + spatial_matrix: collections.abc.Iterable[builtins.float] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["spatial_matrix", b"spatial_matrix"]) -> None: ... + +global___Transform = Transform + +@typing.final +class Color(google.protobuf.message.Message): + """RGBA in expanded form 0-255""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + R_FIELD_NUMBER: builtins.int + G_FIELD_NUMBER: builtins.int + B_FIELD_NUMBER: builtins.int + A_FIELD_NUMBER: builtins.int + R: builtins.int + """red""" + G: builtins.int + """green""" + B: builtins.int + """blue""" + A: builtins.int + """alpha""" + def __init__( + self, + *, + R: builtins.int = ..., + G: builtins.int = ..., + B: builtins.int = ..., + A: builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["A", b"A", "B", b"B", "G", b"G", "R", b"R"]) -> None: ... + +global___Color = Color + +@typing.final +class Info(google.protobuf.message.Message): + """* + Defines basic fields for almost all objects + The location where you can access the GUID for a reference + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + GUID_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + GUID: builtins.str + """GUID unique value - must always be defined + since guid's have exactly 128bits could be represented with bytes[] + however endian becomes an issue + """ + name: builtins.str + """Generic readable name""" + version: builtins.int + """Version of object iteration""" + def __init__( + self, + *, + GUID: builtins.str = ..., + name: builtins.str = ..., + version: builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["GUID", b"GUID", "name", b"name", "version", b"version"]) -> None: ... + +global___Info = Info + +@typing.final +class Thumbnail(google.protobuf.message.Message): + """* + A basic Thumbnail to be encoded in the file + Most of the Time Fusion can encode the file with transparency as PNG not bitmap + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + WIDTH_FIELD_NUMBER: builtins.int + HEIGHT_FIELD_NUMBER: builtins.int + EXTENSION_FIELD_NUMBER: builtins.int + TRANSPARENT_FIELD_NUMBER: builtins.int + DATA_FIELD_NUMBER: builtins.int + width: builtins.int + """/ Image Width""" + height: builtins.int + """/ Image Height""" + extension: builtins.str + """/ Image Extension - ex. (.png, .bitmap, .jpeg)""" + transparent: builtins.bool + """/ Transparency - true from fusion when correctly configured""" + data: builtins.bytes + """/ Data as read from the file in bytes[] form""" + def __init__( + self, + *, + width: builtins.int = ..., + height: builtins.int = ..., + extension: builtins.str = ..., + transparent: builtins.bool = ..., + data: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["data", b"data", "extension", b"extension", "height", b"height", "transparent", b"transparent", "width", b"width"]) -> None: ... + +global___Thumbnail = Thumbnail diff --git a/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/16x16-disabled.png b/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/16x16-disabled.png new file mode 100644 index 0000000000..f4ba1b8f83 Binary files /dev/null and b/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/16x16-disabled.png differ diff --git a/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/16x16-normal.png b/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/16x16-normal.png new file mode 100644 index 0000000000..34948454a9 Binary files /dev/null and b/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/16x16-normal.png differ diff --git a/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/32x32-disabled.png b/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/32x32-disabled.png new file mode 100644 index 0000000000..770208ec0e Binary files /dev/null and b/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/32x32-disabled.png differ diff --git a/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/32x32-normal.png b/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/32x32-normal.png new file mode 100644 index 0000000000..77458fce7d Binary files /dev/null and b/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/32x32-normal.png differ diff --git a/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/64x64-normal.png b/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/64x64-normal.png new file mode 100644 index 0000000000..3076b744ca Binary files /dev/null and b/exporter/SynthesisFusionAddin/src/Resources/SynthesisWebsite/64x64-normal.png differ diff --git a/exporter/SynthesisFusionAddin/src/Resources/kg_icon/16x16-normal.png b/exporter/SynthesisFusionAddin/src/Resources/kg_icon/16x16-normal.png deleted file mode 100644 index 86e8fd3cf8..0000000000 Binary files a/exporter/SynthesisFusionAddin/src/Resources/kg_icon/16x16-normal.png and /dev/null differ diff --git a/exporter/SynthesisFusionAddin/src/Resources/lbs_icon/16x16-normal.png b/exporter/SynthesisFusionAddin/src/Resources/lbs_icon/16x16-normal.png deleted file mode 100644 index ced649bb03..0000000000 Binary files a/exporter/SynthesisFusionAddin/src/Resources/lbs_icon/16x16-normal.png and /dev/null differ diff --git a/exporter/SynthesisFusionAddin/src/Types.py b/exporter/SynthesisFusionAddin/src/Types.py index 1aca3a5162..4219bf1814 100644 --- a/exporter/SynthesisFusionAddin/src/Types.py +++ b/exporter/SynthesisFusionAddin/src/Types.py @@ -1,9 +1,11 @@ import os import pathlib import platform -from dataclasses import dataclass, field, fields, is_dataclass +from dataclasses import MISSING, dataclass, field, fields, is_dataclass from enum import Enum, EnumType -from typing import Union, get_origin +from typing import Any, TypeAlias, get_args, get_origin + +import adsk.fusion # Not 100% sure what this is for - Brandon JointParentType = Enum("JointParentType", ["ROOT", "END"]) @@ -11,24 +13,32 @@ WheelType = Enum("WheelType", ["STANDARD", "OMNI", "MECANUM"]) SignalType = Enum("SignalType", ["PWM", "CAN", "PASSIVE"]) ExportMode = Enum("ExportMode", ["ROBOT", "FIELD"]) # Dynamic / Static export -PreferredUnits = Enum("PreferredUnits", ["METRIC", "IMPERIAL"]) ExportLocation = Enum("ExportLocation", ["UPLOAD", "DOWNLOAD"]) +UnitSystem = Enum("UnitSystem", ["METRIC", "IMPERIAL"]) + +FUSION_UNIT_SYSTEM: dict[int, UnitSystem] = { + adsk.fusion.DistanceUnits.MillimeterDistanceUnits: UnitSystem.METRIC, + adsk.fusion.DistanceUnits.CentimeterDistanceUnits: UnitSystem.METRIC, + adsk.fusion.DistanceUnits.MeterDistanceUnits: UnitSystem.METRIC, + adsk.fusion.DistanceUnits.InchDistanceUnits: UnitSystem.IMPERIAL, + adsk.fusion.DistanceUnits.FootDistanceUnits: UnitSystem.IMPERIAL, +} @dataclass class Wheel: - jointToken: str = field(default=None) - wheelType: WheelType = field(default=None) - signalType: SignalType = field(default=None) + jointToken: str = field(default="") + wheelType: WheelType = field(default=WheelType.STANDARD) + signalType: SignalType = field(default=SignalType.PWM) @dataclass class Joint: - jointToken: str = field(default=None) - parent: JointParentType = field(default=None) - signalType: SignalType = field(default=None) - speed: float = field(default=None) - force: float = field(default=None) + jointToken: str = field(default="") + parent: JointParentType = field(default=JointParentType.ROOT) + signalType: SignalType = field(default=SignalType.PWM) + speed: float = field(default=float("-inf")) + force: float = field(default=float("-inf")) # Transition: AARD-1865 # Should consider changing how the parser handles wheels and joints as there is overlap between @@ -39,9 +49,9 @@ class Joint: @dataclass class Gamepiece: - occurrenceToken: str = field(default=None) - weight: float = field(default=None) - friction: float = field(default=None) + occurrenceToken: str = field(default="") + weight: float = field(default=float("-inf")) + friction: float = field(default=float("-inf")) class PhysicalDepth(Enum): @@ -72,26 +82,12 @@ class ModelHierarchy(Enum): SingleMesh = 3 -class LBS(float): - """Mass Unit in Pounds.""" - - -class KG(float): - """Mass Unit in Kilograms.""" - - -def toLbs(kgs: float) -> LBS: - return LBS(round(kgs * 2.2062, 2)) - - -def toKg(pounds: float) -> KG: - return KG(round(pounds / 2.2062, 2)) - - +KG: TypeAlias = float +LBS: TypeAlias = float PRIMITIVES = (bool, str, int, float, type(None)) -def encodeNestedObjects(obj: any) -> any: +def encodeNestedObjects(obj: Any) -> Any: if isinstance(obj, Enum): return obj.value elif hasattr(obj, "__dict__"): @@ -101,27 +97,27 @@ 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." for field in fields(obj): if field.name in data: - setattr(obj, field.name, makeObjectFromJson(field.type, data[field.name])) + setattr(obj, field.name, makeObjectFromJson(type(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 538968034d..53168fd24d 100644 --- a/exporter/SynthesisFusionAddin/src/UI/Camera.py +++ b/exporter/SynthesisFusionAddin/src/UI/Camera.py @@ -1,15 +1,15 @@ import os -from adsk.core import SaveImageFileOptions +import adsk.core -from ..general_imports import * -from ..Logging import logFailure, timed -from ..Types import OString -from . import Helper +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) @@ -23,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 = SaveImageFileOptions.create(str(path.getPath())) + saveOptions = adsk.core.SaveImageFileOptions.create(path) saveOptions.height = size saveOptions.width = size saveOptions.isAntiAliased = True @@ -38,7 +39,7 @@ def captureThumbnail(size=250): app.activeViewport.saveAsImageFileWithOptions(saveOptions) app.activeViewport.camera = originalCamera - return str(path.getPath()) + return path def clearIconCache() -> None: @@ -46,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 4a501b1319..a2c6968439 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ConfigCommand.py @@ -3,30 +3,31 @@ """ import os -import traceback +import pathlib +import webbrowser from enum import Enum +from typing import Any import adsk.core import adsk.fusion -from ..APS.APS import getAuth, getUserInfo, refreshAuthToken -from ..general_imports import * -from ..Logging import getLogger, logFailure -from ..Parser.ExporterOptions import ExporterOptions -from ..Parser.SynthesisParser.Parser import Parser -from ..Parser.SynthesisParser.Utilities import guid_occurrence -from ..Types import ExportLocation, ExportMode, Gamepiece, PreferredUnits -from . import CustomGraphics, FileDialogConfig, Helper, IconPaths -from .Configuration.SerialCommand import SerialCommand - -# Transition: AARD-1685 -# In the future all components should be handled in this way. -# This import broke everything when attempting to use absolute imports??? Investigate? -from .JointConfigTab import JointConfigTab +from src import APP_WEBSITE_URL, gm +from src.APS.APS import getAuth, getUserInfo +from src.Logging import getLogger, logFailure +from src.Parser.ExporterOptions import ExporterOptions +from src.Parser.SynthesisParser.Parser import Parser +from src.Types import ExportLocation, ExportMode +from src.UI import FileDialogConfig +from src.UI.Configuration.SerialCommand import SerialCommand +from src.UI.GamepieceConfigTab import GamepieceConfigTab +from src.UI.GeneralConfigTab import GeneralConfigTab +from src.UI.JointConfigTab import JointConfigTab # ====================================== CONFIG COMMAND ====================================== +generalConfigTab: GeneralConfigTab jointConfigTab: JointConfigTab +gamepieceConfigTab: GamepieceConfigTab logger = getLogger() @@ -36,17 +37,10 @@ """ INPUTS_ROOT = None -""" -These lists are crucial, and contain all of the relevant object selections. -- GamepieceListGlobal: list of gamepieces (adsk.fusion.Occurrence) -""" -GamepieceListGlobal = [] - -# Default to compressed files -compress = True - -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: @@ -59,16 +53,7 @@ def GUID(arg): object = gm.app.activeDocument.design.findEntityByToken(arg)[0] return object else: # type(obj) - return arg.entityToken - - -def gamepieceTable(): - """### Returns the gamepiece table command input - - Returns: - adsk.fusion.TableCommandInput - """ - return INPUTS_ROOT.itemById("gamepiece_table") + return arg.entityToken # type: ignore[union-attr] class JointMotions(Enum): @@ -87,36 +72,6 @@ class JointMotions(Enum): BALL = 6 -class FullMassCalculation: - def __init__(self): - self.totalMass = 0.0 - self.bRepMassInRoot() - self.traverseOccurrenceHierarchy() - - @logFailure(messageBox=True) - def bRepMassInRoot(self): - for body in gm.app.activeDocument.design.rootComponent.bRepBodies: - if not body.isLightBulbOn: - continue - physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) - self.totalMass += physical.mass - - @logFailure(messageBox=True) - def traverseOccurrenceHierarchy(self): - for occ in gm.app.activeDocument.design.rootComponent.allOccurrences: - if not occ.isLightBulbOn: - continue - - for body in occ.component.bRepBodies: - if not body.isLightBulbOn: - continue - physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) - self.totalMass += physical.mass - - def getTotalMass(self): - return self.totalMass - - class ConfigureCommandCreatedHandler(adsk.core.CommandCreatedEventHandler): """### Start the Command Input Object and define all of the input groups to create our ParserOptions object. @@ -125,15 +80,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__() - self.designAttrs = adsk.core.Application.get().activeProduct.attributes @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 @@ -145,13 +98,6 @@ def notify(self, args): global INPUTS_ROOT # Global CommandInputs arg INPUTS_ROOT = cmd.commandInputs - # ====================================== GENERAL TAB ====================================== - """ - Creates the general tab. - - Parent container for all the command inputs in the tab. - """ - inputs = INPUTS_ROOT.addTabCommandInput("general_settings", "General").children - # ~~~~~~~~~~~~~~~~ HELP FILE ~~~~~~~~~~~~~~~~ """ Sets the small "i" icon in bottom left of the panel. @@ -159,119 +105,44 @@ def notify(self, args): """ cmd.helpFile = os.path.join(".", "src", "Resources", "HTML", "info.html") - # ~~~~~~~~~~~~~~~~ EXPORT MODE ~~~~~~~~~~~~~~~~ - """ - Dropdown to choose whether to export robot or field element - """ - dropdownExportMode = inputs.addDropDownCommandInput( - "mode", - "Export Mode", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - - dynamic = exporterOptions.exportMode == ExportMode.ROBOT - dropdownExportMode.listItems.add("Dynamic", dynamic) - dropdownExportMode.listItems.add("Static", not dynamic) + global generalConfigTab + generalConfigTab = GeneralConfigTab(args, exporterOptions) - dropdownExportMode.tooltip = "Export Mode" - dropdownExportMode.tooltipDescription = "
Does this object move dynamically?" + global gamepieceConfigTab + gamepieceConfigTab = GamepieceConfigTab(args, exporterOptions) + generalConfigTab.gamepieceConfigTab = gamepieceConfigTab - # ~~~~~~~~~~~~~~~~ EXPORT LOCATION ~~~~~~~~~~~~~~~~~~ + if not exporterOptions.exportMode == ExportMode.FIELD: + gamepieceConfigTab.isVisible = False - dropdownExportLocation = inputs.addDropDownCommandInput( - "location", "Export Location", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle - ) - - upload: bool = exporterOptions.exportLocation == ExportLocation.UPLOAD - dropdownExportLocation.listItems.add("Upload", upload) - dropdownExportLocation.listItems.add("Download", not upload) - - dropdownExportLocation.tooltip = "Export Location" - dropdownExportLocation.tooltipDescription = ( - "
Do you want to upload this mirabuf file to APS, or download it to your local machine?" - ) - - # ~~~~~~~~~~~~~~~~ WEIGHT CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Table for weight config. - - Used this to align multiple commandInputs on the same row - """ - weightTableInput = self.createTableInput( - "weight_table", - "Weight Table", - inputs, - 4, - "3:2:2:1", - 1, - ) - weightTableInput.tablePresentationStyle = 2 # set transparent background for table - - weight_name = inputs.addStringValueInput("weight_name", "Weight") - weight_name.value = "Weight" - weight_name.isReadOnly = True - - auto_calc_weight = self.createBooleanInput( - "auto_calc_weight", - "‎", - inputs, - checked=False, - tooltip="Approximate the weight of your robot assembly.", - tooltipadvanced="This may take a moment...", - enabled=True, - isCheckBox=False, - ) - auto_calc_weight.resourceFolder = IconPaths.stringIcons["calculate-enabled"] - auto_calc_weight.isFullWidth = True - - imperialUnits = exporterOptions.preferredUnits == PreferredUnits.IMPERIAL - if imperialUnits: - # ExporterOptions always contains the metric value - displayWeight = exporterOptions.robotWeight * 2.2046226218 - else: - displayWeight = exporterOptions.robotWeight - - weight_input = inputs.addValueInput( - "weight_input", - "Weight Input", - "", - adsk.core.ValueInput.createByReal(displayWeight), - ) - weight_input.tooltip = "Robot weight" - weight_input.tooltipDescription = """(in pounds)
This is the weight of the entire robot assembly.""" - - weight_unit = inputs.addDropDownCommandInput( - "weight_unit", - "Weight Unit", - adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - - weight_unit.listItems.add("‎", imperialUnits, IconPaths.massIcons["LBS"]) - weight_unit.listItems.add("‎", not imperialUnits, IconPaths.massIcons["KG"]) - weight_unit.tooltip = "Unit of mass" - weight_unit.tooltipDescription = "
Configure the unit of mass for the weight calculation." - - weightTableInput.addCommandInput(weight_name, 0, 0) # add command inputs to table - weightTableInput.addCommandInput(auto_calc_weight, 0, 1) # add command inputs to table - weightTableInput.addCommandInput(weight_input, 0, 2) # add command inputs to table - weightTableInput.addCommandInput(weight_unit, 0, 3) # add command inputs to table + if exporterOptions.gamepieces: + for synGamepiece in exporterOptions.gamepieces: + fusionOccurrence = gm.app.activeDocument.design.findEntityByToken(synGamepiece.occurrenceToken)[0] + gamepieceConfigTab.addGamepiece(fusionOccurrence, synGamepiece) global jointConfigTab jointConfigTab = JointConfigTab(args) + generalConfigTab.jointConfigTab = jointConfigTab + + if not exporterOptions.exportMode == ExportMode.ROBOT: + jointConfigTab.isVisible = False # Transition: AARD-1685 # There remains some overlap between adding joints as wheels. # Should investigate changes to improve performance. if exporterOptions.joints: for synJoint in exporterOptions.joints: - fusionJoint = gm.app.activeDocument.design.findEntityByToken(synJoint.jointToken)[0] - jointConfigTab.addJoint(fusionJoint, synJoint) + fusionJoints = gm.app.activeDocument.design.findEntityByToken(synJoint.jointToken) + if len(fusionJoints): + jointConfigTab.addJoint(fusionJoints[0], synJoint) else: for joint in [ *gm.app.activeDocument.design.rootComponent.allJoints, *gm.app.activeDocument.design.rootComponent.allAsBuiltJoints, ]: if ( - joint.jointMotion.jointType in (JointMotions.REVOLUTE.value, JointMotions.SLIDER.value) + joint.jointMotion.jointType + in (JointMotions.REVOLUTE.value, JointMotions.SLIDER.value, JointMotions.BALL.value) and not joint.isSuppressed ): jointConfigTab.addJoint(joint) @@ -281,205 +152,9 @@ def notify(self, args): # Should consider changing how the parser handles wheels and joints to avoid overlap if exporterOptions.wheels: for wheel in exporterOptions.wheels: - fusionJoint = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken)[0] - jointConfigTab.addWheel(fusionJoint, wheel) - - # ~~~~~~~~~~~~~~~~ GAMEPIECE CONFIGURATION ~~~~~~~~~~~~~~~~ - """ - Gamepiece group command input, isVisible=False by default - - Container for gamepiece selection table - """ - gamepieceConfig = inputs.addGroupCommandInput("gamepiece_config", "Gamepiece Configuration") - gamepieceConfig.isExpanded = True - gamepieceConfig.isVisible = False - gamepieceConfig.tooltip = "Select and define the gamepieces in your field." - gamepiece_inputs = gamepieceConfig.children - - # GAMEPIECE MASS CONFIGURATION - """ - Mass unit dropdown and calculation for gamepiece elements - """ - weightTableInput_f = self.createTableInput("weight_table_f", "Weight Table", gamepiece_inputs, 3, "6:2:1", 1) - weightTableInput_f.tablePresentationStyle = 2 # set to clear background - - weight_name_f = gamepiece_inputs.addStringValueInput("weight_name", "Weight") - weight_name_f.value = "Unit of Mass" - weight_name_f.isReadOnly = True - - auto_calc_weight_f = self.createBooleanInput( # CALCULATE button - "auto_calc_weight_f", - "‎", - gamepiece_inputs, - checked=False, - tooltip="Approximate the weight of all your selected gamepieces.", - enabled=True, - isCheckBox=False, - ) - auto_calc_weight_f.resourceFolder = IconPaths.stringIcons["calculate-enabled"] - auto_calc_weight_f.isFullWidth = True - - weight_unit_f = gamepiece_inputs.addDropDownCommandInput( - "weight_unit_f", - "Unit of Mass", - adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - weight_unit_f.listItems.add("‎", True, IconPaths.massIcons["LBS"]) # add listdropdown mass options - weight_unit_f.listItems.add("‎", False, IconPaths.massIcons["KG"]) # add listdropdown mass options - weight_unit_f.tooltip = "Unit of mass" - weight_unit_f.tooltipDescription = "
Configure the unit of mass for for the weight calculation." - - weightTableInput_f.addCommandInput(weight_name_f, 0, 0) # add command inputs to table - weightTableInput_f.addCommandInput(auto_calc_weight_f, 0, 1) # add command inputs to table - weightTableInput_f.addCommandInput(weight_unit_f, 0, 2) # add command inputs to table - - # GAMEPIECE SELECTION TABLE - """ - All selected gamepieces appear here - """ - gamepieceTableInput = self.createTableInput( - "gamepiece_table", - "Gamepiece", - gamepiece_inputs, - 4, - "1:8:5:12", - 50, - ) - - addFieldInput = gamepiece_inputs.addBoolValueInput("field_add", "Add", False) - - removeFieldInput = gamepiece_inputs.addBoolValueInput("field_delete", "Remove", False) - addFieldInput.isEnabled = removeFieldInput.isEnabled = True - - removeFieldInput.tooltip = "Remove a field element" - addFieldInput.tooltip = "Add a field element" - - gamepieceSelectInput = gamepiece_inputs.addSelectionInput( - "gamepiece_select", - "Selection", - "Select the unique gamepieces in your field.", - ) - gamepieceSelectInput.addSelectionFilter("Occurrences") - gamepieceSelectInput.setSelectionLimits(0) - gamepieceSelectInput.isEnabled = True - gamepieceSelectInput.isVisible = False - - gamepieceTableInput.addToolbarCommandInput(addFieldInput) - gamepieceTableInput.addToolbarCommandInput(removeFieldInput) - - """ - Gamepiece table column headers. (the permanent captions in the first row of table) - """ - gamepieceTableInput.addCommandInput( - self.createTextBoxInput( - "e_header", - "Gamepiece name", - gamepiece_inputs, - "Gamepiece", - bold=False, - ), - 0, - 1, - ) - - gamepieceTableInput.addCommandInput( - self.createTextBoxInput( - "w_header", - "Gamepiece weight", - gamepiece_inputs, - "Weight", - background="#d9d9d9", - ), - 0, - 2, - ) - - gamepieceTableInput.addCommandInput( - self.createTextBoxInput( - "f_header", - "Friction coefficient", - gamepiece_inputs, - "Friction coefficient", - background="#d9d9d9", - ), - 0, - 3, - ) - - # ====================================== ADVANCED TAB ====================================== - """ - Creates the advanced tab, which is the parent container for internal command inputs - """ - advancedSettings: adsk.core.TabCommandInput = INPUTS_ROOT.addTabCommandInput("advanced_settings", "Advanced") - advancedSettings.tooltip = ( - "Additional Advanced Settings to change how your model will be translated into Unity." - ) - a_input: adsk.core.CommandInputs = advancedSettings.children - - # ~~~~~~~~~~~~~~~~ EXPORTER SETTINGS ~~~~~~~~~~~~~~~~ - """ - Exporter settings group command - """ - exporterSettings = a_input.addGroupCommandInput("exporter_settings", "Exporter Settings") - exporterSettings.isExpanded = True - exporterSettings.isEnabled = True - exporterSettings.tooltip = "tooltip" # TODO: update tooltip - exporter_settings = exporterSettings.children - - self.createBooleanInput( - "compress", - "Compress Output", - exporter_settings, - checked=exporterOptions.compressOutput, - tooltip="Compress the output file for a smaller file size.", - tooltipadvanced="
Use the GZIP compression system to compress the resulting file which will be opened in the simulator, perfect if you want to share the file.
", - enabled=True, - ) - - self.createBooleanInput( - "export_as_part", - "Export As Part", - exporter_settings, - checked=exporterOptions.exportAsPart, - tooltip="Use to export as a part for Mix And Match", - enabled=True, - ) - - # ~~~~~~~~~~~~~~~~ PHYSICS SETTINGS ~~~~~~~~~~~~~~~~ - """ - Physics settings group command - """ - physicsSettings: adsk.core.GroupCommandInput = a_input.addGroupCommandInput( - "physics_settings", "Physics Settings" - ) - - physicsSettings.isExpanded = True - physicsSettings.isEnabled = True - physicsSettings.tooltip = "Settings relating to the custom physics of the robot, like the wheel friction" - physics_settings: adsk.core.CommandInputs = physicsSettings.children - - frictionOverrideInput = self.createBooleanInput( - "friction_override", - "Friction Override", - physics_settings, - checked=True, - tooltip="Manually override the default friction values on the bodies in the assembly.", - enabled=True, - isCheckBox=False, - ) - frictionOverrideInput.resourceFolder = IconPaths.stringIcons["friction_override-enabled"] - frictionOverrideInput.isFullWidth = True - - valueList = [1] - for i in range(20): - valueList.append(i / 20) - - frictionCoeffSlider: adsk.core.FloatSliderCommandInput = physics_settings.addFloatSliderListCommandInput( - "friction_override_coeff", "Friction Coefficient", "", valueList - ) - frictionCoeffSlider.isVisible = True - frictionCoeffSlider.valueOne = 0.5 - frictionCoeffSlider.tooltip = "Friction coefficient of field element." - frictionCoeffSlider.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." + fusionJoints = gm.app.activeDocument.design.findEntityByToken(wheel.jointToken) + if len(fusionJoints): + jointConfigTab.addWheel(fusionJoints[0], wheel) # ~~~~~~~~~~~~~~~~ JOINT SETTINGS ~~~~~~~~~~~~~~~~ """ @@ -593,142 +268,6 @@ def notify(self, args): cmd.destroy.add(onDestroy) gm.handlers.append(onDestroy) # 8 - # Transition: AARD-1685 - # Functionality will be fully moved to `CreateCommandInputsHelper` in AARD-1683 - @logFailure - def createBooleanInput( - self, - _id: str, - name: str, - inputs: adsk.core.CommandInputs, - tooltip="", - tooltipadvanced="", - checked=True, - enabled=True, - isCheckBox=True, - ) -> adsk.core.BoolValueCommandInput: - """### Simple helper to generate all of the options for me to create a boolean command input - - Args: - _id (str): id value of the object - pretty much lowercase name - name (str): name as displayed by the command prompt - inputs (adsk.core.CommandInputs): parent command input container - tooltip (str, optional): Description on hover of the checkbox. Defaults to "". - tooltipadvanced (str, optional): Long hover description. Defaults to "". - checked (bool, optional): Is checked by default?. Defaults to True. - - Returns: - adsk.core.BoolValueCommandInput: Recently created command input - """ - _input = inputs.addBoolValueInput(_id, name, isCheckBox) - _input.value = checked - _input.isEnabled = enabled - _input.tooltip = tooltip - _input.tooltipDescription = tooltipadvanced - return _input - - # Transition: AARD-1685 - # Functionality will be fully moved to `CreateCommandInputsHelper` in AARD-1683 - @logFailure - def createTableInput( - self, - _id: str, - name: str, - inputs: adsk.core.CommandInputs, - columns: int, - ratio: str, - maxRows: int, - minRows=1, - columnSpacing=0, - rowSpacing=0, - ) -> adsk.core.TableCommandInput: - """### Simple helper to generate all the TableCommandInput options. - - Args: - _id (str): unique ID of command - name (str): displayed name - inputs (adsk.core.CommandInputs): parent command input container - columns (int): column count - ratio (str): column width ratio - maxRows (int): the maximum number of displayed rows possible - minRows (int, optional): the minimum number of displayed rows. Defaults to 1. - columnSpacing (int, optional): spacing in between the columns, in pixels. Defaults to 0. - rowSpacing (int, optional): spacing in between the rows, in pixels. Defaults to 0. - - Returns: - adsk.core.TableCommandInput: created tableCommandInput - """ - _input = inputs.addTableCommandInput(_id, name, columns, ratio) - _input.minimumVisibleRows = minRows - _input.maximumVisibleRows = maxRows - _input.columnSpacing = columnSpacing - _input.rowSpacing = rowSpacing - return _input - - # Transition: AARD-1685 - # Functionality will be fully moved to `CreateCommandInputsHelper` in AARD-1683 - @logFailure - def createTextBoxInput( - self, - _id: str, - name: str, - inputs: adsk.core.CommandInputs, - text: str, - italics=True, - bold=True, - fontSize=10, - alignment="center", - rowCount=1, - read=True, - background="whitesmoke", - tooltip="", - advanced_tooltip="", - ) -> adsk.core.TextBoxCommandInput: - """### Helper to generate a textbox input from inputted options. - - Args: - _id (str): unique ID - name (str): displayed name - inputs (adsk.core.CommandInputs): parent command input container - text (str): the user-visible text in command - italics (bool, optional): is italics? Defaults to True. - bold (bool, optional): isBold? Defaults to True. - fontSize (int, optional): fontsize. Defaults to 10. - alignment (str, optional): HTML style alignment (left, center, right). Defaults to "center". - rowCount (int, optional): number of rows in textbox. Defaults to 1. - read (bool, optional): read only? Defaults to True. - background (str, optional): background color (HTML color names or hex) Defaults to "whitesmoke". - - Returns: - adsk.core.TextBoxCommandInput: newly created textBoxCommandInput - """ - i = ["", ""] - b = ["", ""] - - if bold: - b[0] = "" - b[1] = "" - if italics: - i[0] = "" - i[1] = "" - - # simple wrapper for html formatting - wrapper = """ -
-

- %s%s{}%s%s -

- - """.format( - text - ) - _text = wrapper % (background, alignment, fontSize, b[0], i[0], i[1], b[1]) - - _input = inputs.addTextBoxCommandInput(_id, name, _text, rowCount, read) - _input.tooltip = tooltip - _input.tooltipDescription = advanced_tooltip - return _input - class ConfigureCommandExecuteHandler(adsk.core.CommandEventHandler): """### Called when Ok is pressed confirming the export @@ -749,155 +288,69 @@ 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 processedFileName = gm.app.activeDocument.name.replace(" ", "_") - dropdownExportMode = INPUTS_ROOT.itemById("mode") - if dropdownExportMode.selectedItem.index == 0: - isRobot = True - elif dropdownExportMode.selectedItem.index == 1: - isRobot = False + if generalConfigTab.exportLocation == ExportLocation.DOWNLOAD: + savepath = FileDialogConfig.saveFileDialog(defaultPath="~/Documents/") - processedFileName = gm.app.activeDocument.name.replace(" ", "_") - dropdownExportMode = INPUTS_ROOT.itemById("mode") - if dropdownExportMode.selectedItem.index == 0: - isRobot = True - elif dropdownExportMode.selectedItem.index == 1: - isRobot = False - dropdownExportLocation = INPUTS_ROOT.itemById("location") - if dropdownExportLocation.selectedItem.index == 1: # Download - savepath = FileDialogConfig.saveFileDialog(defaultPath=exporterOptions.fileLocation) - - if savepath == False: + if not savepath: # save was canceled return - updatedPath = pathlib.Path(savepath).parent - if updatedPath != self.current.filePath: - self.current.filePath = str(updatedPath) + # Transition: AARD-1742 + # With the addition of a 'release' build the fusion exporter will not have permissions within the sourced + # folder. Because of this we cannot use this kind of tmp path anymore. This code was already unused and + # should be removed. + # updatedPath = pathlib.Path(savepath).parent + # if updatedPath != self.current.filePath: + # self.current.filePath = str(updatedPath) else: savepath = processedFileName adsk.doEvents() - # get active document - design = gm.app.activeDocument.design - name = design.rootComponent.name.rsplit(" ", 1)[0] - version = design.rootComponent.name.rsplit(" ", 1)[1] - - _exportGamepieces = [] # TODO work on the code to populate Gamepiece - _robotWeight = float - _mode: ExportMode - _location: ExportLocation - - """ - Loops through all rows in the gamepiece table to extract the input values - """ - gamepieceTableInput = gamepieceTable() - weight_unit_f = INPUTS_ROOT.itemById("weight_unit_f") - for row in range(gamepieceTableInput.rowCount): - if row == 0: - continue - - weightValue = gamepieceTableInput.getInputAtPosition(row, 2).value # weight/mass input, float - - if weight_unit_f.selectedItem.index == 0: - weightValue /= 2.2046226218 - frictionValue = gamepieceTableInput.getInputAtPosition(row, 3).valueOne # friction value, float - - _exportGamepieces.append( - Gamepiece( - guid_occurrence(GamepieceListGlobal[row - 1]), - weightValue, - frictionValue, - ) - ) - - """ - Robot Weight - """ - weight_input = INPUTS_ROOT.itemById("weight_input") - weight_unit = INPUTS_ROOT.itemById("weight_unit") - - if weight_unit.selectedItem.index == 0: - selectedUnits = PreferredUnits.IMPERIAL - _robotWeight = float(weight_input.value) / 2.2046226218 - else: - selectedUnits = PreferredUnits.METRIC - _robotWeight = float(weight_input.value) - - """ - Export Mode - """ - dropdownExportMode = INPUTS_ROOT.itemById("mode") - if dropdownExportMode.selectedItem.index == 0: - _mode = ExportMode.ROBOT - elif dropdownExportMode.selectedItem.index == 1: - _mode = ExportMode.FIELD + design = gm.app.activeDocument.design - """ - Export Location - """ - dropdownExportLocation = INPUTS_ROOT.itemById("location") - if dropdownExportLocation.selectedItem.index == 0: - _location = ExportLocation.UPLOAD - elif dropdownExportLocation.selectedItem.index == 1: - _location = ExportLocation.DOWNLOAD + name_split: list[str] = design.rootComponent.name.split(" ") + if len(name_split) < 2: + gm.ui.messageBox("Please open the robot design you would like to export", "Synthesis: Error") + return - """ - Advanced Settings - """ - global compress - compress = ( - eventArgs.command.commandInputs.itemById("advanced_settings") - .children.itemById("exporter_settings") - .children.itemById("compress") - ).value + name = name_split[0] + version = name_split[1] selectedJoints, selectedWheels = jointConfigTab.getSelectedJointsAndWheels() - - export_as_part_boolean = ( - eventArgs.command.commandInputs.itemById("advanced_settings") - .children.itemById("exporter_settings") - .children.itemById("export_as_part") - ).value - - frictionOverrideSlider = ( - eventArgs.command.commandInputs.itemById("advanced_settings") - .children.itemById("physics_settings") - .children.itemById("friction_override_coeff") - ) - - frictionOverride = frictionOverrideSlider.isVisible - frictionOverrideCoeff = frictionOverrideSlider.valueOne + selectedGamepieces = gamepieceConfigTab.getGamepieces() exporterOptions = ExporterOptions( - savepath, + str(savepath), name, version, materials=0, joints=selectedJoints, wheels=selectedWheels, - gamepieces=_exportGamepieces, - preferredUnits=selectedUnits, - robotWeight=_robotWeight, - exportMode=_mode, - exportLocation=_location, - compressOutput=compress, - exportAsPart=export_as_part_boolean, - frictionOverride=frictionOverride, - frictionOverrideCoeff=frictionOverrideCoeff, + gamepieces=selectedGamepieces, + robotWeight=generalConfigTab.robotWeight, + autoCalcRobotWeight=generalConfigTab.autoCalculateWeight, + autoCalcGamepieceWeight=gamepieceConfigTab.autoCalculateWeight, + exportMode=generalConfigTab.exportMode, + exportLocation=generalConfigTab.exportLocation, + compressOutput=generalConfigTab.compress, + exportAsPart=generalConfigTab.exportAsPart, + frictionOverride=generalConfigTab.overrideFriction, + frictionOverrideCoeff=generalConfigTab.frictionOverrideCoeff, + openSynthesisUponExport=generalConfigTab.openSynthesisUponExport, ) Parser(exporterOptions).export() @@ -907,6 +360,12 @@ def notify(self, args): # If we run into an exporting error we should return back to the panel with all current options # still in tact. Even if they did not save. jointConfigTab.reset() + gamepieceConfigTab.reset() + + if generalConfigTab.openSynthesisUponExport: + res = webbrowser.open(APP_WEBSITE_URL) + if not res: + gm.ui.messageBox("Failed to open Synthesis in your default browser.") class CommandExecutePreviewHandler(adsk.core.CommandEventHandler): @@ -916,44 +375,19 @@ 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: args (CommandEventArgs): command event argument """ - try: - eventArgs = adsk.core.CommandEventArgs.cast(args) - # inputs = eventArgs.command.commandInputs # equivalent to INPUTS_ROOT global - - auto_calc_weight_f = INPUTS_ROOT.itemById("auto_calc_weight_f") - - addFieldInput = INPUTS_ROOT.itemById("field_add") - removeFieldInput = INPUTS_ROOT.itemById("field_delete") - - # Transition: AARD-1685 - # This is how all preview handles should be done in the future - jointConfigTab.handlePreviewEvent(args) - - gamepieceTableInput = gamepieceTable() - if gamepieceTableInput.rowCount <= 1: - removeFieldInput.isEnabled = auto_calc_weight_f.isEnabled = False - else: - removeFieldInput.isEnabled = auto_calc_weight_f.isEnabled = True - - if not addFieldInput.isEnabled or not removeFieldInput: - for gamepiece in GamepieceListGlobal: - gamepiece.component.opacity = 0.25 - CustomGraphics.createTextGraphics(gamepiece, GamepieceListGlobal) - else: - gm.app.activeDocument.design.rootComponent.opacity = 1 - except AttributeError: - pass + jointConfigTab.handlePreviewEvent(args) + gamepieceConfigTab.handlePreviewEvent(args) class MySelectHandler(adsk.core.SelectionEventHandler): @@ -965,22 +399,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 @@ -1001,7 +439,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: @@ -1069,44 +507,19 @@ 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: args (SelectionEventArgs): A selection event argument """ - # eventArgs = adsk.core.SelectionEventArgs.cast(args) - - self.selectedOcc = adsk.fusion.Occurrence.cast(args.selection.entity) - self.selectedJoint = args.selection.entity - - selectionInput = args.activeInput - - dropdownExportMode = INPUTS_ROOT.itemById("mode") - duplicateSelection = INPUTS_ROOT.itemById("duplicate_selection") - # indicator = INPUTS_ROOT.itemById("algorithmic_indicator") - - if self.selectedOcc: - self.cmd.setCursor("", 0, 0) - if dropdownExportMode.selectedItem.index == 1: - occurrenceList = gm.app.activeDocument.design.rootComponent.allOccurrencesByComponent( - self.selectedOcc.component - ) - for occ in occurrenceList: - if occ not in GamepieceListGlobal: - addGamepieceToTable(occ) - else: - removeGamePieceFromTable(GamepieceListGlobal.index(occ)) - - selectionInput.isEnabled = False - selectionInput.isVisible = False + if gamepieceConfigTab.isVisible: + self.cmd.setCursor("", 0, 0) # Reset select cursor back to normal cursor. + gamepieceConfigTab.handleSelectionEvent(args, args.selection.entity) - # Transition: AARD-1685 - # This is how all handle selection events should be done in the future although it will look - # slightly differently for each type of handle. - elif self.selectedJoint: + if jointConfigTab.isVisible: self.cmd.setCursor("", 0, 0) # Reset select cursor back to normal cursor. - jointConfigTab.handleSelectionEvent(args, self.selectedJoint) + jointConfigTab.handleSelectionEvent(args, args.selection.entity) class MyPreSelectHandler(adsk.core.SelectionEventHandler): @@ -1116,12 +529,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) @@ -1133,37 +546,7 @@ def notify(self, args): return preSelected = preSelectedOcc if preSelectedOcc else preSelectedJoint - - dropdownExportMode = INPUTS_ROOT.itemById("mode") - if preSelected and design: - if dropdownExportMode.selectedItem.index == 0: # Dynamic - if preSelected.entityToken in onSelect.allWheelPreselections: - self.cmd.setCursor( - IconPaths.mouseIcons["remove"], - 0, - 0, - ) - else: - self.cmd.setCursor( - IconPaths.mouseIcons["add"], - 0, - 0, - ) - - elif dropdownExportMode.selectedItem.index == 1: # Static - if preSelected.entityToken in onSelect.allGamepiecePreselections: - self.cmd.setCursor( - IconPaths.mouseIcons["remove"], - 0, - 0, - ) - else: - self.cmd.setCursor( - IconPaths.mouseIcons["add"], - 0, - 0, - ) - else: # Should literally be impossible? - Brandon + if not preSelected: self.cmd.setCursor("", 0, 0) @@ -1173,12 +556,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) @@ -1194,7 +577,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] @@ -1202,7 +585,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 @@ -1210,218 +593,15 @@ def reset(self): self.cmd.setCursor("", 0, 0) gm.ui.activeSelections.clear() - @logFailure - def weight(self, isLbs=True): # maybe add a progress dialog?? - """### Get the total design weight using the predetermined units. - - Args: - isLbs (bool, optional): Is selected mass unit pounds? Defaults to True. - - Returns: - value (float): weight value in specified unit - """ - if gm.app.activeDocument.design: - massCalculation = FullMassCalculation() - totalMass = massCalculation.getTotalMass() + def notify(self, args: adsk.core.InputChangedEventArgs) -> None: + if generalConfigTab.isActive: + generalConfigTab.handleInputChanged(args) - value = float + if jointConfigTab.isVisible and jointConfigTab.isActive: + jointConfigTab.handleInputChanged(args, INPUTS_ROOT) - self.allWeights[0] = round(totalMass * 2.2046226218, 2) - - self.allWeights[1] = round(totalMass, 2) - - if isLbs: - value = self.allWeights[0] - else: - value = self.allWeights[1] - - value = round(value, 2) # round weight to 2 decimals places - return value - - @logFailure(messageBox=True) - def notify(self, args): - eventArgs = adsk.core.InputChangedEventArgs.cast(args) - cmdInput = eventArgs.input - - # Transition: AARD-1685 - # Should be how all input changed handles are done in the future - jointConfigTab.handleInputChanged(args, INPUTS_ROOT) - - MySelectHandler.lastInputCmd = cmdInput - inputs = cmdInput.commandInputs - onSelect = gm.handlers[3] - - frictionCoeff = INPUTS_ROOT.itemById("friction_override_coeff") - - gamepieceSelect = inputs.itemById("gamepiece_select") - gamepieceTableInput = gamepieceTable() - weightTableInput = inputs.itemById("weight_table") - - weight_input = INPUTS_ROOT.itemById("weight_input") - gamepieceConfig = inputs.itemById("gamepiece_config") - addFieldInput = INPUTS_ROOT.itemById("field_add") - - indicator = INPUTS_ROOT.itemById("algorithmic_indicator") - - # gm.ui.messageBox(cmdInput.id) # DEBUG statement, displays CommandInput user-defined id - - position = int - - if cmdInput.id == "mode": - modeDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - - if modeDropdown.selectedItem.index == 0: - if gamepieceConfig: - gm.ui.activeSelections.clear() - gm.app.activeDocument.design.rootComponent.opacity = 1 - - gamepieceConfig.isVisible = False - weightTableInput.isVisible = True - - addFieldInput.isEnabled = True - - elif modeDropdown.selectedItem.index == 1: - if gamepieceConfig: - gm.ui.activeSelections.clear() - gm.app.activeDocument.design.rootComponent.opacity = 1 - - elif cmdInput.id == "blank_gp" or cmdInput.id == "name_gp" or cmdInput.id == "weight_gp": - self.reset() - - gamepieceSelect.isEnabled = False - addFieldInput.isEnabled = True - - cmdInput_str = cmdInput.id - - if cmdInput_str == "name_gp": - position = gamepieceTableInput.getPosition(adsk.core.TextBoxCommandInput.cast(cmdInput))[1] - 1 - elif cmdInput_str == "weight_gp": - position = gamepieceTableInput.getPosition(adsk.core.ValueCommandInput.cast(cmdInput))[1] - 1 - elif cmdInput_str == "blank_gp": - position = gamepieceTableInput.getPosition(adsk.core.ImageCommandInput.cast(cmdInput))[1] - 1 - else: - position = gamepieceTableInput.getPosition(adsk.core.FloatSliderCommandInput.cast(cmdInput))[1] - 1 - - gm.ui.activeSelections.add(GamepieceListGlobal[position]) - - elif cmdInput.id == "field_add": - self.reset() - - gamepieceSelect.isVisible = True - gamepieceSelect.isEnabled = True - gamepieceSelect.clearSelection() - addFieldInput.isEnabled = False - - elif cmdInput.id == "field_delete": - gm.ui.activeSelections.clear() - - addFieldInput.isEnabled = True - - if gamepieceTableInput.selectedRow == -1 or gamepieceTableInput.selectedRow == 0: - gamepieceTableInput.selectedRow = gamepieceTableInput.rowCount - 1 - gm.ui.messageBox("Select a row to delete.") - else: - index = gamepieceTableInput.selectedRow - 1 - removeGamePieceFromTable(index) - - elif cmdInput.id == "gamepiece_select": - addFieldInput.isEnabled = True - - elif cmdInput.id == "friction_override": - boolValue = adsk.core.BoolValueCommandInput.cast(cmdInput) - - if boolValue.value: - frictionCoeff.isVisible = True - else: - frictionCoeff.isVisible = False - - elif cmdInput.id == "weight_unit": - unitDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - weightInput = weightTableInput.getInputAtPosition(0, 2) - if unitDropdown.selectedItem.index == 0: - self.isLbs = True - - weightInput.tooltipDescription = ( - """(in pounds)
This is the weight of the entire robot assembly.""" - ) - elif unitDropdown.selectedItem.index == 1: - self.isLbs = False - - weightInput.tooltipDescription = ( - """(in kilograms)
This is the weight of the entire robot assembly.""" - ) - - elif cmdInput.id == "weight_unit_f": - unitDropdown = adsk.core.DropDownCommandInput.cast(cmdInput) - if unitDropdown.selectedItem.index == 0: - self.isLbs_f = True - - for row in range(gamepieceTableInput.rowCount): - if row == 0: - continue - weightInput = gamepieceTableInput.getInputAtPosition(row, 2) - weightInput.tooltipDescription = "(in pounds)" - elif unitDropdown.selectedItem.index == 1: - self.isLbs_f = False - - for row in range(gamepieceTableInput.rowCount): - if row == 0: - continue - weightInput = gamepieceTableInput.getInputAtPosition(row, 2) - weightInput.tooltipDescription = "(in kilograms)" - - elif cmdInput.id == "auto_calc_weight": - button = adsk.core.BoolValueCommandInput.cast(cmdInput) - - if button.value == True: # CALCULATE button pressed - if self.allWeights.count(None) == 2: # if button is pressed for the first time - if self.isLbs: # if pounds unit selected - self.allWeights[0] = self.weight() - weight_input.value = self.allWeights[0] - else: # if kg unit selected - self.allWeights[1] = self.weight(False) - weight_input.value = self.allWeights[1] - else: # if a mass value has already been configured - if ( - weight_input.value != self.allWeights[0] - or weight_input.value != self.allWeights[1] - or not weight_input.isValidExpression - ): - if self.isLbs: - weight_input.value = self.allWeights[0] - else: - weight_input.value = self.allWeights[1] - - elif cmdInput.id == "auto_calc_weight_f": - button = adsk.core.BoolValueCommandInput.cast(cmdInput) - - if button.value == True: # CALCULATE button pressed - if self.isLbs_f: - for row in range(gamepieceTableInput.rowCount): - if row == 0: - continue - weightInput = gamepieceTableInput.getInputAtPosition(row, 2) - physical = GamepieceListGlobal[row - 1].component.getPhysicalProperties( - adsk.fusion.CalculationAccuracy.LowCalculationAccuracy - ) - value = round(physical.mass * 2.2046226218, 2) - weightInput.value = value - - else: - for row in range(gamepieceTableInput.rowCount): - if row == 0: - continue - weightInput = gamepieceTableInput.getInputAtPosition(row, 2) - physical = GamepieceListGlobal[row - 1].component.getPhysicalProperties( - adsk.fusion.CalculationAccuracy.LowCalculationAccuracy - ) - value = round(physical.mass, 2) - weightInput.value = value - elif cmdInput.id == "compress": - checkBox = adsk.core.BoolValueCommandInput.cast(cmdInput) - if checkBox.value: - global compress - compress = checkBox.value + if gamepieceConfigTab.isVisible and gamepieceConfigTab.isActive: + gamepieceConfigTab.handleInputChanged(args, INPUTS_ROOT) class MyCommandDestroyHandler(adsk.core.CommandEventHandler): @@ -1431,122 +611,10 @@ class MyCommandDestroyHandler(adsk.core.CommandEventHandler): Args: CommandEventHandler """ - def __init__(self): - super().__init__() - @logFailure(messageBox=True) - def notify(self, args): - onSelect = gm.handlers[3] - + def notify(self, args: adsk.core.CommandEventArgs) -> None: jointConfigTab.reset() - GamepieceListGlobal.clear() - onSelect.allWheelPreselections.clear() - onSelect.wheelJointList.clear() + gamepieceConfigTab.reset() for group in gm.app.activeDocument.design.rootComponent.customGraphicsGroups: group.deleteMe() - - # Currently causes Internal Autodesk Error - # gm.ui.activeSelections.clear() - gm.app.activeDocument.design.rootComponent.opacity = 1 - - -@logFailure -def addGamepieceToTable(gamepiece: adsk.fusion.Occurrence) -> None: - """### Adds a gamepiece occurrence to its global list and gamepiece table. - - Args: - gamepiece (adsk.fusion.Occurrence): Gamepiece occurrence to be added - """ - onSelect = gm.handlers[3] - gamepieceTableInput = gamepieceTable() - - def addPreselections(child_occurrences): - for occ in child_occurrences: - onSelect.allGamepiecePreselections.append(occ.entityToken) - - if occ.childOccurrences: - addPreselections(occ.childOccurrences) - - if gamepiece.childOccurrences: - addPreselections(gamepiece.childOccurrences) - else: - onSelect.allGamepiecePreselections.append(gamepiece.entityToken) - - GamepieceListGlobal.append(gamepiece) - cmdInputs = adsk.core.CommandInputs.cast(gamepieceTableInput.commandInputs) - blankIcon = cmdInputs.addImageCommandInput("blank_gp", "Blank", IconPaths.gamepieceIcons["blank"]) - - type = cmdInputs.addTextBoxCommandInput("name_gp", "Occurrence name", gamepiece.name, 1, True) - - value = 0.0 - physical = gamepiece.component.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) - value = physical.mass - - # check if dropdown unit is kg or lbs. bool value taken from ConfigureCommandInputChanged - massUnitInString = "" - onInputChanged = gm.handlers[1] - if onInputChanged.isLbs_f: - value = round(value * 2.2046226218, 2) # lbs - massUnitInString = "(in pounds)" - else: - value = round(value, 2) # kg - massUnitInString = "(in kilograms)" - - weight = cmdInputs.addValueInput( - "weight_gp", - "Weight Input", - "", - adsk.core.ValueInput.createByString(str(value)), - ) - - valueList = [1] - for i in range(20): - valueList.append(i / 20) - - friction_coeff = cmdInputs.addFloatSliderListCommandInput("friction_coeff", "", "", valueList) - friction_coeff.valueOne = 0.5 - - type.tooltip = gamepiece.name - - weight.tooltip = "Weight of field element" - weight.tooltipDescription = massUnitInString - - friction_coeff.tooltip = "Friction coefficient of field element" - friction_coeff.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." - row = gamepieceTableInput.rowCount - - gamepieceTableInput.addCommandInput(blankIcon, row, 0) - gamepieceTableInput.addCommandInput(type, row, 1) - gamepieceTableInput.addCommandInput(weight, row, 2) - gamepieceTableInput.addCommandInput(friction_coeff, row, 3) - - -@logFailure -def removeGamePieceFromTable(index: int) -> None: - """### Removes a gamepiece occurrence from its global list and gamepiece table. - - Args: - index (int): index of gamepiece item in its global list. - """ - onSelect = gm.handlers[3] - gamepieceTableInput = gamepieceTable() - gamepiece = GamepieceListGlobal[index] - - def removePreselections(child_occurrences): - for occ in child_occurrences: - onSelect.allGamepiecePreselections.remove(occ.entityToken) - - if occ.childOccurrences: - removePreselections(occ.childOccurrences) - - try: - if gamepiece.childOccurrences: - removePreselections(GamepieceListGlobal[index].childOccurrences) - else: - onSelect.allGamepiecePreselections.remove(gamepiece.entityToken) - - del GamepieceListGlobal[index] - gamepieceTableInput.deleteRow(index + 1) - except IndexError: - pass diff --git a/exporter/SynthesisFusionAddin/src/UI/Configuration/SerialCommand.py b/exporter/SynthesisFusionAddin/src/UI/Configuration/SerialCommand.py index 16084148c5..61c1e30108 100644 --- a/exporter/SynthesisFusionAddin/src/UI/Configuration/SerialCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/Configuration/SerialCommand.py @@ -7,9 +7,11 @@ import json -from ...Types import OString +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/CreateCommandInputsHelper.py b/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py index 536b93b57d..1f78b469bd 100644 --- a/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py +++ b/exporter/SynthesisFusionAddin/src/UI/CreateCommandInputsHelper.py @@ -1,6 +1,6 @@ import adsk.core -from ..Logging import logFailure +from src.Logging import logFailure @logFailure diff --git a/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py b/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py index 52a49fa5d4..58a331a198 100644 --- a/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py +++ b/exporter/SynthesisFusionAddin/src/UI/CustomGraphics.py @@ -1,15 +1,12 @@ -import logging -import traceback - import adsk.core import adsk.fusion -from ..general_imports import * -from ..Logging import logFailure +from src import gm +from src.Logging import logFailure @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 281ebf3f0e..c8da4590a6 100644 --- a/exporter/SynthesisFusionAddin/src/UI/Events.py +++ b/exporter/SynthesisFusionAddin/src/UI/Events.py @@ -1,7 +1,10 @@ -from typing import Sequence, Tuple +import json +from typing import Sequence -from ..general_imports import * -from ..Logging import getLogger +import adsk.core + +from src import gm +from src.Logging import getLogger """ # This file is Special It links all function names to command requests that palletes can make automatically @@ -11,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 """ @@ -58,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 6f6f764cd9..d7708bfec6 100644 --- a/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py +++ b/exporter/SynthesisFusionAddin/src/UI/FileDialogConfig.py @@ -1,15 +1,15 @@ -from typing import Union +import os +import tempfile +from pathlib import Path import adsk.core import adsk.fusion -from ..general_imports import * +from src import gm +from src.Types import OString -# from ..proto_out import Configuration_pb2 -from ..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: @@ -17,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" @@ -40,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 @@ -55,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) @@ -78,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 new file mode 100644 index 0000000000..842389a667 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/UI/GamepieceConfigTab.py @@ -0,0 +1,311 @@ +import adsk.core +import adsk.fusion + +from src.Logging import logFailure +from src.Parser.ExporterOptions import ExporterOptions +from src.Parser.SynthesisParser.Utilities import guid_occurrence +from src.Types import Gamepiece, UnitSystem +from src.UI.CreateCommandInputsHelper import ( + createBooleanInput, + createTableInput, + createTextBoxInput, +) +from src.Util import convertMassUnitsFrom, convertMassUnitsTo, getFusionUnitSystem + + +class GamepieceConfigTab: + selectedGamepieceList: list[adsk.fusion.Occurrence] = [] + selectedGamepieceEntityIDs: set[str] = set() + gamepieceConfigTab: adsk.core.TabCommandInput + gamepieceTable: adsk.core.TableCommandInput + previousAutoCalcWeightCheckboxState: bool + previousSelectedUnitDropdownIndex: int + + @logFailure + def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: ExporterOptions) -> None: + inputs = args.command.commandInputs + self.gamepieceConfigTab = inputs.addTabCommandInput("gamepieceSettings", "Gamepiece Settings") + self.gamepieceConfigTab.tooltip = "Field gamepiece configuration options." + gamepieceTabInputs = self.gamepieceConfigTab.children + + createBooleanInput( + "autoCalcGamepieceWeight", + "Auto Calculate Gamepiece Weight", + gamepieceTabInputs, + checked=exporterOptions.autoCalcGamepieceWeight, + tooltip="Approximate the weight of each selected gamepiece.", + ) + self.previousAutoCalcWeightCheckboxState = exporterOptions.autoCalcGamepieceWeight + + self.gamepieceTable = createTableInput( + "gamepieceTable", + "Gamepiece", + gamepieceTabInputs, + 4, + "8:5:12", + ) + + self.gamepieceTable.addCommandInput( + createTextBoxInput("gamepieceNameHeader", "Name", gamepieceTabInputs, "Name", bold=False), 0, 0 + ) + fusUnitSystem = getFusionUnitSystem() + self.gamepieceTable.addCommandInput( + createTextBoxInput( + "gamepieceWeightHeader", + "Weight", + gamepieceTabInputs, + f"Weight {'(lbs)' if fusUnitSystem is UnitSystem.IMPERIAL else '(kg)'}", + bold=False, + ), + 0, + 1, + ) + self.gamepieceTable.addCommandInput( + createTextBoxInput( + "frictionHeader", + "Gamepiece Friction Coefficient", + gamepieceTabInputs, + "Gamepiece Friction Coefficient", + background="#d9d9d9", + ), + 0, + 2, + ) + gamepieceSelectCancelButton = gamepieceTabInputs.addBoolValueInput( + "gamepieceSelectCancelButton", "Cancel", False + ) + gamepieceSelectCancelButton.isEnabled = gamepieceSelectCancelButton.isVisible = False + + gamepieceAddButton = gamepieceTabInputs.addBoolValueInput("gamepieceAddButton", "Add", False) + gamepieceRemoveButton = gamepieceTabInputs.addBoolValueInput("gamepieceRemoveButton", "Remove", False) + gamepieceAddButton.isEnabled = gamepieceRemoveButton.isEnabled = True + + gamepieceSelect = gamepieceTabInputs.addSelectionInput( + "gamepieceSelect", "Selection", "Select the unique gamepieces in your field." + ) + gamepieceSelect.addSelectionFilter("Occurrences") + gamepieceSelect.setSelectionLimits(0) + gamepieceSelect.isEnabled = gamepieceSelect.isVisible = False # Visibility is triggered by `gamepieceAddButton` + + self.gamepieceTable.addToolbarCommandInput(gamepieceAddButton) + self.gamepieceTable.addToolbarCommandInput(gamepieceRemoveButton) + self.gamepieceTable.addToolbarCommandInput(gamepieceSelectCancelButton) + + gamepieceTabInputs.addTextBoxCommandInput("gamepieceTabSpacer", "", "", 1, True) + + self.reset() + + @property + def isVisible(self) -> bool: + 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 autoCalculateWeight(self) -> bool: + autoCalcWeightButton: adsk.core.BoolValueCommandInput = self.gamepieceConfigTab.children.itemById( + "autoCalcGamepieceWeight" + ) + return autoCalcWeightButton.value or False + + @logFailure + def weightInputs(self) -> list[adsk.core.ValueCommandInput]: + gamepieceWeightInputs = [] + for row in range(1, self.gamepieceTable.rowCount): # Row is 1 indexed + gamepieceWeightInputs.append(self.gamepieceTable.getInputAtPosition(row, 1)) + + return gamepieceWeightInputs + + @logFailure + def addGamepiece(self, gamepiece: adsk.fusion.Occurrence, synGamepiece: Gamepiece | None = None) -> bool: + if gamepiece.entityToken in self.selectedGamepieceEntityIDs: + return False + + def addChildOccurrences(childOccurrences: adsk.fusion.OccurrenceList) -> None: + for occ in childOccurrences: + self.selectedGamepieceEntityIDs.add(occ.entityToken) + + if occ.childOccurrences: + addChildOccurrences(occ.childOccurrences) + + if gamepiece.childOccurrences: + addChildOccurrences(gamepiece.childOccurrences) + else: + self.selectedGamepieceEntityIDs.add(gamepiece.entityToken) + + self.selectedGamepieceList.append(gamepiece) + + commandInputs = self.gamepieceTable.commandInputs + gamepieceName = commandInputs.addTextBoxCommandInput( + "gamepieceName", "Occurrence Name", gamepiece.name, 1, True + ) + gamepieceName.tooltip = gamepiece.name + + valueList = [1] + [i / 20 for i in range(20)] + frictionCoefficient = commandInputs.addFloatSliderListCommandInput( + "gamepieceFrictionCoefficient", "", "", valueList + ) + frictionCoefficient.tooltip = "Friction coefficient of field element" + frictionCoefficient.tooltipDescription = "Friction coefficients range from 0 (ice) to 1 (rubber)." + if synGamepiece: + frictionCoefficient.valueOne = synGamepiece.friction + else: + frictionCoefficient.valueOne = 0.5 + + physical = gamepiece.component.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) + gamepieceMass = round(convertMassUnitsFrom(physical.mass), 2) + weight = commandInputs.addValueInput( + "gamepieceWeight", "Weight Input", "", adsk.core.ValueInput.createByString(str(gamepieceMass)) + ) + weight.tooltip = "Weight of field element" + weight.isEnabled = not self.previousAutoCalcWeightCheckboxState + + row = self.gamepieceTable.rowCount + self.gamepieceTable.addCommandInput(gamepieceName, row, 0) + self.gamepieceTable.addCommandInput(weight, row, 1) + self.gamepieceTable.addCommandInput(frictionCoefficient, row, 2) + + return True + + @logFailure + def removeIndexedGamepiece(self, index: int) -> None: + self.removeGamepiece(self.selectedGamepieceList[index]) + + @logFailure + def removeGamepiece(self, gamepiece: adsk.fusion.Occurrence) -> None: + def removeChildOccurrences(childOccurrences: adsk.fusion.OccurrenceList) -> None: + for occ in childOccurrences: + self.selectedGamepieceEntityIDs.remove(occ.entityToken) + + if occ.childOccurrences: + removeChildOccurrences(occ.childOccurrences) + + if gamepiece.childOccurrences: + removeChildOccurrences(gamepiece.childOccurrences) + else: + self.selectedGamepieceEntityIDs.remove(gamepiece.entityToken) + + i = self.selectedGamepieceList.index(gamepiece) + self.selectedGamepieceList.remove(gamepiece) + self.gamepieceTable.deleteRow(i + 1) # Row is 1 indexed + + @logFailure + def getGamepieces(self) -> list[Gamepiece]: + gamepieces: list[Gamepiece] = [] + for row in range(1, self.gamepieceTable.rowCount): # Row is 1 indexed + gamepieceEntityToken = guid_occurrence(self.selectedGamepieceList[row - 1]) + gamepieceWeight = convertMassUnitsTo(self.gamepieceTable.getInputAtPosition(row, 1).value) + gamepieceFrictionCoefficient = self.gamepieceTable.getInputAtPosition(row, 2).valueOne + gamepieces.append(Gamepiece(gamepieceEntityToken, gamepieceWeight, gamepieceFrictionCoefficient)) + + return gamepieces + + def reset(self) -> None: + self.selectedGamepieceEntityIDs.clear() + self.selectedGamepieceList.clear() + + @logFailure + def calcGamepieceWeights(self) -> None: + for row in range(1, self.gamepieceTable.rowCount): # Row is 1 indexed + weightInput: adsk.core.ValueCommandInput = self.gamepieceTable.getInputAtPosition(row, 1) + physical = self.selectedGamepieceList[row - 1].component.getPhysicalProperties( + adsk.fusion.CalculationAccuracy.LowCalculationAccuracy + ) + weightInput.value = round(convertMassUnitsFrom(physical.mass), 2) + + @logFailure + def handleInputChanged( + self, args: adsk.core.InputChangedEventArgs, globalCommandInputs: adsk.core.CommandInputs + ) -> None: + gamepieceAddButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("gamepieceAddButton") + gamepieceTable: adsk.core.TableCommandInput = args.inputs.itemById("gamepieceTable") + gamepieceRemoveButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById("gamepieceRemoveButton") + gamepieceSelectCancelButton: adsk.core.BoolValueCommandInput = globalCommandInputs.itemById( + "gamepieceSelectCancelButton" + ) + gamepieceSelection: adsk.core.SelectionCommandInput = self.gamepieceConfigTab.children.itemById( + "gamepieceSelect" + ) + spacer: adsk.core.SelectionCommandInput = self.gamepieceConfigTab.children.itemById("gamepieceTabSpacer") + + commandInput = args.input + if commandInput.id == "autoCalcGamepieceWeight": + autoCalcWeightButton = adsk.core.BoolValueCommandInput.cast(commandInput) + if autoCalcWeightButton.value == self.previousAutoCalcWeightCheckboxState: + return + + if autoCalcWeightButton.value: + self.calcGamepieceWeights() + for weightInput in self.weightInputs(): + weightInput.isEnabled = False + else: + for weightInput in self.weightInputs(): + weightInput.isEnabled = True + + self.previousAutoCalcWeightCheckboxState = autoCalcWeightButton.value + + elif commandInput.id == "gamepieceAddButton": + gamepieceSelection.isVisible = gamepieceSelection.isEnabled = True + gamepieceSelection.clearSelection() + gamepieceAddButton.isEnabled = gamepieceRemoveButton.isEnabled = False + gamepieceSelectCancelButton.isVisible = gamepieceSelectCancelButton.isEnabled = True + spacer.isVisible = False + + elif commandInput.id == "gamepieceRemoveButton": + gamepieceAddButton.isEnabled = True + if gamepieceTable.selectedRow == -1 or gamepieceTable.selectedRow == 0: + ui = adsk.core.Application.get().userInterface + ui.messageBox("Select a row to delete.") + else: + self.removeIndexedGamepiece(gamepieceTable.selectedRow - 1) # selectedRow is 1 indexed + + elif commandInput.id == "gamepieceSelectCancelButton": + gamepieceSelection.isEnabled = gamepieceSelection.isVisible = False + gamepieceSelectCancelButton.isEnabled = gamepieceSelectCancelButton.isVisible = False + gamepieceAddButton.isEnabled = gamepieceRemoveButton.isEnabled = True + spacer.isVisible = True + + @logFailure + def handleSelectionEvent(self, args: adsk.core.SelectionEventArgs, selectedOcc: adsk.fusion.Occurrence) -> None: + selectionInput = args.activeInput + rootComponent = adsk.core.Application.get().activeDocument.design.rootComponent + occurrenceList: list[adsk.fusion.Occurrence] = rootComponent.allOccurrencesByComponent(selectedOcc.component) + for occ in occurrenceList: + if not self.addGamepiece(occ): + ui = adsk.core.Application.get().userInterface + result = ui.messageBox( + "You have already selected this Gamepiece.\nWould you like to remove it?", + "Synthesis: Remove Gamepiece Confirmation", + adsk.core.MessageBoxButtonTypes.YesNoButtonType, + adsk.core.MessageBoxIconTypes.QuestionIconType, + ) + + if result == adsk.core.DialogResults.DialogYes: + self.removeGamepiece(occ) + + selectionInput.isEnabled = selectionInput.isVisible = False + + @logFailure + def handlePreviewEvent(self, args: adsk.core.CommandEventArgs) -> None: + commandInputs = args.command.commandInputs + gamepieceAddButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("gamepieceAddButton") + gamepieceRemoveButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("gamepieceRemoveButton") + gamepieceSelectCancelButton: adsk.core.BoolValueCommandInput = commandInputs.itemById( + "gamepieceSelectCancelButton" + ) + gamepieceSelection: adsk.core.SelectionCommandInput = self.gamepieceConfigTab.children.itemById( + "gamepieceSelect" + ) + spacer: adsk.core.SelectionCommandInput = self.gamepieceConfigTab.children.itemById("gamepieceTabSpacer") + + gamepieceRemoveButton.isEnabled = self.gamepieceTable.rowCount > 1 + if not gamepieceSelection.isEnabled: + gamepieceAddButton.isEnabled = True + gamepieceSelectCancelButton.isVisible = gamepieceSelectCancelButton.isEnabled = False + spacer.isVisible = True diff --git a/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py new file mode 100644 index 0000000000..f734113975 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/UI/GeneralConfigTab.py @@ -0,0 +1,254 @@ +import adsk.core +import adsk.fusion + +from src.Logging import logFailure +from src.Parser.ExporterOptions import ExporterOptions +from src.Types import KG, ExportLocation, ExportMode, UnitSystem +from src.UI.CreateCommandInputsHelper import createBooleanInput +from src.UI.GamepieceConfigTab import GamepieceConfigTab +from src.UI.JointConfigTab import JointConfigTab +from src.Util import ( + convertMassUnitsFrom, + convertMassUnitsTo, + designMassCalculation, + getFusionUnitSystem, +) + + +class GeneralConfigTab: + generalOptionsTab: adsk.core.TabCommandInput + previousAutoCalcWeightCheckboxState: bool + previousFrictionOverrideCheckboxState: bool + previousSelectedUnitDropdownIndex: int + previousSelectedModeDropdownIndex: int + jointConfigTab: JointConfigTab + gamepieceConfigTab: GamepieceConfigTab + + @logFailure + def __init__(self, args: adsk.core.CommandCreatedEventArgs, exporterOptions: ExporterOptions) -> None: + inputs = args.command.commandInputs + self.generalOptionsTab = inputs.addTabCommandInput("generalSettings", "General Settings") + self.generalOptionsTab.tooltip = "General configuration options for your robot export." + generalTabInputs = self.generalOptionsTab.children + + dropdownExportMode = generalTabInputs.addDropDownCommandInput( + "exportModeDropdown", + "Export Mode", + dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, + ) + + dynamic = exporterOptions.exportMode == ExportMode.ROBOT + dropdownExportMode.listItems.add("Dynamic", dynamic) + dropdownExportMode.listItems.add("Static", not dynamic) + dropdownExportMode.tooltip = "Export Mode" + dropdownExportMode.tooltipDescription = "
Does this object move dynamically?" + self.previousSelectedModeDropdownIndex = int(not dynamic) + + dropdownExportLocation = generalTabInputs.addDropDownCommandInput( + "exportLocation", "Export Location", dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle + ) + + upload = exporterOptions.exportLocation == ExportLocation.UPLOAD + dropdownExportLocation.listItems.add("Upload", upload) + dropdownExportLocation.listItems.add("Download", not upload) + dropdownExportLocation.tooltip = "Export Location" + dropdownExportLocation.tooltipDescription = ( + "
Do you want to upload this mirabuf file to APS, or download it to your local machine?" + ) + + autoCalcWeightButton = createBooleanInput( + "autoCalcWeightButton", + "Auto Calculate Robot Weight", + generalTabInputs, + checked=exporterOptions.autoCalcRobotWeight, + tooltip="Approximate the weight of your robot assembly.", + ) + self.previousAutoCalcWeightCheckboxState = exporterOptions.autoCalcRobotWeight + + displayWeight = convertMassUnitsFrom(exporterOptions.robotWeight) + + fusUnitSystem = getFusionUnitSystem() + weightInput = generalTabInputs.addValueInput( + "weightInput", + f"Weight {'(lbs)' if fusUnitSystem is UnitSystem.IMPERIAL else '(kg)'}", + "", + adsk.core.ValueInput.createByReal(displayWeight), + ) + + createBooleanInput( + "compressOutputButton", + "Compress Output", + generalTabInputs, + checked=exporterOptions.compressOutput, + tooltip="Compress the output file for a smaller file size.", + tooltipadvanced="
Use the GZIP compression system to compress the resulting file, " + "perfect if you want to share your robot design around.
", + enabled=True, + ) + + exportAsPartButton = createBooleanInput( + "exportAsPartButton", + "Export As Part", + generalTabInputs, + checked=exporterOptions.exportAsPart, + tooltip="Use to export as a part for Mix And Match", + enabled=True, + ) + + frictionOverrideButton = createBooleanInput( + "frictionOverride", + "Friction Override", + generalTabInputs, + checked=exporterOptions.frictionOverride, + tooltip="Manually override the default friction values on the bodies in the assembly.", + ) + self.previousFrictionOverrideCheckboxState = exporterOptions.frictionOverride + + valueList = [1] + [i / 20 for i in range(20)] + frictionCoefficient = generalTabInputs.addFloatSliderListCommandInput("frictionCoefficient", "", "", valueList) + frictionCoefficient.valueOne = exporterOptions.frictionOverrideCoeff + frictionCoefficient.tooltip = "Friction coefficients range from 0 (ice) to 1 (rubber)." + frictionCoefficient.isVisible = exporterOptions.frictionOverride + + createBooleanInput( + "openSynthesisOnExportButton", + "Open Synthesis on Export", + generalTabInputs, + checked=exporterOptions.openSynthesisUponExport, + tooltip="Launch the Synthesis website upon export.", + ) + + if exporterOptions.exportMode == ExportMode.FIELD: + autoCalcWeightButton.isVisible = False + exportAsPartButton.isVisible = False + weightInput.isVisible = False + frictionOverrideButton.isVisible = frictionCoefficient.isVisible = False + + @property + def isActive(self) -> bool: + return self.generalOptionsTab.isActive or False + + @property + def exportMode(self) -> ExportMode: + exportModeDropdown: adsk.core.DropDownCommandInput = self.generalOptionsTab.children.itemById( + "exportModeDropdown" + ) + if exportModeDropdown.selectedItem.index == 0: + return ExportMode.ROBOT + else: + assert exportModeDropdown.selectedItem.index == 1 + return ExportMode.FIELD + + @property + def compress(self) -> bool: + compressButton: adsk.core.BoolValueCommandInput = self.generalOptionsTab.children.itemById( + "compressOutputButton" + ) + return compressButton.value or False + + @property + def exportAsPart(self) -> bool: + exportAsPartButton: adsk.core.BoolValueCommandInput = self.generalOptionsTab.children.itemById( + "exportAsPartButton" + ) + return exportAsPartButton.value or False + + @property + def robotWeight(self) -> KG: + weightInput: adsk.core.ValueCommandInput = self.generalOptionsTab.children.itemById("weightInput") + return convertMassUnitsTo(weightInput.value) + + @property + def autoCalculateWeight(self) -> bool: + autoCalcWeightButton: adsk.core.BoolValueCommandInput = self.generalOptionsTab.children.itemById( + "autoCalcWeightButton" + ) + return autoCalcWeightButton.value or False + + @property + def exportLocation(self) -> ExportLocation: + exportLocationDropdown: adsk.core.DropDownCommandInput = self.generalOptionsTab.children.itemById( + "exportLocation" + ) + if exportLocationDropdown.selectedItem.index == 0: + return ExportLocation.UPLOAD + else: + assert exportLocationDropdown.selectedItem.index == 1 + return ExportLocation.DOWNLOAD + + @property + def overrideFriction(self) -> bool: + overrideFrictionButton: adsk.core.BoolValueCommandInput = self.generalOptionsTab.children.itemById( + "frictionOverride" + ) + return overrideFrictionButton.value or False + + @property + def frictionOverrideCoeff(self) -> float: + frictionSlider: adsk.core.FloatSliderCommandInput = self.generalOptionsTab.children.itemById( + "frictionCoefficient" + ) + return frictionSlider.valueOne or -1.0 + + @property + def openSynthesisUponExport(self) -> bool: + openSynthesisButton: adsk.core.BoolValueCommandInput = self.generalOptionsTab.children.itemById( + "openSynthesisOnExportButton" + ) + return openSynthesisButton.value or False + + @logFailure + def handleInputChanged(self, args: adsk.core.InputChangedEventArgs) -> None: + autoCalcWeightButton: adsk.core.BoolValueCommandInput = args.inputs.itemById("autoCalcWeightButton") + weightInput: adsk.core.ValueCommandInput = args.inputs.itemById("weightInput") + exportAsPartButton: adsk.core.BoolValueCommandInput = args.inputs.itemById("exportAsPartButton") + overrideFrictionButton: adsk.core.BoolValueCommandInput = args.inputs.itemById("frictionOverride") + frictionSlider: adsk.core.FloatSliderCommandInput = args.inputs.itemById("frictionCoefficient") + commandInput = args.input + if commandInput.id == "exportModeDropdown": + modeDropdown = adsk.core.DropDownCommandInput.cast(commandInput) + + if modeDropdown.selectedItem.index == self.previousSelectedModeDropdownIndex: + return + + if modeDropdown.selectedItem.index == 0: + self.jointConfigTab.isVisible = True + self.gamepieceConfigTab.isVisible = False + + autoCalcWeightButton.isVisible = True + weightInput.isVisible = True + exportAsPartButton.isVisible = True + overrideFrictionButton.isVisible = True + frictionSlider.isVisible = overrideFrictionButton.value + else: + assert modeDropdown.selectedItem.index == 1 + self.jointConfigTab.isVisible = False + self.gamepieceConfigTab.isVisible = True + + autoCalcWeightButton.isVisible = False + weightInput.isVisible = False + exportAsPartButton.isVisible = False + overrideFrictionButton.isVisible = frictionSlider.isVisible = False + + self.previousSelectedModeDropdownIndex = modeDropdown.selectedItem.index + + elif commandInput.id == "autoCalcWeightButton": + autoCalcWeightButton = adsk.core.BoolValueCommandInput.cast(commandInput) + if autoCalcWeightButton.value == self.previousAutoCalcWeightCheckboxState: + return + + if autoCalcWeightButton.value: + weightInput.value = designMassCalculation() + weightInput.isEnabled = False + else: + weightInput.isEnabled = True + + self.previousAutoCalcWeightCheckboxState = autoCalcWeightButton.value + + elif commandInput.id == "frictionOverride": + frictionOverrideButton = adsk.core.BoolValueCommandInput.cast(commandInput) + if frictionOverrideButton.value == self.previousFrictionOverrideCheckboxState: + return + + frictionSlider.isVisible = frictionOverrideButton.value + self.previousFrictionOverrideCheckboxState = frictionOverrideButton.value diff --git a/exporter/SynthesisFusionAddin/src/UI/HUI.py b/exporter/SynthesisFusionAddin/src/UI/HUI.py index 3b52de9999..d5be4473f7 100644 --- a/exporter/SynthesisFusionAddin/src/UI/HUI.py +++ b/exporter/SynthesisFusionAddin/src/UI/HUI.py @@ -1,12 +1,16 @@ -from ..general_imports import * -from ..Logging import logFailure -from . import Handlers, OsHelper +from typing import Any, Callable + +import adsk.core + +from src import INTERNAL_ID, gm +from src.Logging import logFailure +from src.UI import Handlers, OsHelper # no longer used class HPalette: - handlers = [] - events = [] + handlers: list[Any] = [] + events: list[adsk.core.Event] = [] def __init__( self, @@ -17,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: @@ -67,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 @@ -84,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 @@ -92,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: @@ -168,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. @@ -184,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. @@ -193,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 710f61e8c8..0e60ba36f7 100644 --- a/exporter/SynthesisFusionAddin/src/UI/Handlers.py +++ b/exporter/SynthesisFusionAddin/src/UI/Handlers.py @@ -1,4 +1,6 @@ -from ..general_imports import * +from typing import Any + +import adsk.core class HButtonCommandCreatedEvent(adsk.core.CommandCreatedEventHandler): @@ -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 7c8e3a5930..9253af0600 100644 --- a/exporter/SynthesisFusionAddin/src/UI/Helper.py +++ b/exporter/SynthesisFusionAddin/src/UI/Helper.py @@ -1,53 +1,54 @@ from inspect import getmembers, isfunction -from typing import Union -from ..general_imports import * -from . import HUI, Events +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: @@ -69,5 +70,3 @@ def openPanel() -> None: func_list = [o for o in getmembers(Events, isfunction)] palette_new = HUI.HPalette(name, APP_TITLE, True, True, False, 400, 500, func_list) gm.elements.append(palette_new) - - return diff --git a/exporter/SynthesisFusionAddin/src/UI/IconPaths.py b/exporter/SynthesisFusionAddin/src/UI/IconPaths.py index 261720494c..8b377eb659 100644 --- a/exporter/SynthesisFusionAddin/src/UI/IconPaths.py +++ b/exporter/SynthesisFusionAddin/src/UI/IconPaths.py @@ -1,6 +1,6 @@ import os -from . import OsHelper +from src.UI import OsHelper """ Dictionaries that store all the icon paths in ConfigCommand. All path strings are OS-independent @@ -32,11 +32,6 @@ "remove": resources + os.path.join("MousePreselectIcons", "mouse-remove-icon.png"), } -massIcons = { - "KG": resources + os.path.join("kg_icon"), # resource folder - "LBS": resources + os.path.join("lbs_icon"), # resource folder -} - signalIcons = { "PWM": resources + os.path.join("PWM_icon"), # resource folder "CAN": resources + os.path.join("CAN_icon"), # resource folder diff --git a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py index 4f25ebfe01..ee8dfb2f0a 100644 --- a/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py +++ b/exporter/SynthesisFusionAddin/src/UI/JointConfigTab.py @@ -1,19 +1,17 @@ -import logging -import traceback - import adsk.core import adsk.fusion -from ..general_imports import INTERNAL_ID -from ..Logging import logFailure -from ..Types import Joint, JointParentType, SignalType, Wheel, WheelType -from . import IconPaths -from .CreateCommandInputsHelper import ( +from src.Logging import logFailure +from src.Types import Joint, JointParentType, SignalType, Wheel, WheelType +from src.UI import IconPaths +from src.UI.CreateCommandInputsHelper import ( createBooleanInput, createTableInput, createTextBoxInput, ) +from ..Parser.SynthesisParser.Joints import AcceptedJointTypes + class JointConfigTab: selectedJointList: list[adsk.fusion.Joint] = [] @@ -106,6 +104,18 @@ def __init__(self, args: adsk.core.CommandCreatedEventArgs) -> None: self.reset() + @property + def isVisible(self) -> bool: + 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: @@ -224,6 +234,16 @@ def addJoint(self, fusionJoint: adsk.fusion.Joint, synJoint: Joint | None = None jointSpeed.tooltip = "Meters per second" self.jointConfigTable.addCommandInput(jointSpeed, row, 4) + else: + jointSpeed = commandInputs.addValueInput( + "jointSpeed", + "Speed", + "m", + adsk.core.ValueInput.createByReal(0), + ) + jointSpeed.tooltip = "Unavailable" + self.jointConfigTable.addCommandInput(jointSpeed, row, 4) + if synJoint: jointForceValue = synJoint.force * 100 # Currently a factor of 100 - Should be investigated else: @@ -260,6 +280,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: @@ -297,6 +318,9 @@ def addWheel(self, joint: adsk.fusion.Joint, wheel: Wheel | None = None) -> None signalType.tooltip = "Wheel signal type is linked with the respective joint signal type." i = self.selectedJointList.index(joint) jointSignalType = SignalType(self.jointConfigTable.getInputAtPosition(i + 1, 3).selectedItem.index + 1) + + # Invisible white space characters are required in the list item name field to make this work. + # I have no idea why, Fusion API needs some special education help - Brandon signalType.listItems.add("‎", jointSignalType is SignalType.PWM, IconPaths.signalIcons["PWM"]) signalType.listItems.add("‎", jointSignalType is SignalType.CAN, IconPaths.signalIcons["CAN"]) signalType.listItems.add("‎", jointSignalType is SignalType.PASSIVE, IconPaths.signalIcons["PASSIVE"]) @@ -396,6 +420,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] @@ -436,22 +468,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: @@ -461,12 +483,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 @@ -475,7 +491,7 @@ def handleInputChanged( def handleSelectionEvent(self, args: adsk.core.SelectionEventArgs, selectedJoint: adsk.fusion.Joint) -> None: selectionInput = args.activeInput jointType = selectedJoint.jointMotion.jointType - if jointType == adsk.fusion.JointTypes.RevoluteJointType or jointType == adsk.fusion.JointTypes.SliderJointType: + if jointType in AcceptedJointTypes: if not self.addJoint(selectedJoint): ui = adsk.core.Application.get().userInterface result = ui.messageBox( @@ -498,9 +514,7 @@ def handlePreviewEvent(self, args: adsk.core.CommandEventArgs) -> None: jointSelectCancelButton: adsk.core.BoolValueCommandInput = commandInputs.itemById("jointSelectCancelButton") jointSelection: adsk.core.SelectionCommandInput = commandInputs.itemById("jointSelection") - if self.jointConfigTable.rowCount <= 1: - jointRemoveButton.isEnabled = False - + jointRemoveButton.isEnabled = self.jointConfigTable.rowCount > 1 if not jointSelection.isEnabled: - jointAddButton.isEnabled = jointRemoveButton.isEnabled = True + jointAddButton.isEnabled = True jointSelectCancelButton.isVisible = jointSelectCancelButton.isEnabled = False diff --git a/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py b/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py index 30c9f078e6..30cb8831d7 100644 --- a/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py +++ b/exporter/SynthesisFusionAddin/src/UI/MarkingMenu.py @@ -1,30 +1,28 @@ -import logging.handlers -import traceback +from typing import Callable import adsk.core import adsk.fusion -from ..Logging import logFailure +from src.Logging import getLogger, logFailure # Ripped all the boiler plate from the example code: https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-c90ce6a2-c282-11e6-a365-3417ebc87622 # 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") @@ -50,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) @@ -71,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: @@ -129,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() @@ -202,11 +191,12 @@ 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() else: - ui.messageBox(str(obj) + " is not a valid object") + logger.warn(f"{str(obj)} is not a valid object") + cmdDefs.clear() handlers.clear() 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 bd45cfc062..abf6f7df32 100644 --- a/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py +++ b/exporter/SynthesisFusionAddin/src/UI/ShowAPSAuthCommand.py @@ -1,15 +1,14 @@ import json -import os import time import traceback import urllib.parse import urllib.request -import webbrowser +from typing import Any import adsk.core +from src import gm from src.APS.APS import CLIENT_ID, auth_path, convertAuthToken, getCodeChallenge -from src.general_imports import APP_NAME, DESCRIPTION, INTERNAL_ID, gm, my_addin_path from src.Logging import getLogger logger = getLogger() @@ -17,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") @@ -62,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() @@ -79,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() @@ -104,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() @@ -120,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 new file mode 100644 index 0000000000..7441a071b4 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/UI/ShowWebsiteCommand.py @@ -0,0 +1,31 @@ +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__() + + @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: Any) -> None: + super().__init__() + + @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/TableUtilities.py b/exporter/SynthesisFusionAddin/src/UI/TableUtilities.py deleted file mode 100644 index 9e274e7148..0000000000 --- a/exporter/SynthesisFusionAddin/src/UI/TableUtilities.py +++ /dev/null @@ -1,374 +0,0 @@ -''' -from curses.textpad import Textbox -import adsk.fusion, adsk.core, traceback -from ..general_imports import * -from . import IconPaths, OsHelper -from . ConfigCommand import * - -def addWheelToTable(wheel: adsk.fusion.Joint) -> None: - """### Adds a wheel joint to its global list and wheel table. - - Args: - wheel (adsk.fusion.Joint): wheel Joint object to be added. - """ - try: - onSelect = gm.handlers[3] - wheelTableInput = INPUTS_ROOT.itemById("wheel_table") - - # def addPreselections(child_occurrences): - # for occ in child_occurrences: - # onSelect.allWheelPreselections.append(occ.entityToken) - - # if occ.childOccurrences: - # addPreselections(occ.childOccurrences) - - # if wheel.childOccurrences: - # addPreselections(wheel.childOccurrences) - # else: - # onSelect.allWheelPreselections.append(wheel.entityToken) - onSelect.allWheelPreselections.append(wheel.entityToken) - - WheelListGlobal.append(wheel) - cmdInputs = adsk.core.CommandInputs.cast(wheelTableInput.commandInputs) - - icon = cmdInputs.addImageCommandInput( - "placeholder_w", "Placeholder", IconPaths.wheelIcons["standard"] - ) - - name = cmdInputs.addTextBoxCommandInput( - "name_w", "Joint name", wheel.name, 1, True - ) - name.tooltip = wheel.name - - wheelType = cmdInputs.addDropDownCommandInput( - "wheel_type_w", - "Wheel Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - wheelType.listItems.add("Standard", True, "") - wheelType.listItems.add("Omni", False, "") - wheelType.listItems.add("Mecanum", False, "") - wheelType.tooltip = "Wheel type" - wheelType.tooltipDescription = "
Omni-directional wheels can be used just like regular drive wheels but they have the advantage of being able to roll freely perpendicular to the drive direction.
" - wheelType.toolClipFilename = OsHelper.getOSPath(".", "src", "Resources") + os.path.join("WheelIcons", "omni-wheel-preview.png") - - signalType = cmdInputs.addDropDownCommandInput( - "signal_type_w", - "Signal Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - signalType.isFullWidth = True - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) - signalType.tooltip = "Signal type" - - row = wheelTableInput.rowCount - - wheelTableInput.addCommandInput(icon, row, 0) - wheelTableInput.addCommandInput(name, row, 1) - wheelTableInput.addCommandInput(wheelType, row, 2) - wheelTableInput.addCommandInput(signalType, row, 3) - - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addWheelToTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - -def addJointToTable(joint: adsk.fusion.Joint) -> None: - """### Adds a Joint object to its global list and joint table. - - Args: - joint (adsk.fusion.Joint): Joint object to be added - """ - try: - JointListGlobal.append(joint) - jointTableInput = INPUTS_ROOT.itemById("joint_table") - cmdInputs = adsk.core.CommandInputs.cast(jointTableInput.commandInputs) - - # joint type icons - if joint.jointMotion.jointType == adsk.fusion.JointTypes.RigidJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Rigid", IconPaths.jointIcons["rigid"] - ) - icon.tooltip = "Rigid joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.RevoluteJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Revolute", IconPaths.jointIcons["revolute"] - ) - icon.tooltip = "Revolute joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.SliderJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Slider", IconPaths.jointIcons["slider"] - ) - icon.tooltip = "Slider joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PlanarJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Planar", IconPaths.jointIcons["planar"] - ) - icon.tooltip = "Planar joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.PinSlotJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Pin Slot", IconPaths.jointIcons["pin_slot"] - ) - icon.tooltip = "Pin slot joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.CylindricalJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Cylindrical", IconPaths.jointIcons["cylindrical"] - ) - icon.tooltip = "Cylindrical joint" - - elif joint.jointMotion.jointType == adsk.fusion.JointTypes.BallJointType: - icon = cmdInputs.addImageCommandInput( - "placeholder", "Ball", IconPaths.jointIcons["ball"] - ) - icon.tooltip = "Ball joint" - - # joint name - name = cmdInputs.addTextBoxCommandInput( - "name_j", "Occurrence name", "", 1, True - ) - name.tooltip = joint.name - name.formattedText = "

{}

".format(joint.name) - - jointType = cmdInputs.addDropDownCommandInput( - "joint_parent", - "Joint Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - jointType.isFullWidth = True - jointType.listItems.add("Root", True) - - # after each additional joint added, add joint to the dropdown of all preview rows/joints - for row in range(jointTableInput.rowCount): - if row != 0: - dropDown = jointTableInput.getInputAtPosition(row, 2) - dropDown.listItems.add(JointListGlobal[-1].name, False) - - # add all parent joint options to added joint dropdown - for j in range(len(JointListGlobal) - 1): - jointType.listItems.add(JointListGlobal[j].name, False) - - jointType.tooltip = "Possible parent joints" - jointType.tooltipDescription = "
The root component is usually the parent." - - signalType = cmdInputs.addDropDownCommandInput( - "signal_type", - "Signal Type", - dropDownStyle=adsk.core.DropDownStyles.LabeledIconDropDownStyle, - ) - signalType.listItems.add("‎", True, IconPaths.signalIcons["PWM"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["CAN"]) - signalType.listItems.add("‎", False, IconPaths.signalIcons["PASSIVE"]) - signalType.tooltip = "Signal type" - - defaultMotorSpeed = adsk.core.ValueInput() - defaultMotorSpeed.realValue = 90 - testMotorSpeed = cmdInputs.addTextBoxCommandInput( - "test_j", "Omfg", "", 1, True - ) - testMotorSpeed.formattedText = 'j' - - row = jointTableInput.rowCount - - jointTableInput.addCommandInput(icon, row, 0) - jointTableInput.addCommandInput(name, row, 1) - jointTableInput.addCommandInput(jointType, row, 2) - jointTableInput.addCommandInput(signalType, row, 3) - jointTableInput.addCommandInput(testMotorSpeed, row, 4) - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addJointToTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - -def addGamepieceToTable(gamepiece: adsk.fusion.Occurrence) -> None: - """### Adds a gamepiece occurrence to its global list and gamepiece table. - - Args: - gamepiece (adsk.fusion.Occurrence): Gamepiece occurrence to be added - """ - try: - onSelect = gm.handlers[3] - gamepieceTableInput = INPUTS_ROOT.itemById("gamepiece_table") - def addPreselections(child_occurrences): - for occ in child_occurrences: - onSelect.allGamepiecePreselections.append(occ.entityToken) - - if occ.childOccurrences: - addPreselections(occ.childOccurrences) - - if gamepiece.childOccurrences: - addPreselections(gamepiece.childOccurrences) - else: - onSelect.allGamepiecePreselections.append(gamepiece.entityToken) - - GamepieceListGlobal.append(gamepiece) - cmdInputs = adsk.core.CommandInputs.cast(gamepieceTableInput.commandInputs) - blankIcon = cmdInputs.addImageCommandInput( - "blank_gp", "Blank", IconPaths.gamepieceIcons["blank"] - ) - - type = cmdInputs.addTextBoxCommandInput( - "name_gp", "Occurrence name", gamepiece.name, 1, True - ) - - value = 0.0 - physical = gamepiece.component.getPhysicalProperties( - adsk.fusion.CalculationAccuracy.LowCalculationAccuracy - ) - value = physical.mass - - # check if dropdown unit is kg or lbs. bool value taken from ConfigureCommandInputChanged - massUnitInString = "" - onInputChanged = gm.handlers[1] - if onInputChanged.isLbs_f: - value = round( - value * 2.2046226218, 2 # lbs - ) - massUnitInString = "(in pounds)" - else: - value = round( - value, 2 # kg - ) - massUnitInString = "(in kilograms)" - - weight = cmdInputs.addValueInput( - "weight_gp", "Weight Input", "", adsk.core.ValueInput.createByString(str(value)) - ) - - valueList = [1] - for i in range(20): - valueList.append(i / 20) - - friction_coeff = cmdInputs.addFloatSliderListCommandInput( - "friction_coeff", "", "", valueList - ) - friction_coeff.valueOne = 0.5 - - type.tooltip = gamepiece.name - - weight.tooltip = "Weight of field element" - weight.tooltipDescription = massUnitInString - - friction_coeff.tooltip = "Friction coefficient of field element" - friction_coeff.tooltipDescription = ( - "Friction coefficients range from 0 (ice) to 1 (rubber)." - ) - row = gamepieceTableInput.rowCount - - gamepieceTableInput.addCommandInput(blankIcon, row, 0) - gamepieceTableInput.addCommandInput(type, row, 1) - gamepieceTableInput.addCommandInput(weight, row, 2) - gamepieceTableInput.addCommandInput(friction_coeff, row, 3) - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.addGamepieceToTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - -def removeWheelFromTable(index: int) -> None: - """### Removes a wheel occurrence from its global list and wheel table. - - Args: - index (int): index of wheel item in its global list - """ - try: - onSelect = gm.handlers[3] - wheelTableInput = INPUTS_ROOT.itemById("wheel_table") - wheel = WheelListGlobal[index] - - def removePreselections(child_occurrences): - for occ in child_occurrences: - onSelect.allWheelPreselections.remove(occ.entityToken) - - if occ.childOccurrences: - removePreselections(occ.childOccurrences) - - if wheel.childOccurrences: - removePreselections(wheel.childOccurrences) - else: - onSelect.allWheelPreselections.remove(wheel.entityToken) - - del WheelListGlobal[index] - wheelTableInput.deleteRow(index + 1) - - #updateJointTable(wheel) - except IndexError: - pass - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeWheelFromTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - -def removeJointFromTable(joint: adsk.fusion.Joint) -> None: - """### Removes a joint occurrence from its global list and joint table. - - Args: - joint (adsk.fusion.Joint): Joint object to be removed - """ - try: - index = JointListGlobal.index(joint) - jointTableInput = INPUTS_ROOT.itemById("joint_table") - JointListGlobal.remove(joint) - - jointTableInput.deleteRow(index + 1) - - for row in range(jointTableInput.rowCount): - if row == 0: - continue - - dropDown = jointTableInput.getInputAtPosition(row, 2) - listItems = dropDown.listItems - - if row > index: - if listItems.item(index + 1).isSelected: - listItems.item(index).isSelected = True - listItems.item(index + 1).deleteMe() - else: - listItems.item(index + 1).deleteMe() - else: - if listItems.item(index).isSelected: - listItems.item(index - 1).isSelected = True - listItems.item(index).deleteMe() - else: - listItems.item(index).deleteMe() - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeJointFromTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) - -def removeGamePieceFromTable(index: int) -> None: - """### Removes a gamepiece occurrence from its global list and gamepiece table. - - Args: - index (int): index of gamepiece item in its global list. - """ - onSelect = gm.handlers[3] - gamepieceTableInput = INPUTS_ROOT.itemById("gamepiece_table") - gamepiece = GamepieceListGlobal[index] - - def removePreselections(child_occurrences): - for occ in child_occurrences: - onSelect.allGamepiecePreselections.remove(occ.entityToken) - - if occ.childOccurrences: - removePreselections(occ.childOccurrences) - try: - if gamepiece.childOccurrences: - removePreselections(GamepieceListGlobal[index].childOccurrences) - else: - onSelect.allGamepiecePreselections.remove(gamepiece.entityToken) - - del GamepieceListGlobal[index] - gamepieceTableInput.deleteRow(index + 1) - except IndexError: - pass - except: - logging.getLogger("{INTERNAL_ID}.UI.ConfigCommand.removeGamePieceFromTable()").error( - "Failed:\n{}".format(traceback.format_exc()) - ) -''' diff --git a/exporter/SynthesisFusionAddin/src/UI/Toolbar.py b/exporter/SynthesisFusionAddin/src/UI/Toolbar.py index bfcc34189a..93e6173502 100644 --- a/exporter/SynthesisFusionAddin/src/UI/Toolbar.py +++ b/exporter/SynthesisFusionAddin/src/UI/Toolbar.py @@ -1,6 +1,7 @@ -from ..general_imports import * -from ..Logging import logFailure -from ..strings import INTERNAL_ID +import adsk.core + +from src import INTERNAL_ID, gm +from src.Logging import logFailure class Toolbar: @@ -9,27 +10,23 @@ class Toolbar: - holds handlers """ - uid = None - tab = None - panels = [] - controls = [] + uid: str + tab: adsk.core.ToolbarTab + panels: list[str] = [] + controls: list[str] = [] @logFailure def __init__(self, name: str): self.uid = f"{name}_{INTERNAL_ID}_toolbar" self.name = name - designWorkspace = gm.ui.workspaces.itemById("FusionSolidEnvironment") - - if designWorkspace: - allDesignTabs = designWorkspace.toolbarTabs - - self.tab = allDesignTabs.itemById(self.uid) - - if self.tab is None: - self.tab = allDesignTabs.add(self.uid, name) + designWorkspace = gm.ui.workspaces.itemById("FusionSolidEnvironment") or adsk.core.Workspace() + allDesignTabs = designWorkspace.toolbarTabs + self.tab = allDesignTabs.itemById(self.uid) + if self.tab is None: + self.tab = allDesignTabs.add(self.uid, name) - self.tab.activate() + self.tab.activate() def getPanel(self, name: str, visibility: bool = True) -> str | None: """# Gets a control for a panel to the tabbed toolbar diff --git a/exporter/SynthesisFusionAddin/src/Util.py b/exporter/SynthesisFusionAddin/src/Util.py new file mode 100644 index 0000000000..c5f91ef635 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/Util.py @@ -0,0 +1,48 @@ +import os + +import adsk.core +import adsk.fusion + +from src.Types import FUSION_UNIT_SYSTEM, KG, LBS, UnitSystem + + +def getFusionUnitSystem() -> UnitSystem: + fusDesign = adsk.fusion.Design.cast(adsk.core.Application.get().activeProduct) + return FUSION_UNIT_SYSTEM.get(fusDesign.fusionUnitsManager.distanceDisplayUnits, UnitSystem.METRIC) + + +def convertMassUnitsFrom(input: KG | LBS) -> KG | LBS: + """Converts stored Synthesis mass units into user selected Fusion units.""" + unitManager = adsk.fusion.Design.cast(adsk.core.Application.get().activeProduct).fusionUnitsManager + toString = "kg" if getFusionUnitSystem() is UnitSystem.METRIC else "lbmass" + return unitManager.convert(input, "kg", toString) or 0.0 + + +def convertMassUnitsTo(input: KG | LBS) -> KG | LBS: + """Converts user selected Fusion mass units into Synthesis units.""" + unitManager = adsk.fusion.Design.cast(adsk.core.Application.get().activeProduct).fusionUnitsManager + fromString = "kg" if getFusionUnitSystem() is UnitSystem.METRIC else "lbmass" + return unitManager.convert(input, fromString, "kg") or 0.0 + + +def designMassCalculation() -> KG | LBS: + """Calculates and returns the total mass of the active design in Fusion units.""" + app = adsk.core.Application.get() + mass = 0.0 + for body in [x for x in app.activeDocument.design.rootComponent.bRepBodies if x.isLightBulbOn]: + physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) + mass += physical.mass + + for occ in [x for x in app.activeDocument.design.rootComponent.allOccurrences if x.isLightBulbOn]: + for body in [x for x in occ.component.bRepBodies if x.isLightBulbOn]: + physical = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.LowCalculationAccuracy) + mass += physical.mass + + # Internally, Fusion always uses metric units, same as Synthesis + return round(convertMassUnitsFrom(mass), 2) + + +def makeDirectories(directory: str) -> str: + """Ensures than an input directory exists and attempts to create it if it doesn't.""" + os.makedirs(directory, exist_ok=True) + return directory diff --git a/exporter/SynthesisFusionAddin/src/__init__.py b/exporter/SynthesisFusionAddin/src/__init__.py new file mode 100644 index 0000000000..42d1e6d391 --- /dev/null +++ b/exporter/SynthesisFusionAddin/src/__init__.py @@ -0,0 +1,35 @@ +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" +APP_WEBSITE_URL = "https://synthesis.autodesk.com/fission/" +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() +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", + "IS_RELEASE", + "SYSTEM", + "SUPPORT_PATH", + "gm", +] diff --git a/exporter/SynthesisFusionAddin/src/general_imports.py b/exporter/SynthesisFusionAddin/src/general_imports.py deleted file mode 100644 index 67d5e3b3cc..0000000000 --- a/exporter/SynthesisFusionAddin/src/general_imports.py +++ /dev/null @@ -1,47 +0,0 @@ -import json -import os -import pathlib -import sys -import traceback -import uuid -from datetime import datetime -from time import time -from types import FunctionType - -import adsk.core -import adsk.fusion - -from .GlobalManager import * -from .Logging import getLogger -from .strings import * - -logger = getLogger() - -# hard coded to bypass errors for now -PROTOBUF = True -DEBUG = True - -try: - path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) - - path_proto_files = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "proto", "proto_out")) - - if not path in sys.path: - sys.path.insert(1, path) - - if not path_proto_files in sys.path: - sys.path.insert(2, path_proto_files) - - from proto import deps - - deps.installDependencies() - -except: - logger.error("Failed:\n{}".format(traceback.format_exc())) - -try: - # Setup the global state - gm: GlobalManager = GlobalManager() - my_addin_path = os.path.dirname(os.path.realpath(__file__)) -except: - logger.error("Failed:\n{}".format(traceback.format_exc())) diff --git a/exporter/SynthesisFusionAddin/src/strings.py b/exporter/SynthesisFusionAddin/src/strings.py deleted file mode 100644 index b5d79c055c..0000000000 --- a/exporter/SynthesisFusionAddin/src/strings.py +++ /dev/null @@ -1,4 +0,0 @@ -APP_NAME = "Synthesis" -APP_TITLE = "Synthesis Robot Exporter" -DESCRIPTION = "Exports files from Fusion into the Synthesis Format" -INTERNAL_ID = "Synthesis" diff --git a/exporter/SynthesisFusionAddin/tools/verifyIsortFormatting.py b/exporter/SynthesisFusionAddin/tools/verifyIsortFormatting.py index 0f9d899d5a..61772e05cb 100644 --- a/exporter/SynthesisFusionAddin/tools/verifyIsortFormatting.py +++ b/exporter/SynthesisFusionAddin/tools/verifyIsortFormatting.py @@ -18,9 +18,16 @@ def main() -> None: exitCode = 0 for i, (oldFileState, newFileState) in enumerate(zip(oldFileStates, newFileStates)): for j, (previousLine, newLine) in enumerate(zip(oldFileState, newFileState)): - if previousLine != newLine: - print(f"File {files[i]} is not formatted correctly!\nLine: {j + 1}") - exitCode = 1 + if previousLine == newLine: + continue + + print(f"File {files[i]} is not formatted correctly!\nLine: {j + 1}") + oldFileStateRange = range(max(0, j - 10), min(len(oldFileState), j + 11)) + print("\nOld file state:\n" + "\n".join(oldFileState[k].strip() for k in oldFileStateRange)) + newFileStateRange = range(max(0, j - 10), min(len(newFileState), j + 11)) + print("\nNew file state:\n" + "\n".join(newFileState[k].strip() for k in newFileStateRange)) + exitCode = 1 + break if not exitCode: print("All files are formatted correctly with isort!") 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 bf462bd999..b3a5d0fd1c 100644 --- a/fission/index.html +++ b/fission/index.html @@ -1,16 +1,26 @@ - - - - - - - - Fission | Synthesis - - -
- - + + + + + + + + + Fission | Synthesis + + +
+ + diff --git a/fission/package.json b/fission/package.json index 6e1ae08b2b..c8fb579c58 100644 --- a/fission/package.json +++ b/fission/package.json @@ -4,6 +4,8 @@ "version": "0.0.1", "type": "module", "scripts": { + "init": "(bun run assetpack && bun run playwright:install) || (npm run assetpack && npm run playwright:install)", + "host": "vite --open --host", "dev": "vite --open", "build": "tsc && vite build", "build:prod": "tsc && vite build --base=/fission/ --outDir dist/prod", @@ -14,7 +16,7 @@ "lint:fix": "eslint . --ext ts,tsx --report-unused-disable-directives --fix", "prettier": "bun x prettier src --check || npx prettier src --check", "prettier:fix": "bun x prettier src --write || npx prettier src --write", - "format": "(bun run prettier:fix && bun run lint:fix) || (npm run prettier:fix && npm run lint:fix)", + "format": "bun run prettier:fix && bun run lint:fix || npm run prettier:fix && npm run lint:fix", "assetpack": "curl -o public/assetpack.zip https://synthesis.autodesk.com/Downloadables/assetpack.zip && tar -xf public/assetpack.zip -C public/", "playwright:install": "bun x playwright install || npx playwright install" }, @@ -28,11 +30,12 @@ "@react-three/fiber": "^8.15.15", "@vitest/browser": "^1.6.0", "@vitest/coverage-v8": "^1.6.0", + "@xyflow/react": "^12.3.2", "async-mutex": "^0.5.0", "colord": "^2.9.3", "framer-motion": "^10.13.1", "lygia": "^1.1.3", - "playwright": "^1.45.0", + "playwright": "^1.46.0", "postprocessing": "^6.35.6", "react": "^18.2.0", "react-colorful": "^5.6.1", @@ -45,6 +48,9 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@mui/material": "^5.15.6", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", "@types/node": "^20.4.4", "@types/pako": "^2.0.3", "@types/react": "^18.2.47", @@ -52,6 +58,7 @@ "@types/three": "^0.160.0", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", + "@vitejs/plugin-basic-ssl": "^1.1.0", "@vitejs/plugin-react": "^4.0.3", "@vitejs/plugin-react-swc": "^3.5.0", "autoprefixer": "^10.4.14", @@ -59,6 +66,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^8.8.0", "eslint-import-resolver-alias": "^1.1.2", + "eslint-plugin-import": "^2.30.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "jsdom": "^24.1.0", @@ -67,12 +75,17 @@ "prettier": "3.3.2", "protobufjs": "^7.2.6", "protobufjs-cli": "^1.1.2", + "rollup": "^4.22.4", "tailwindcss": "^3.3.3", "tsconfig-paths": "^4.2.0", "typescript": "^5.4.5", - "vite": "^5.1.4", + "vite": "5.2.14", "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 1b3d308176..8738695b69 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -20,7 +20,6 @@ import UpdateAvailableModal from "@/modals/UpdateAvailableModal" import ViewModal from "@/modals/ViewModal" import ConnectToMultiplayerModal from "@/modals/aether/ConnectToMultiplayerModal" import ServerHostingModal from "@/modals/aether/ServerHostingModal" -import ChangeInputsModal from "@/ui/modals/configuring/ChangeInputsModal.tsx" import ChooseMultiplayerModeModal from "@/modals/configuring/ChooseMultiplayerModeModal" import ChooseSingleplayerModeModal from "@/modals/configuring/ChooseSingleplayerModeModal" import ConfigMotorModal from "@/modals/configuring/ConfigMotorModal" @@ -37,28 +36,19 @@ import ThemeEditorModal from "@/modals/configuring/theme-editor/ThemeEditorModal import MatchModeModal from "@/modals/spawning/MatchModeModal" import RobotSwitchPanel from "@/panels/RobotSwitchPanel" import SpawnLocationsPanel from "@/panels/SpawnLocationPanel" -import ConfigureGamepiecePickupPanel from "@/panels/configuring/ConfigureGamepiecePickupPanel" -import ConfigureShotTrajectoryPanel from "@/panels/configuring/ConfigureShotTrajectoryPanel" -import ScoringZonesPanel from "@/panels/configuring/scoring/ScoringZonesPanel" import ScoreboardPanel from "@/panels/information/ScoreboardPanel" import DriverStationPanel from "@/panels/simulation/DriverStationPanel" import PokerPanel from "@/panels/PokerPanel.tsx" -import ManageAssembliesModal from "@/modals/spawning/ManageAssembliesModal.tsx" import World from "@/systems/World.ts" -import { AddRobotsModal, AddFieldsModal, SpawningModal } from "@/modals/spawning/SpawningModals.tsx" import ImportLocalMirabufModal from "@/modals/mirabuf/ImportLocalMirabufModal.tsx" import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel.tsx" import Skybox from "./ui/components/Skybox.tsx" import ChooseInputSchemePanel from "./ui/panels/configuring/ChooseInputSchemePanel.tsx" import ProgressNotifications from "./ui/components/ProgressNotification.tsx" -import ConfigureRobotModal from "./ui/modals/configuring/ConfigureRobotModal.tsx" -import ResetAllInputsModal from "./ui/modals/configuring/ResetAllInputsModal.tsx" -import ZoneConfigPanel from "./ui/panels/configuring/scoring/ZoneConfigPanel.tsx" import SceneOverlay from "./ui/components/SceneOverlay.tsx" -import WPILibWSWorker from "@/systems/simulation/wpilib_brain/WPILibWSWorker.ts?worker" import WSViewPanel from "./ui/panels/WSViewPanel.tsx" -import Lazy from "./util/Lazy.ts" + import RCConfigPWMGroupModal from "@/modals/configuring/rio-config/RCConfigPWMGroupModal.tsx" import RCConfigCANGroupModal from "@/modals/configuring/rio-config/RCConfigCANGroupModal.tsx" import DebugPanel from "./ui/panels/DebugPanel.tsx" @@ -66,8 +56,15 @@ import NewInputSchemeModal from "./ui/modals/configuring/theme-editor/NewInputSc import AssignNewSchemeModal from "./ui/modals/configuring/theme-editor/AssignNewSchemeModal.tsx" import AnalyticsConsent from "./ui/components/AnalyticsConsent.tsx" import PreferencesSystem from "./systems/preferences/PreferencesSystem.ts" - -const worker = new Lazy(() => new WPILibWSWorker()) +import APSManagementModal from "./ui/modals/APSManagementModal.tsx" +import ConfigurePanel from "./ui/panels/configuring/assembly-config/ConfigurePanel.tsx" +import WiringPanel from "./ui/panels/simulation/WiringPanel.tsx" +import CameraSelectionPanel from "./ui/panels/configuring/CameraSelectionPanel.tsx" +import ContextMenu from "./ui/components/ContextMenu.tsx" +import GlobalUIComponent from "./ui/components/GlobalUIComponent.tsx" +import InitialConfigPanel from "./ui/panels/configuring/initial-config/InitialConfigPanel.tsx" +import WPILibConnectionStatus from "./ui/components/WPILibConnectionStatus.tsx" +import AutoTestPanel from "./ui/panels/simulation/AutoTestPanel.tsx" function Synthesis() { const { openModal, closeModal, getActiveModalElement } = useModalManager(initialModals) @@ -99,8 +96,6 @@ function Synthesis() { setConsentPopupDisable(false) } - worker.getValue() - let mainLoopHandle = 0 const mainLoop = () => { mainLoopHandle = requestAnimationFrame(mainLoop) @@ -168,8 +163,10 @@ function Synthesis() { closeAllPanels={closeAllPanels} > - + + + {panelElements.length > 0 && panelElements} {modalElement && ( @@ -179,6 +176,7 @@ function Synthesis() { )} + {!consentPopupDisable ? ( @@ -195,9 +193,6 @@ function Synthesis() { const initialModals = [ , - , - , - , , , , @@ -208,7 +203,6 @@ const initialModals = [ , , , - , , , , @@ -223,10 +217,8 @@ const initialModals = [ , , , - , , - , - , + , ] const initialPanels: ReactElement[] = [ @@ -234,25 +226,16 @@ const initialPanels: ReactElement[] = [ , , , - , - , - , - , , , , , , + , + , + , + , + , ] export default Synthesis diff --git a/fission/src/aps/APS.ts b/fission/src/aps/APS.ts index 4873d92136..ff6f1204ad 100644 --- a/fission/src/aps/APS.ts +++ b/fission/src/aps/APS.ts @@ -1,5 +1,5 @@ import World from "@/systems/World" -import { MainHUD_AddToast } from "@/ui/components/MainHUD" +import { Global_AddToast } from "@/ui/components/GlobalUIControls" import { Mutex } from "async-mutex" const APS_AUTH_KEY = "aps_auth" @@ -14,7 +14,7 @@ export const ENDPOINT_SYNTHESIS_CHALLENGE = `/api/aps/challenge` const ENDPOINT_AUTODESK_AUTHENTICATION_AUTHORIZE = "https://developer.api.autodesk.com/authentication/v2/authorize" const ENDPOINT_AUTODESK_AUTHENTICATION_TOKEN = "https://developer.api.autodesk.com/authentication/v2/token" -const ENDPOINT_AUTODESK_REVOKE_TOKEN = "https://developer.api.autodesk.com/authentication/v2/revoke" +const ENDPOINT_AUTODESK_AUTHENTICATION_REVOKE = "https://developer.api.autodesk.com/authentication/v2/revoke" const ENDPOINT_AUTODESK_USERINFO = "https://api.userprofile.autodesk.com/userinfo" export interface APSAuth { @@ -163,7 +163,7 @@ class APS { ["client_id", CLIENT_ID], ] as string[][]), } - const res = await fetch(ENDPOINT_AUTODESK_REVOKE_TOKEN, opts) + const res = await fetch(ENDPOINT_AUTODESK_AUTHENTICATION_REVOKE, opts) if (!res.ok) { console.log("Failed to revoke auth token:\n") return false @@ -205,7 +205,7 @@ class APS { } catch (e) { console.error(e) World.AnalyticsSystem?.Exception("APS Login Failure") - MainHUD_AddToast("error", "Error signing in.", "Please try again.") + Global_AddToast?.("error", "Error signing in.", "Please try again.") } }) } @@ -236,7 +236,7 @@ class APS { const json = await res.json() if (!res.ok) { if (shouldRelog) { - MainHUD_AddToast("warning", "Must Re-signin.", json.userMessage) + Global_AddToast?.("warning", "Must Re-signin.", json.userMessage) this.auth = undefined await this.requestAuthCode() return false @@ -249,13 +249,13 @@ class APS { if (this.auth) { await this.loadUserInfo(this.auth) if (APS.userInfo) { - MainHUD_AddToast("info", "ADSK Login", `Hello, ${APS.userInfo.givenName}`) + Global_AddToast?.("info", "ADSK Login", `Hello, ${APS.userInfo.givenName}`) } } return true } catch (e) { World.AnalyticsSystem?.Exception("APS Login Failure") - MainHUD_AddToast("error", "Error signing in.", "Please try again.") + Global_AddToast?.("error", "Error signing in.", "Please try again.") this.auth = undefined await this.requestAuthCode() return false @@ -281,7 +281,7 @@ class APS { const json = await res.json() if (!res.ok) { World.AnalyticsSystem?.Exception("APS Login Failure") - MainHUD_AddToast("error", "Error signing in.", json.userMessage) + Global_AddToast?.("error", "Error signing in.", json.userMessage) this.auth = undefined return } @@ -293,7 +293,7 @@ class APS { if (auth) { await this.loadUserInfo(auth) if (APS.userInfo) { - MainHUD_AddToast("info", "ADSK Login", `Hello, ${APS.userInfo.givenName}`) + Global_AddToast?.("info", "ADSK Login", `Hello, ${APS.userInfo.givenName}`) } } else { console.error("Couldn't get auth data.") @@ -306,7 +306,7 @@ class APS { if (retry_login) { this.auth = undefined World.AnalyticsSystem?.Exception("APS Login Failure") - MainHUD_AddToast("error", "Error signing in.", "Please try again.") + Global_AddToast?.("error", "Error signing in.", "Please try again.") } } @@ -327,7 +327,7 @@ class APS { const json = await res.json() if (!res.ok) { World.AnalyticsSystem?.Exception("APS Failure: User Info") - MainHUD_AddToast("error", "Error fetching user data.", json.userMessage) + Global_AddToast?.("error", "Error fetching user data.", json.userMessage) this.auth = undefined await this.requestAuthCode() return @@ -339,15 +339,11 @@ class APS { email: json.email, } - if (json.sub) { - World.AnalyticsSystem?.SetUserId(json.sub as string) - } - this.userInfo = info } catch (e) { console.error(e) World.AnalyticsSystem?.Exception("APS Login Failure: User Info") - MainHUD_AddToast("error", "Error signing in.", "Please try again.") + Global_AddToast?.("error", "Error signing in.", "Please try again.") this.auth = undefined } } @@ -363,7 +359,7 @@ class APS { } catch (e) { console.error(e) World.AnalyticsSystem?.Exception("APS Login Failure: Code Challenge") - MainHUD_AddToast("error", "Error signing in.", "Please try again.") + Global_AddToast?.("error", "Error signing in.", "Please try again.") } } } diff --git a/fission/src/aps/APSDataManagement.ts b/fission/src/aps/APSDataManagement.ts index 6deac410ea..52aa1fc21b 100644 --- a/fission/src/aps/APSDataManagement.ts +++ b/fission/src/aps/APSDataManagement.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { MainHUD_AddToast } from "@/ui/components/MainHUD" +import { Global_AddToast } from "@/ui/components/GlobalUIControls" import APS from "./APS" import TaskStatus from "@/util/TaskStatus" import { Mutex } from "async-mutex" @@ -140,9 +140,9 @@ export async function getHubs(): Promise { console.log(auth) console.log(APS.userInfo) if (e instanceof APSDataError) { - MainHUD_AddToast("error", e.title, e.detail) + Global_AddToast?.("error", e.title, e.detail) } else if (e instanceof Error) { - MainHUD_AddToast("error", "Failed to get hubs.", e.message) + Global_AddToast?.("error", "Failed to get hubs.", e.message) } return undefined } @@ -179,7 +179,7 @@ export async function getProjects(hub: Hub): Promise { } catch (e) { console.error("Failed to get hubs") if (e instanceof Error) { - MainHUD_AddToast("error", "Failed to get hubs.", e.message) + Global_AddToast?.("error", "Failed to get hubs.", e.message) } return undefined } @@ -224,7 +224,7 @@ export async function getFolderData(project: Project, folder: Folder): Promise void public constructor(parentAssembly: MirabufSceneObject) { super() - - console.debug("Trying to create intake sensor...") - this._parentAssembly = parentAssembly } @@ -47,16 +41,8 @@ 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 - if (InputSystem.getInput("intake", brainIndex)) { + if (this._parentAssembly.intakeActive) { if (this._joltBodyId && !World.PhysicsSystem.isPaused) { const body1 = event.message.body1 const body2 = event.message.body2 @@ -71,8 +57,6 @@ class IntakeSensorSceneObject extends SceneObject { } OnContactPersistedEvent.AddListener(this._collision) - - console.debug("Intake sensor created successfully!") } } @@ -88,25 +72,12 @@ class IntakeSensorSceneObject extends SceneObject { World.PhysicsSystem.SetBodyPosition(this._joltBodyId, ThreeVector3_JoltVec3(position)) World.PhysicsSystem.SetBodyRotation(this._joltBodyId, ThreeQuaternion_JoltQuat(rotation)) - - if (this._mesh) { - this._mesh.position.setFromMatrixPosition(bodyTransform) - this._mesh.rotation.setFromRotationMatrix(bodyTransform) - } } } public Dispose(): void { - console.debug("Destroying intake sensor") - if (this._joltBodyId) { World.PhysicsSystem.DestroyBodyIds(this._joltBodyId) - - if (this._mesh) { - this._mesh.geometry.dispose() - ;(this._mesh.material as THREE.Material).dispose() - World.SceneRenderer.scene.remove(this._mesh) - } } if (this._collision) OnContactPersistedEvent.RemoveListener(this._collision) diff --git a/fission/src/mirabuf/MirabufInstance.ts b/fission/src/mirabuf/MirabufInstance.ts index 174cbcbff3..94e169700e 100644 --- a/fission/src/mirabuf/MirabufInstance.ts +++ b/fission/src/mirabuf/MirabufInstance.ts @@ -109,9 +109,8 @@ class MirabufInstance { } public constructor(parser: MirabufParser, materialStyle?: MaterialStyle, progressHandle?: ProgressHandle) { - if (parser.errors.some(x => x[0] >= ParseErrorSeverity.Unimportable)) { + if (parser.errors.some(x => x[0] >= ParseErrorSeverity.Unimportable)) throw new Error("Parser has significant errors...") - } this._mirabufParser = parser this._materials = new Map() @@ -126,37 +125,32 @@ class MirabufInstance { } /** - * Parses all mirabuf appearances into ThreeJs materials. + * Parses all mirabuf appearances into ThreeJS and Jolt materials. */ private LoadMaterials(materialStyle: MaterialStyle) { Object.entries(this._mirabufParser.assembly.data!.materials!.appearances!).forEach( ([appearanceId, appearance]) => { - let hex = 0xe32b50 - let opacity = 1.0 - if (appearance.albedo) { - const { A, B, G, R } = appearance.albedo - if (A && B && G && R) { - hex = (A << 24) | (R << 16) | (G << 8) | B - opacity = A / 255.0 - } - } - - let material: THREE.Material - if (materialStyle == MaterialStyle.Regular) { - material = new THREE.MeshPhongMaterial({ - color: hex, - shininess: 0.0, - opacity: opacity, - transparent: opacity < 1.0, - }) - } else if (materialStyle == MaterialStyle.Normals) { - material = new THREE.MeshNormalMaterial() - } else if (materialStyle == MaterialStyle.Toon) { - material = World.SceneRenderer.CreateToonMaterial(hex, 5) - console.debug("Toon Material") - } - - this._materials.set(appearanceId, material!) + const { A, B, G, R } = appearance.albedo ?? {} + const [hex, opacity] = + A && B && G && R ? [(A << 24) | (R << 16) | (G << 8) | B, A / 255.0] : [0xe32b50, 1.0] + + const material = + materialStyle === MaterialStyle.Regular + ? new THREE.MeshStandardMaterial({ + // No specular? + color: hex, + roughness: appearance.roughness ?? 0.5, + metalness: appearance.metallic ?? 0.0, + shadowSide: THREE.DoubleSide, + opacity: opacity, + transparent: opacity < 1.0, + }) + : materialStyle === MaterialStyle.Normals + ? new THREE.MeshNormalMaterial() + : World.SceneRenderer.CreateToonMaterial(hex, 5) + + World.SceneRenderer.SetupMaterial(material) + this._materials.set(appearanceId, material) } ) } @@ -176,63 +170,52 @@ class MirabufInstance { const batchMap = new Map]>>() const countMap = new Map() - // Filter all instances by first material, then body - for (const instance of Object.values(instances)) { - const definition = assembly.data!.parts!.partDefinitions![instance.partDefinitionReference!]! - const bodies = definition.bodies - if (bodies) { - for (const body of bodies) { - if (!body) continue - const mesh = body.triangleMesh - if ( - mesh && - mesh.mesh && - mesh.mesh.verts && - mesh.mesh.normals && - mesh.mesh.uv && - mesh.mesh.indices - ) { - const appearanceOverride = body.appearanceOverride - const material: THREE.Material = WIREFRAME - ? new THREE.MeshStandardMaterial({ wireframe: true, color: 0x000000 }) - : appearanceOverride && this._materials.has(appearanceOverride) - ? this._materials.get(appearanceOverride)! - : fillerMaterials[nextFillerMaterial++ % fillerMaterials.length] - - let materialBodyMap = batchMap.get(material) - if (!materialBodyMap) { - materialBodyMap = new Map]>() - batchMap.set(material, materialBodyMap) - } - - const partBodyGuid = this.GetPartBodyGuid(definition, body) - let bodyInstances = materialBodyMap.get(partBodyGuid) - if (!bodyInstances) { - bodyInstances = [body, new Array()] - materialBodyMap.set(partBodyGuid, bodyInstances) - } - bodyInstances[1].push(instance) - - if (countMap.has(material)) { - const count = countMap.get(material)! - count.maxInstances += 1 - count.maxVertices += mesh.mesh.verts.length / 3 - count.maxIndices += mesh.mesh.indices.length - } else { - const count: BatchCounts = { - maxInstances: 1, - maxVertices: mesh.mesh.verts.length / 3, - maxIndices: mesh.mesh.indices.length, - } - countMap.set(material, count) - } - } + Object.values(instances).forEach(instance => { + const definition = assembly.data!.parts!.partDefinitions![instance.partDefinitionReference!] + const bodies = definition?.bodies ?? [] + bodies.forEach(body => { + const mesh = body?.triangleMesh?.mesh + if (!mesh?.verts || !mesh.normals || !mesh.uv || !mesh.indices) return + + const appearanceOverride = body.appearanceOverride + + const material = WIREFRAME + ? new THREE.MeshStandardMaterial({ wireframe: true, color: 0x000000 }) + : appearanceOverride && this._materials.has(appearanceOverride) + ? this._materials.get(appearanceOverride)! + : fillerMaterials[nextFillerMaterial++ % fillerMaterials.length] + + let materialBodyMap = batchMap.get(material) + if (!materialBodyMap) { + materialBodyMap = new Map]>() + batchMap.set(material, materialBodyMap) + } + + const partBodyGuid = this.GetPartBodyGuid(definition, body) + let bodyInstances = materialBodyMap.get(partBodyGuid) + if (!bodyInstances) { + bodyInstances = [body, new Array()] + materialBodyMap.set(partBodyGuid, bodyInstances) + } + bodyInstances[1].push(instance) + + if (countMap.has(material)) { + const count = countMap.get(material)! + count.maxInstances += 1 + count.maxVertices += mesh.verts.length / 3 + count.maxIndices += mesh.indices.length + return } - } - } - console.debug(batchMap) + const count: BatchCounts = { + maxInstances: 1, + maxVertices: mesh.verts.length / 3, + maxIndices: mesh.indices.length, + } + countMap.set(material, count) + }) + }) // Construct batched meshes batchMap.forEach((materialBodyMap, material) => { diff --git a/fission/src/mirabuf/MirabufLoader.ts b/fission/src/mirabuf/MirabufLoader.ts index 00c0068733..89b61c82e6 100644 --- a/fission/src/mirabuf/MirabufLoader.ts +++ b/fission/src/mirabuf/MirabufLoader.ts @@ -10,8 +10,9 @@ export type MirabufCacheID = string export interface MirabufCacheInfo { id: MirabufCacheID - cacheKey: string miraType: MiraType + cacheKey: string + buffer?: ArrayBuffer name?: string thumbnailStorageID?: string } @@ -21,7 +22,7 @@ export interface MirabufRemoteInfo { src: string } -type MiraCache = { [id: string]: MirabufCacheInfo } +type MapCache = { [id: MirabufCacheID]: MirabufCacheInfo } const robotsDirName = "Robots" const fieldsDirName = "Fields" @@ -29,6 +30,48 @@ const root = await navigator.storage.getDirectory() const robotFolderHandle = await root.getDirectoryHandle(robotsDirName, { create: true }) const fieldFolderHandle = await root.getDirectoryHandle(fieldsDirName, { create: true }) +export let backUpRobots: MapCache = {} +export let backUpFields: MapCache = {} + +export const canOPFS = await (async () => { + try { + if (robotFolderHandle.name == robotsDirName) { + robotFolderHandle.entries + robotFolderHandle.keys + + const fileHandle = await robotFolderHandle.getFileHandle("0", { create: true }) + const writable = await fileHandle.createWritable() + await writable.close() + await fileHandle.getFile() + + robotFolderHandle.removeEntry(fileHandle.name) + + return true + } else { + console.log(`No access to OPFS`) + return false + } + } catch (e) { + console.log(`No access to OPFS`) + + // Copy-pasted from RemoveAll() + for await (const key of robotFolderHandle.keys()) { + robotFolderHandle.removeEntry(key) + } + for await (const key of fieldFolderHandle.keys()) { + fieldFolderHandle.removeEntry(key) + } + + window.localStorage.setItem(robotsDirName, "{}") + window.localStorage.setItem(fieldsDirName, "{}") + + backUpRobots = {} + backUpFields = {} + + return false + } +})() + export function UnzipMira(buff: Uint8Array): Uint8Array { // Check if file is gzipped via magic gzip numbers 31 139 if (buff[0] == 31 && buff[1] == 139) { @@ -42,17 +85,16 @@ class MirabufCachingService { /** * Get the map of mirabuf keys and paired MirabufCacheInfo from local storage * - * @param {MiraType} miraType Type of Mirabuf Assembly. + * @param {MiraType} miraType Type of Mirabuf Assembly * - * @returns {MiraCache} Map of cached keys and paired MirabufCacheInfo + * @returns {MapCache} Map of cached keys and paired MirabufCacheInfo */ - public static GetCacheMap(miraType: MiraType): MiraCache { + public static GetCacheMap(miraType: MiraType): MapCache { if ( - (window.localStorage.getItem(MIRABUF_LOCALSTORAGE_GENERATION_KEY) ?? "") == MIRABUF_LOCALSTORAGE_GENERATION + (window.localStorage.getItem(MIRABUF_LOCALSTORAGE_GENERATION_KEY) ?? "") != MIRABUF_LOCALSTORAGE_GENERATION ) { window.localStorage.setItem(MIRABUF_LOCALSTORAGE_GENERATION_KEY, MIRABUF_LOCALSTORAGE_GENERATION) - window.localStorage.setItem(robotsDirName, "{}") - window.localStorage.setItem(fieldsDirName, "{}") + this.RemoveAll() return {} } @@ -159,18 +201,21 @@ class MirabufCachingService { thumbnailStorageID?: string ): Promise { try { - const map: MiraCache = this.GetCacheMap(miraType) + const map: MapCache = this.GetCacheMap(miraType) const id = map[key].id + const _buffer = miraType == MiraType.ROBOT ? backUpRobots[id].buffer : backUpFields[id].buffer const _name = map[key].name const _thumbnailStorageID = map[key].thumbnailStorageID - const hi: MirabufCacheInfo = { + const info: MirabufCacheInfo = { id: id, cacheKey: key, miraType: miraType, + buffer: _buffer, name: name ?? _name, thumbnailStorageID: thumbnailStorageID ?? _thumbnailStorageID, } - map[key] = hi + map[key] = info + miraType == MiraType.ROBOT ? (backUpRobots[id] = info) : (backUpFields[id] = info) window.localStorage.setItem(miraType == MiraType.ROBOT ? robotsDirName : fieldsDirName, JSON.stringify(map)) return true } catch (e) { @@ -213,16 +258,21 @@ class MirabufCachingService { */ public static async Get(id: MirabufCacheID, miraType: MiraType): Promise { try { - const fileHandle = await (miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle).getFileHandle( - id, - { - create: false, - } - ) - - // Get assembly from file - if (fileHandle) { - const buff = await fileHandle.getFile().then(x => x.arrayBuffer()) + // Get buffer from hashMap. If not in hashMap, check OPFS. Otherwise, buff is undefined + const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields + const buff = + cache[id]?.buffer ?? + (await (async () => { + const fileHandle = canOPFS + ? await (miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle).getFileHandle(id, { + create: false, + }) + : undefined + return fileHandle ? await fileHandle.getFile().then(x => x.arrayBuffer()) : undefined + })()) + + // If we have buffer, get assembly + if (buff) { const assembly = this.AssemblyFromBuffer(buff) World.AnalyticsSystem?.Event("Cache Get", { key: id, @@ -232,11 +282,10 @@ class MirabufCachingService { }) return assembly } else { - console.error(`Failed to get file handle for ID: ${id}`) - return undefined + console.error(`Failed to find arrayBuffer for id: ${id}`) } } catch (e) { - console.error(`Failed to find file from OPFS\n${e}`) + console.error(`Failed to find file\n${e}`) return undefined } } @@ -261,8 +310,15 @@ class MirabufCachingService { ) } - const dir = miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle - await dir.removeEntry(id) + if (canOPFS) { + const dir = miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle + await dir.removeEntry(id) + } + + const backUpCache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields + if (backUpCache) { + delete backUpCache[id] + } World.AnalyticsSystem?.Event("Cache Remove", { key: key, @@ -280,15 +336,20 @@ class MirabufCachingService { * Removes all Mirabuf files from the caching services. Mostly for debugging purposes. */ public static async RemoveAll() { - for await (const key of robotFolderHandle.keys()) { - robotFolderHandle.removeEntry(key) - } - for await (const key of fieldFolderHandle.keys()) { - fieldFolderHandle.removeEntry(key) + if (canOPFS) { + for await (const key of robotFolderHandle.keys()) { + robotFolderHandle.removeEntry(key) + } + for await (const key of fieldFolderHandle.keys()) { + fieldFolderHandle.removeEntry(key) + } } - window.localStorage.removeItem(robotsDirName) - window.localStorage.removeItem(fieldsDirName) + window.localStorage.setItem(robotsDirName, "{}") + window.localStorage.setItem(fieldsDirName, "{}") + + backUpRobots = {} + backUpFields = {} } // Optional name for when assembly is being decoded anyway like in CacheAndGetLocal() @@ -298,28 +359,19 @@ class MirabufCachingService { miraType?: MiraType, name?: string ): Promise { - // Store in OPFS - const backupID = Date.now().toString() try { + const backupID = Date.now().toString() if (!miraType) { - console.log("Double loading") + console.debug("Double loading") miraType = this.AssemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD } - const fileHandle = await (miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle).getFileHandle( - backupID, - { create: true } - ) - const writable = await fileHandle.createWritable() - await writable.write(miraBuff) - await writable.close() - // Local cache map - const map: MiraCache = this.GetCacheMap(miraType) + const map: MapCache = this.GetCacheMap(miraType) const info: MirabufCacheInfo = { id: backupID, - cacheKey: key, miraType: miraType, + cacheKey: key, name: name, } map[key] = info @@ -331,6 +383,29 @@ class MirabufCachingService { type: miraType == MiraType.ROBOT ? "robot" : "field", fileSize: miraBuff.byteLength, }) + + // Store buffer + if (canOPFS) { + // Store in OPFS + const fileHandle = await ( + miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle + ).getFileHandle(backupID, { create: true }) + const writable = await fileHandle.createWritable() + await writable.write(miraBuff) + await writable.close() + } + + // Store in hash + const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields + const mapInfo: MirabufCacheInfo = { + id: backupID, + miraType: miraType, + cacheKey: key, + buffer: miraBuff, + name: name, + } + cache[backupID] = mapInfo + return info } catch (e) { console.error("Failed to cache mira " + e) @@ -353,7 +428,7 @@ class MirabufCachingService { export enum MiraType { ROBOT = 1, - FIELD = 2, + FIELD, } export default MirabufCachingService diff --git a/fission/src/mirabuf/MirabufParser.ts b/fission/src/mirabuf/MirabufParser.ts index dd7df1e7cf..1b81165f1c 100644 --- a/fission/src/mirabuf/MirabufParser.ts +++ b/fission/src/mirabuf/MirabufParser.ts @@ -83,97 +83,31 @@ class MirabufParser { this.GenerateTreeValues() this.LoadGlobalTransforms() - // eslint-disable-next-line @typescript-eslint/no-this-alias - const that = this - - function traverseTree(nodes: mirabuf.INode[], op: (node: mirabuf.INode) => void) { - nodes.forEach(x => { - if (x.children) { - traverseTree(x.children, op) - } - op(x) - }) - } - - // 1: Initial Rigidgroups from ancestorial breaks in joints - const jointInstanceKeys = Object.keys(assembly.data!.joints!.jointInstances!) as string[] - jointInstanceKeys.forEach(key => { - if (key != GROUNDED_JOINT_ID) { - const jInst = assembly.data!.joints!.jointInstances![key] - const [ancestorA, ancestorB] = this.FindAncestorialBreak(jInst.parentPart!, jInst.childPart!) - const parentRN = this.NewRigidNode() - this.MovePartToRigidNode(ancestorA, parentRN) - this.MovePartToRigidNode(ancestorB, this.NewRigidNode()) - if (jInst.parts && jInst.parts.nodes) - traverseTree(jInst.parts.nodes, x => this.MovePartToRigidNode(x.value!, parentRN)) - } - }) - - // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); + this.InitializeRigidGroups() // 1: from ancestral breaks in joints // Fields Only: Assign Game Piece rigid nodes - if (!assembly.dynamic) { - // Collect all definitions labelled as gamepieces (dynamic = true) - const gamepieceDefinitions: Set = new Set() - - Object.values(assembly.data!.parts!.partDefinitions!).forEach((def: mirabuf.IPartDefinition) => { - if (def.dynamic) gamepieceDefinitions.add(def.info!.GUID!) - }) - - // Create gamepiece rigid nodes from partinstances with corresponding definitions - Object.values(assembly.data!.parts!.partInstances!).forEach((inst: mirabuf.IPartInstance) => { - if (gamepieceDefinitions.has(inst.partDefinitionReference!)) { - const instNode = this.BinarySearchDesignTree(inst.info!.GUID!) - if (instNode) { - const gpRn = this.NewRigidNode(GAMEPIECE_SUFFIX) - gpRn.isGamePiece = true - this.MovePartToRigidNode(instNode!.value!, gpRn) - instNode.children && - traverseTree(instNode.children, x => this.MovePartToRigidNode(x.value!, gpRn)) - } else { - this._errors.push([ParseErrorSeverity.LikelyIssues, "Failed to find Game piece in Design Tree"]) - } - } - }) - } + if (!assembly.dynamic) this.AssignGamePieceRigidNodes() // 2: Grounded joint const gInst = assembly.data!.joints!.jointInstances![GROUNDED_JOINT_ID] const gNode = this.NewRigidNode() this.MovePartToRigidNode(gInst.parts!.nodes!.at(0)!.value!, gNode) - // traverseTree(gInst.parts!.nodes!, x => (!this._partToNodeMap.has(x.value!)) && this.MovePartToRigidNode(x.value!, gNode)); - // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); // 3: Traverse and round up const traverseNodeRoundup = (node: mirabuf.INode, parentNode: RigidNode) => { - const currentNode = that._partToNodeMap.get(node.value!) - if (!currentNode) { - that.MovePartToRigidNode(node.value!, parentNode) - } + const currentNode = this._partToNodeMap.get(node.value!) + if (!currentNode) this.MovePartToRigidNode(node.value!, parentNode) - if (node.children) { - node.children!.forEach(x => traverseNodeRoundup(x, currentNode ? currentNode : parentNode)) - } + if (!node.children) return + node.children.forEach(x => traverseNodeRoundup(x, currentNode ?? parentNode)) } this._designHierarchyRoot.children?.forEach(x => traverseNodeRoundup(x, gNode)) // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); - // 4: Bandage via rigidgroups - assembly.data!.joints!.rigidGroups!.forEach(rg => { - let rn: RigidNode | null = null - rg.occurrences!.forEach(y => { - const currentRn = this._partToNodeMap.get(y)! - if (!rn) { - rn = currentRn - } else if (currentRn.id != rn.id) { - rn = this.MergeRigidNodes(currentRn, rn) - } - }) - }) - + this.BandageRigidNodes(assembly) // 4: Bandage via RigidGroups // this.DebugPrintHierarchy(1, ...this._designHierarchyRoot.children!); // 5. Remove Empty RNs @@ -181,70 +115,134 @@ class MirabufParser { // 6. If field, find grounded node and set isDynamic to false. Also just find grounded node again this._groundedNode = this.partToNodeMap.get(gInst.parts!.nodes!.at(0)!.value!) - if (!assembly.dynamic && this._groundedNode) { - this._groundedNode.isDynamic = false - } + if (!assembly.dynamic && this._groundedNode) this._groundedNode.isDynamic = false // 7. Update root RigidNode - const rootNode = this._partToNodeMap.get(gInst.parts!.nodes!.at(0)!.value!) - if (rootNode) { - rootNode.isRoot = true - this._rootNode = rootNode.id - } else { - this._rootNode = this._rigidNodes[0].id + const rootNodeId = this._partToNodeMap.get(gInst.parts!.nodes!.at(0)!.value!)?.id ?? this._rigidNodes[0].id + this._rootNode = rootNodeId + + // 8. Retrieve Masses + this._rigidNodes.forEach(rn => { + rn.mass = 0 + rn.parts.forEach(part => { + const inst = assembly.data?.parts?.partInstances?.[part] + if (!inst?.partDefinitionReference) return + const def = assembly.data?.parts?.partDefinitions?.[inst.partDefinitionReference!] + rn.mass += def?.massOverride ? def.massOverride : def?.physicalData?.mass ?? 0 + }) + }) + + this._directedGraph = this.GenerateRigidNodeGraph(assembly, rootNodeId) + + if (!this.assembly.data?.parts?.partDefinitions) { + console.warn("Failed to get part definitions") + return } + } + + private TraverseTree(nodes: mirabuf.INode[], op: (node: mirabuf.INode) => void) { + nodes.forEach(node => { + if (node.children) this.TraverseTree(node.children, op) + op(node) + }) + } + + private InitializeRigidGroups() { + const jointInstanceKeys = Object.keys(this._assembly.data!.joints!.jointInstances!) as string[] + jointInstanceKeys.forEach(key => { + if (key === GROUNDED_JOINT_ID) return + + const jInst = this._assembly.data!.joints!.jointInstances![key] + const [ancestorA, ancestorB] = this.FindAncestorialBreak(jInst.parentPart!, jInst.childPart!) + const parentRN = this.NewRigidNode() + + this.MovePartToRigidNode(ancestorA, parentRN) + this.MovePartToRigidNode(ancestorB, this.NewRigidNode()) + + if (jInst.parts && jInst.parts.nodes) + this.TraverseTree(jInst.parts.nodes, x => this.MovePartToRigidNode(x.value!, parentRN)) + }) + } + + private AssignGamePieceRigidNodes() { + // Collect all definitions labeled as gamepieces (dynamic = true) + const gamepieceDefinitions: Set = new Set( + Object.values(this._assembly.data!.parts!.partDefinitions!) + .filter(def => def.dynamic) + .map((def: mirabuf.IPartDefinition) => { + return def.info!.GUID! + }) + ) + + // Create gamepiece rigid nodes from PartInstances with corresponding definitions + Object.values(this._assembly.data!.parts!.partInstances!).forEach((inst: mirabuf.IPartInstance) => { + if (!gamepieceDefinitions.has(inst.partDefinitionReference!)) return + + const instNode = this.BinarySearchDesignTree(inst.info!.GUID!) + if (!instNode) { + this._errors.push([ParseErrorSeverity.LikelyIssues, "Failed to find Game piece in Design Tree"]) + return + } + + const gpRn = this.NewRigidNode(GAMEPIECE_SUFFIX) + gpRn.isGamePiece = true + this.MovePartToRigidNode(instNode!.value!, gpRn) + if (instNode.children) this.TraverseTree(instNode.children, x => this.MovePartToRigidNode(x.value!, gpRn)) + }) + } + + private BandageRigidNodes(assembly: mirabuf.Assembly) { + assembly.data!.joints!.rigidGroups!.forEach(rg => { + let rn: RigidNode | null = null + rg.occurrences!.forEach(y => { + const currentRn = this._partToNodeMap.get(y)! + + rn = !rn ? currentRn : currentRn.id != rn.id ? this.MergeRigidNodes(currentRn, rn) : rn + }) + }) + } - // 8. Generate Rigid Node Graph + private GenerateRigidNodeGraph(assembly: mirabuf.Assembly, rootNodeId: string): Graph { // Build undirected graph const graph = new Graph() - graph.AddNode(rootNode ? rootNode.id : this._rigidNodes[0].id) + graph.AddNode(rootNodeId) const jointInstances = Object.values(assembly.data!.joints!.jointInstances!) as mirabuf.joint.JointInstance[] jointInstances.forEach((x: mirabuf.joint.JointInstance) => { const rA = this._partToNodeMap.get(x.parentPart) const rB = this._partToNodeMap.get(x.childPart) - if (rA && rB && rA.id != rB.id) { - graph.AddNode(rA.id) - graph.AddNode(rB.id) - graph.AddEdgeUndirected(rA.id, rB.id) - } + if (!rA || !rB || rA.id == rB.id) return + graph.AddNode(rA.id) + graph.AddNode(rB.id) + graph.AddEdgeUndirected(rA.id, rB.id) }) + const directedGraph = new Graph() const whiteGreyBlackMap = new Map() - this._rigidNodes.forEach(x => { - whiteGreyBlackMap.set(x.id, false) - directedGraph.AddNode(x.id) + this._rigidNodes.forEach(node => { + whiteGreyBlackMap.set(node.id, false) + directedGraph.AddNode(node.id) }) - function directedRecursive(node: string) { - graph.GetAdjacencyList(node).forEach(x => { - if (whiteGreyBlackMap.has(x)) { + + const directedRecursive = (node: string) => { + graph + .GetAdjacencyList(node) + .filter(x => whiteGreyBlackMap.has(x)) + .forEach(x => { directedGraph.AddEdgeDirected(node, x) whiteGreyBlackMap.delete(x) directedRecursive(x) - } - }) - } - if (rootNode) { - whiteGreyBlackMap.delete(rootNode.id) - directedRecursive(rootNode.id) - } else { - whiteGreyBlackMap.delete(this._rigidNodes[0].id) - directedRecursive(this._rigidNodes[0].id) + }) } - this._directedGraph = directedGraph - // Transition: GH-1014 - const partDefinitions: { [k: string]: mirabuf.IPartDefinition } | null | undefined = - this.assembly.data?.parts?.partDefinitions - if (!partDefinitions) { - console.log("Failed to get part definitions") - return - } - console.log(partDefinitions) + whiteGreyBlackMap.delete(rootNodeId) + directedRecursive(rootNodeId) + + return directedGraph } private NewRigidNode(suffix?: string): RigidNode { - const node = new RigidNode(`${this._nodeNameCounter++}${suffix ? suffix : ""}`) + const node = new RigidNode(`${this._nodeNameCounter++}${suffix ?? ""}`) this._rigidNodes.push(node) return node } @@ -284,41 +282,34 @@ class MirabufParser { this._globalTransforms.clear() const getTransforms = (node: mirabuf.INode, parent: THREE.Matrix4) => { - for (const child of node.children!) { - if (!partInstances.has(child.value!)) { - continue - } - const partInstance = partInstances.get(child.value!)! + node.children!.forEach(child => { + const partInstance: mirabuf.IPartInstance | undefined = partInstances.get(child.value!) - if (this._globalTransforms.has(child.value!)) continue + if (!partInstance || this.globalTransforms.has(child.value!)) return const mat = MirabufTransform_ThreeMatrix4(partInstance.transform!)! // console.log(`[${partInstance.info!.name!}] -> ${matToString(mat)}`); this._globalTransforms.set(child.value!, mat.premultiply(parent)) getTransforms(child, mat) - } + }) } - for (const child of root.children!) { + root.children?.forEach(child => { const partInstance = partInstances.get(child.value!)! - let mat - if (!partInstance.transform) { - const def = partDefinitions[partInstances.get(child.value!)!.partDefinitionReference!] - if (!def.baseTransform) { - mat = new THREE.Matrix4().identity() - } else { - mat = MirabufTransform_ThreeMatrix4(def.baseTransform) - } - } else { - mat = MirabufTransform_ThreeMatrix4(partInstance.transform) - } + const def = partDefinitions[partInstance.partDefinitionReference!] + + const mat = partInstance.transform + ? MirabufTransform_ThreeMatrix4(partInstance.transform) + : def.baseTransform + ? MirabufTransform_ThreeMatrix4(def.baseTransform) + : new THREE.Matrix4().identity() // console.log(`[${partInstance.info!.name!}] -> ${matToString(mat!)}`); - this._globalTransforms.set(partInstance.info!.GUID!, mat!) - getTransforms(child, mat!) - } + this._globalTransforms.set(partInstance.info!.GUID!, mat) + getTransforms(child, mat) + }) } private FindAncestorialBreak(partA: string, partB: string): [string, string] { @@ -414,17 +405,17 @@ class MirabufParser { * Collection of mirabuf parts that are bound together */ class RigidNode { - public isRoot: boolean public id: RigidNodeId public parts: Set = new Set() public isDynamic: boolean public isGamePiece: boolean + public mass: number - public constructor(id: RigidNodeId, isDynamic?: boolean, isGamePiece?: boolean) { + public constructor(id: RigidNodeId, isDynamic?: boolean, isGamePiece?: boolean, mass?: number) { this.id = id this.isDynamic = isDynamic ?? true - this.isRoot = false this.isGamePiece = isGamePiece ?? false + this.mass = mass ?? 0 } } @@ -443,14 +434,14 @@ export class RigidNodeReadOnly { return this._original.isDynamic } - public get isRoot(): boolean { - return this._original.isRoot - } - public get isGamePiece(): boolean { return this._original.isGamePiece } + public get mass(): number { + return this._original.mass + } + public constructor(original: RigidNode) { this._original = original } diff --git a/fission/src/mirabuf/MirabufSceneObject.ts b/fission/src/mirabuf/MirabufSceneObject.ts index 2804f3ae1b..9be2078ae3 100644 --- a/fission/src/mirabuf/MirabufSceneObject.ts +++ b/fission/src/mirabuf/MirabufSceneObject.ts @@ -4,13 +4,11 @@ import MirabufInstance from "./MirabufInstance" import MirabufParser, { ParseErrorSeverity, RigidNodeId, RigidNodeReadOnly } from "./MirabufParser" import World from "@/systems/World" import Jolt from "@barclah/jolt-physics" -import { JoltMat44_ThreeMatrix4 } from "@/util/TypeConversions" +import { JoltMat44_ThreeMatrix4, JoltVec3_ThreeVector3 } from "@/util/TypeConversions" import * as THREE from "three" import JOLT from "@/util/loading/JoltSyncLoader" import { BodyAssociate, LayerReserve } from "@/systems/physics/PhysicsSystem" import Mechanism from "@/systems/physics/Mechanism" -import InputSystem from "@/systems/input/InputSystem" -import TransformGizmos from "@/ui/components/TransformGizmos" import { EjectorPreferences, FieldPreferences, IntakePreferences } from "@/systems/preferences/PreferenceTypes" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import { MiraType } from "./MirabufLoader" @@ -21,6 +19,20 @@ import ScoringZoneSceneObject from "./ScoringZoneSceneObject" import { SceneOverlayTag } from "@/ui/components/SceneOverlayEvents" import { ProgressHandle } from "@/ui/components/ProgressNotificationData" import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" +import { ContextData, ContextSupplier } from "@/ui/components/ContextMenuData" +import { CustomOrbitControls } from "@/systems/scene/CameraControls" +import GizmoSceneObject from "@/systems/scene/GizmoSceneObject" +import { + ConfigMode, + setNextConfigurePanelSettings, +} from "@/ui/panels/configuring/assembly-config/ConfigurePanelControls" +import { Global_OpenPanel } from "@/ui/components/GlobalUIControls" +import { + ConfigurationType, + setSelectedConfigurationType, +} from "@/ui/panels/configuring/assembly-config/ConfigurationType" +import { SimConfigData } from "@/ui/panels/simulation/SimConfigShared" +import WPILibBrain from "@/systems/simulation/wpilib_brain/WPILibBrain" const DEBUG_BODIES = false @@ -29,7 +41,24 @@ interface RnDebugMeshes { comMesh: THREE.Mesh } -class MirabufSceneObject extends SceneObject { +/** + * The goal with the spotlight assembly is to provide a contextual target assembly + * the user would like to modifiy. Generally this will be which even assembly was + * last spawned in, however, systems (such as the configuration UI) can elect + * assemblies to be in the spotlight when moving from interface to interface. + */ +let spotlightAssembly: number | undefined + +export function setSpotlightAssembly(assembly: MirabufSceneObject) { + spotlightAssembly = assembly.id +} + +// TODO: If nothing is in the spotlight, select last entry before defaulting to undefined +export function getSpotlightAssembly(): MirabufSceneObject | undefined { + return World.SceneRenderer.sceneObjects.get(spotlightAssembly ?? 0) as MirabufSceneObject +} + +class MirabufSceneObject extends SceneObject implements ContextSupplier { private _assemblyName: string private _mirabufInstance: MirabufInstance private _mechanism: Mechanism @@ -38,10 +67,9 @@ class MirabufSceneObject extends SceneObject { private _debugBodies: Map | null private _physicsLayerReserve: LayerReserve | undefined - private _transformGizmos: TransformGizmos | undefined - private _intakePreferences: IntakePreferences | undefined private _ejectorPreferences: EjectorPreferences | undefined + private _simConfigData: SimConfigData | undefined private _fieldPreferences: FieldPreferences | undefined @@ -51,6 +79,22 @@ class MirabufSceneObject extends SceneObject { private _nameTag: SceneOverlayTag | undefined + private _intakeActive = false + private _ejectorActive = false + + public get intakeActive() { + return this._intakeActive + } + public get ejectorActive() { + return this._ejectorActive + } + public set intakeActive(a: boolean) { + this._intakeActive = a + } + public set ejectorActive(a: boolean) { + this._ejectorActive = a + } + get mirabufInstance() { return this._mirabufInstance } @@ -71,6 +115,10 @@ class MirabufSceneObject extends SceneObject { return this._ejectorPreferences } + get simConfigData() { + return this._simConfigData + } + get fieldPreferences() { return this._fieldPreferences } @@ -91,6 +139,12 @@ class MirabufSceneObject extends SceneObject { return this._brain } + public set brain(brain: Brain | undefined) { + this._brain = brain + const simLayer = World.SimulationSystem.GetSimulationLayer(this._mechanism)! + simLayer.SetBrain(brain) + } + public constructor(mirabufInstance: MirabufInstance, assemblyName: string, progressHandle?: ProgressHandle) { super() @@ -100,20 +154,20 @@ class MirabufSceneObject extends SceneObject { progressHandle?.Update("Creating mechanism...", 0.9) this._mechanism = World.PhysicsSystem.CreateMechanismFromParser(this._mirabufInstance.parser) - if (this._mechanism.layerReserve) { - this._physicsLayerReserve = this._mechanism.layerReserve - } + if (this._mechanism.layerReserve) this._physicsLayerReserve = this._mechanism.layerReserve this._debugBodies = null - this.EnableTransformControls() // adding transform gizmo to mirabuf object on its creation - this.getPreferences() // creating nametag for robots if (this.miraType === MiraType.ROBOT) { this._nameTag = new SceneOverlayTag(() => - this._brain instanceof SynthesisBrain ? this._brain.inputSchemeName : "Not Configured" + this._brain instanceof SynthesisBrain + ? this._brain.inputSchemeName + : this._brain instanceof WPILibBrain + ? "Magic" + : "Not Configured" ) } } @@ -150,96 +204,54 @@ class MirabufSceneObject extends SceneObject { }) // Simulation - World.SimulationSystem.RegisterMechanism(this._mechanism) - const simLayer = World.SimulationSystem.GetSimulationLayer(this._mechanism)! - this._brain = new SynthesisBrain(this._mechanism, this._assemblyName) - simLayer.SetBrain(this._brain) + if (this.miraType == MiraType.ROBOT) { + World.SimulationSystem.RegisterMechanism(this._mechanism) + const simLayer = World.SimulationSystem.GetSimulationLayer(this._mechanism)! + this._brain = new SynthesisBrain(this, this._assemblyName) + simLayer.SetBrain(this._brain) + } // Intake this.UpdateIntakeSensor() - this.UpdateScoringZones() - } - public Update(): void { - const brainIndex = this._brain instanceof SynthesisBrain ? this._brain.brainIndex ?? -1 : -1 - if (InputSystem.getInput("eject", brainIndex)) { - this.Eject() - } + setSpotlightAssembly(this) - this._mirabufInstance.parser.rigidNodes.forEach(rn => { - if (!this._mirabufInstance.meshes.size) return // if this.dispose() has been ran then return - const body = World.PhysicsSystem.GetBody(this._mechanism.GetBodyByNodeId(rn.id)!) - const transform = JoltMat44_ThreeMatrix4(body.GetWorldTransform()) - rn.parts.forEach(part => { - const partTransform = this._mirabufInstance.parser.globalTransforms - .get(part)! - .clone() - .premultiply(transform) - const meshes = this._mirabufInstance.meshes.get(part) ?? [] - meshes.forEach(([batch, id]) => batch.setMatrixAt(id, partTransform)) - }) + this.UpdateBatches() - /** - * Update the position and rotation of the body to match the position of the transform gizmo. - * - * This block of code should only be executed if the transform gizmo exists. - */ - if (this._transformGizmos) { - if (InputSystem.isKeyPressed("Enter")) { - // confirming placement of the mirabuf object - this.DisableTransformControls() - return - } else if (InputSystem.isKeyPressed("Escape")) { - // cancelling the creation of the mirabuf scene object - World.SceneRenderer.RemoveSceneObject(this.id) - return - } + const bounds = this.ComputeBoundingBox() + if (!Number.isFinite(bounds.min.y)) return - // if the gizmo is being dragged, copy the mesh position and rotation to the Mirabuf body - if (this._transformGizmos.isBeingDragged()) { - this._transformGizmos.UpdateMirabufPositioning(this, rn) - World.PhysicsSystem.DisablePhysicsForBody(this._mechanism.GetBodyByNodeId(rn.id)!) - } - } - - if (isNaN(body.GetPosition().GetX())) { - const vel = body.GetLinearVelocity() - const pos = body.GetPosition() - console.warn( - `Invalid Position.\nPosition => ${pos.GetX()}, ${pos.GetY()}, ${pos.GetZ()}\nVelocity => ${vel.GetX()}, ${vel.GetY()}, ${vel.GetZ()}` - ) - } - // console.debug(`POSITION: ${body.GetPosition().GetX()}, ${body.GetPosition().GetY()}, ${body.GetPosition().GetZ()}`) + const offset = new JOLT.Vec3( + -(bounds.min.x + bounds.max.x) / 2.0, + 0.1 + (bounds.max.y - bounds.min.y) / 2.0 - (bounds.min.y + bounds.max.y) / 2.0, + -(bounds.min.z + bounds.max.z) / 2.0 + ) - if (this._debugBodies) { - const { colliderMesh, comMesh } = this._debugBodies.get(rn.id)! - colliderMesh.position.setFromMatrixPosition(transform) - colliderMesh.rotation.setFromRotationMatrix(transform) + this._mirabufInstance.parser.rigidNodes.forEach(rn => { + const jBodyId = this._mechanism.GetBodyByNodeId(rn.id) + if (!jBodyId) return - const comTransform = JoltMat44_ThreeMatrix4(body.GetCenterOfMassTransform()) + const newPos = World.PhysicsSystem.GetBody(jBodyId).GetPosition().Add(offset) + World.PhysicsSystem.SetBodyPosition(jBodyId, newPos) - comMesh.position.setFromMatrixPosition(comTransform) - comMesh.rotation.setFromRotationMatrix(comTransform) - } + JOLT.destroy(newPos) }) - this._mirabufInstance.batches.forEach(x => { - x.computeBoundingBox() - x.computeBoundingSphere() - }) + this.UpdateMeshTransforms() - /* Updating the position of the name tag according to the robots position on screen */ - if (this._nameTag && PreferencesSystem.getGlobalPreference("RenderSceneTags")) { - const boundingBox = this.ComputeBoundingBox() - this._nameTag.position = World.SceneRenderer.WorldToPixelSpace( - new THREE.Vector3( - (boundingBox.max.x + boundingBox.min.x) / 2, - boundingBox.max.y + 0.1, - (boundingBox.max.z + boundingBox.min.z) / 2 - ) - ) + const cameraControls = World.SceneRenderer.currentCameraControls as CustomOrbitControls + cameraControls.focusProvider = this + } + + public Update(): void { + if (this.ejectorActive) { + this.Eject() } + + this.UpdateMeshTransforms() + this.UpdateBatches() + this.UpdateNameTag() } public Dispose(): void { @@ -261,7 +273,6 @@ class MirabufSceneObject extends SceneObject { }) this._nameTag?.Dispose() - this.DisableTransformControls() World.SimulationSystem.UnregisterMechanism(this._mechanism) World.PhysicsSystem.DestroyMechanism(this._mechanism) this._mirabufInstance.Dispose(World.SceneRenderer.scene) @@ -275,13 +286,13 @@ class MirabufSceneObject extends SceneObject { this._debugBodies?.clear() this._physicsLayerReserve?.Release() - if (this._brain && this._brain instanceof SynthesisBrain) this._brain?.clearControls() + if (this._brain && this._brain instanceof SynthesisBrain) { + this._brain.clearControls() + } } public Eject() { - if (!this._ejectable) { - return - } + if (!this._ejectable) return this._ejectable.Eject() World.SceneRenderer.RemoveSceneObject(this._ejectable.id) @@ -321,6 +332,70 @@ class MirabufSceneObject extends SceneObject { return mesh } + /** + * Matches mesh transforms to their Jolt counterparts. + */ + public UpdateMeshTransforms() { + this._mirabufInstance.parser.rigidNodes.forEach(rn => { + if (!this._mirabufInstance.meshes.size) return // if this.dispose() has been ran then return + const body = World.PhysicsSystem.GetBody(this._mechanism.GetBodyByNodeId(rn.id)!) + const transform = JoltMat44_ThreeMatrix4(body.GetWorldTransform()) + this.UpdateNodeParts(rn, transform) + + if (isNaN(body.GetPosition().GetX())) { + const vel = body.GetLinearVelocity() + const pos = body.GetPosition() + console.warn( + `Invalid Position.\nPosition => ${pos.GetX()}, ${pos.GetY()}, ${pos.GetZ()}\nVelocity => ${vel.GetX()}, ${vel.GetY()}, ${vel.GetZ()}` + ) + } + + if (this._debugBodies) { + const { colliderMesh, comMesh } = this._debugBodies.get(rn.id)! + colliderMesh.position.setFromMatrixPosition(transform) + colliderMesh.rotation.setFromRotationMatrix(transform) + + const comTransform = JoltMat44_ThreeMatrix4(body.GetCenterOfMassTransform()) + + comMesh.position.setFromMatrixPosition(comTransform) + comMesh.rotation.setFromRotationMatrix(comTransform) + } + }) + } + + public UpdateNodeParts(rn: RigidNodeReadOnly, transform: THREE.Matrix4) { + rn.parts.forEach(part => { + const partTransform = this._mirabufInstance.parser.globalTransforms + .get(part)! + .clone() + .premultiply(transform) + const meshes = this._mirabufInstance.meshes.get(part) ?? [] + meshes.forEach(([batch, id]) => batch.setMatrixAt(id, partTransform)) + }) + } + + /** Updates the batch computations */ + private UpdateBatches() { + this._mirabufInstance.batches.forEach(x => { + x.computeBoundingBox() + x.computeBoundingSphere() + }) + } + + /** Updates the position of the nametag relative to the robots position */ + private UpdateNameTag() { + if (this._nameTag && PreferencesSystem.getGlobalPreference("RenderSceneTags")) { + const boundingBox = this.ComputeBoundingBox() + this._nameTag.position = World.SceneRenderer.WorldToPixelSpace( + new THREE.Vector3( + (boundingBox.max.x + boundingBox.min.x) / 2, + boundingBox.max.y + 0.1, + (boundingBox.max.z + boundingBox.min.z) / 2 + ) + ) + } + } + public UpdateIntakeSensor() { if (this._intakeSensor) { World.SceneRenderer.RemoveSceneObject(this._intakeSensor.id) @@ -336,9 +411,7 @@ class MirabufSceneObject extends SceneObject { public SetEjectable(bodyId?: Jolt.BodyID, removeExisting: boolean = false): boolean { if (this._ejectable) { - if (!removeExisting) { - return false - } + if (!removeExisting) return false World.SceneRenderer.RemoveSceneObject(this._ejectable.id) this._ejectable = undefined @@ -372,34 +445,7 @@ class MirabufSceneObject extends SceneObject { } /** - * Changes the mode of the mirabuf object from being interacted with to being placed. - */ - public EnableTransformControls(): void { - if (this._transformGizmos) return - - this._transformGizmos = new TransformGizmos( - new THREE.Mesh( - new THREE.SphereGeometry(3.0), - new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0 }) - ) - ) - this._transformGizmos.AddMeshToScene() - this._transformGizmos.CreateGizmo("translate", 5.0) - - this.DisablePhysics() - } - - /** - * Changes the mode of the mirabuf object from being placed to being interacted with. - */ - public DisableTransformControls(): void { - if (!this._transformGizmos) return - this._transformGizmos?.RemoveGizmos() - this._transformGizmos = undefined - this.EnablePhysics() - } - - /** + * Calculates the bounding box of the mirabuf object. * * @returns The bounding box of the mirabuf object. */ @@ -412,28 +458,161 @@ class MirabufSceneObject extends SceneObject { return box } + /** + * Once a gizmo is created and attached to this mirabuf object, this will be executed to align the gizmo correctly. + * + * @param gizmo Gizmo attached to the mirabuf object + */ + public PostGizmoCreation(gizmo: GizmoSceneObject) { + const jRootId = this.GetRootNodeId() + if (!jRootId) { + console.error("No root node found.") + return + } + + const jBody = World.PhysicsSystem.GetBody(jRootId) + if (jBody.IsStatic()) { + const aaBox = jBody.GetWorldSpaceBounds() + const mat = new THREE.Matrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) + const center = aaBox.mMin.Add(aaBox.mMax).Div(2.0) + mat.compose(JoltVec3_ThreeVector3(center), new THREE.Quaternion(0, 0, 0, 1), new THREE.Vector3(1, 1, 1)) + gizmo.SetTransform(mat) + } else { + gizmo.SetTransform(JoltMat44_ThreeMatrix4(jBody.GetCenterOfMassTransform())) + } + } + private getPreferences(): void { - this._intakePreferences = PreferencesSystem.getRobotPreferences(this.assemblyName)?.intake - this._ejectorPreferences = PreferencesSystem.getRobotPreferences(this.assemblyName)?.ejector + const robotPrefs = PreferencesSystem.getRobotPreferences(this.assemblyName) + if (robotPrefs) { + this._intakePreferences = robotPrefs.intake + this._ejectorPreferences = robotPrefs.ejector + this._simConfigData = robotPrefs.simConfig + } this._fieldPreferences = PreferencesSystem.getFieldPreferences(this.assemblyName) } - private EnablePhysics() { + public UpdateSimConfig(config: SimConfigData | undefined) { + const robotPrefs = PreferencesSystem.getRobotPreferences(this.assemblyName) + if (robotPrefs) { + this._simConfigData = robotPrefs.simConfig = config + PreferencesSystem.setRobotPreferences(this.assemblyName, robotPrefs) + PreferencesSystem.savePreferences() + ;(this._brain as WPILibBrain)?.loadSimConfig?.() + } + } + + public EnablePhysics() { this._mirabufInstance.parser.rigidNodes.forEach(rn => { World.PhysicsSystem.EnablePhysicsForBody(this._mechanism.GetBodyByNodeId(rn.id)!) }) + this._mechanism.ghostBodies.forEach(x => World.PhysicsSystem.EnablePhysicsForBody(x)) } - private DisablePhysics() { + public DisablePhysics() { this._mirabufInstance.parser.rigidNodes.forEach(rn => { World.PhysicsSystem.DisablePhysicsForBody(this._mechanism.GetBodyByNodeId(rn.id)!) }) + this._mechanism.ghostBodies.forEach(x => World.PhysicsSystem.DisablePhysicsForBody(x)) } public GetRootNodeId(): Jolt.BodyID | undefined { return this._mechanism.GetBodyByNodeId(this._mechanism.rootBody) } + + public LoadFocusTransform(mat: THREE.Matrix4) { + const com = World.PhysicsSystem.GetBody( + this._mechanism.nodeToBody.get(this.rootNodeId)! + ).GetCenterOfMassTransform() + mat.copy(JoltMat44_ThreeMatrix4(com)) + } + + public getSupplierData(): ContextData { + const data: ContextData = { title: this.miraType == MiraType.ROBOT ? "A Robot" : "A Field", items: [] } + + data.items.push( + { + name: "Move", + func: () => { + setSelectedConfigurationType( + this.miraType == MiraType.ROBOT ? ConfigurationType.ROBOT : ConfigurationType.FIELD + ) + setNextConfigurePanelSettings({ + configMode: ConfigMode.MOVE, + selectedAssembly: this, + }) + Global_OpenPanel?.("configure") + }, + }, + { + name: "Configure", + func: () => { + setSelectedConfigurationType( + this.miraType == MiraType.ROBOT ? ConfigurationType.ROBOT : ConfigurationType.FIELD + ) + setNextConfigurePanelSettings({ + configMode: undefined, + selectedAssembly: this, + }) + Global_OpenPanel?.("configure") + }, + } + ) + + if (this.brain?.brainType == "wpilib") { + data.items.push({ + name: "Auto Testing", + func: () => { + Global_OpenPanel?.("auto-test") + }, + }) + } + + if (World.SceneRenderer.currentCameraControls.controlsType == "Orbit") { + const cameraControls = World.SceneRenderer.currentCameraControls as CustomOrbitControls + if (cameraControls.focusProvider == this) { + data.items.push({ + name: "Camera: Unfocus", + func: () => { + cameraControls.focusProvider = undefined + }, + }) + + if (cameraControls.locked) { + data.items.push({ + name: "Camera: Unlock", + func: () => { + cameraControls.locked = false + }, + }) + } else { + data.items.push({ + name: "Camera: Lock", + func: () => { + cameraControls.locked = true + }, + }) + } + } else { + data.items.push({ + name: "Camera: Focus", + func: () => { + cameraControls.focusProvider = this + }, + }) + } + } + + data.items.push({ + name: "Remove", + func: () => { + World.SceneRenderer.RemoveSceneObject(this.id) + }, + }) + + return data + } } export async function CreateMirabuf( diff --git a/fission/src/mirabuf/ScoringZoneSceneObject.ts b/fission/src/mirabuf/ScoringZoneSceneObject.ts index 469655aa12..d6984bac73 100644 --- a/fission/src/mirabuf/ScoringZoneSceneObject.ts +++ b/fission/src/mirabuf/ScoringZoneSceneObject.ts @@ -58,8 +58,6 @@ class ScoringZoneSceneObject extends SceneObject { public constructor(parentAssembly: MirabufSceneObject, index: number, render?: boolean) { super() - console.debug("Trying to create scoring zone...") - this._parentAssembly = parentAssembly this._prefs = this._parentAssembly.fieldPreferences?.scoringZones[index] this._toRender = render ?? PreferencesSystem.getGlobalPreference("RenderScoringZones") @@ -138,8 +136,6 @@ class ScoringZoneSceneObject extends SceneObject { } OnContactRemovedEvent.AddListener(this._collisionRemoved) } - - console.debug("Scoring zone created successfully") } } } @@ -195,8 +191,6 @@ class ScoringZoneSceneObject extends SceneObject { } public Dispose(): void { - console.debug("Destroying scoring zone") - if (this._joltBodyId) { World.PhysicsSystem.DestroyBodyIds(this._joltBodyId) if (this._mesh) { diff --git a/fission/src/systems/World.ts b/fission/src/systems/World.ts index 4ab1186483..a1389e80cd 100644 --- a/fission/src/systems/World.ts +++ b/fission/src/systems/World.ts @@ -9,12 +9,13 @@ import AnalyticsSystem, { AccumTimes } from "./analytics/AnalyticsSystem" class World { private static _isAlive: boolean = false private static _clock: THREE.Clock + private static _currentDeltaT: number = 0 private static _sceneRenderer: SceneRenderer private static _physicsSystem: PhysicsSystem private static _simulationSystem: SimulationSystem private static _inputSystem: InputSystem - private static _analyticsSystem: AnalyticsSystem | undefined + private static _analyticsSystem: AnalyticsSystem | undefined = undefined private static _accumTimes: AccumTimes = { frames: 0, @@ -91,18 +92,22 @@ class World { } public static UpdateWorld() { - const deltaT = World._clock.getDelta() + this._currentDeltaT = World._clock.getDelta() this._accumTimes.frames++ this._accumTimes.totalTime += this.time(() => { - this._accumTimes.simulationTime += this.time(() => World._simulationSystem.Update(deltaT)) - this._accumTimes.physicsTime += this.time(() => World._physicsSystem.Update(deltaT)) - this._accumTimes.inputTime += this.time(() => World._inputSystem.Update(deltaT)) - this._accumTimes.sceneTime += this.time(() => World._sceneRenderer.Update(deltaT)) + this._accumTimes.simulationTime += this.time(() => World._simulationSystem.Update(this._currentDeltaT)) + this._accumTimes.physicsTime += this.time(() => World._physicsSystem.Update(this._currentDeltaT)) + this._accumTimes.inputTime += this.time(() => World._inputSystem.Update(this._currentDeltaT)) + this._accumTimes.sceneTime += this.time(() => World._sceneRenderer.Update(this._currentDeltaT)) }) - World._analyticsSystem?.Update(deltaT) + World._analyticsSystem?.Update(this._currentDeltaT) + } + + public static get currentDeltaT(): number { + return this._currentDeltaT } private static time(func: () => void): number { diff --git a/fission/src/systems/analytics/AnalyticsSystem.ts b/fission/src/systems/analytics/AnalyticsSystem.ts index 6fd1d04a9d..a7dc4da255 100644 --- a/fission/src/systems/analytics/AnalyticsSystem.ts +++ b/fission/src/systems/analytics/AnalyticsSystem.ts @@ -20,36 +20,25 @@ export interface AccumTimes { class AnalyticsSystem extends WorldSystem { private _lastSampleTime = Date.now() + private _consent: boolean public constructor() { super() + this._consent = PreferencesSystem.getGlobalPreference("ReportAnalytics") init({ measurementId: "G-6XNCRD7QNC", debug: import.meta.env.DEV, anonymizeIp: true, sendPageViews: false, - trackingConsent: PreferencesSystem.getGlobalPreference("ReportAnalytics"), + trackingConsent: this._consent, }) PreferencesSystem.addEventListener( e => e.prefName == "ReportAnalytics" && this.ConsentUpdate(e.prefValue as boolean) ) - let betaCode = document.cookie.match(BETA_CODE_COOKIE_REGEX)?.[0] - if (betaCode) { - betaCode = betaCode.substring(betaCode.indexOf("=") + 1, betaCode.indexOf(";")) - - this.SetUserProperty("Beta Code", betaCode) - } else { - console.debug("No code match") - } - - if (MOBILE_USER_AGENT_REGEX.test(navigator.userAgent)) { - this.SetUserProperty("Is Mobile", "true") - } else { - this.SetUserProperty("Is Mobile", "false") - } + this.SendMetaData() } public Event(name: string, params?: { [key: string]: string | number }) { @@ -69,7 +58,33 @@ class AnalyticsSystem extends WorldSystem { } private ConsentUpdate(granted: boolean) { + this._consent = granted consent(granted) + + this.SendMetaData() + } + + private SendMetaData() { + if (import.meta.env.DEV) { + this.SetUserProperty("Internal Traffic", "true") + } + + if (!this._consent) { + return + } + + let betaCode = document.cookie.match(BETA_CODE_COOKIE_REGEX)?.[0] + if (betaCode) { + betaCode = betaCode.substring(betaCode.indexOf("=") + 1, betaCode.indexOf(";")) + + this.SetUserProperty("Beta Code", betaCode) + } + + if (MOBILE_USER_AGENT_REGEX.test(navigator.userAgent)) { + this.SetUserProperty("Is Mobile", "true") + } else { + this.SetUserProperty("Is Mobile", "false") + } } private currentSampleInterval() { diff --git a/fission/src/systems/input/DefaultInputs.ts b/fission/src/systems/input/DefaultInputs.ts index 2b9ac8d65a..4a6a87a687 100644 --- a/fission/src/systems/input/DefaultInputs.ts +++ b/fission/src/systems/input/DefaultInputs.ts @@ -1,6 +1,7 @@ import { InputScheme } from "./InputSchemeManager" import { AxisInput, ButtonInput, EmptyModifierState } from "./InputSystem" +/** The purpose of this class is to store any defaults related to the input system. */ class DefaultInputs { static ernie = () => { return { @@ -91,8 +92,8 @@ class DefaultInputs { shift: false, meta: false, }), - new AxisInput("joint 5", "KeyN", "true", -1, false, false, -1, -1, EmptyModifierState, { - ctrl: false, + new AxisInput("joint 5", "KeyN", "KeyN", -1, false, false, -1, -1, EmptyModifierState, { + ctrl: true, alt: false, shift: false, meta: false, @@ -133,6 +134,7 @@ class DefaultInputs { } } + /** We like this guy */ public static hunter = () => { return { schemeName: "Hunter", @@ -187,7 +189,8 @@ class DefaultInputs { } } - public static get defaultInputCopies() { + /** @returns {InputScheme[]} New copies of the default input schemes without reference to any others. */ + public static get defaultInputCopies(): InputScheme[] { return [ DefaultInputs.ernie(), DefaultInputs.luna(), @@ -197,6 +200,7 @@ class DefaultInputs { ] } + /** @returns {InputScheme} A new blank input scheme with no control bound. */ public static get newBlankScheme(): InputScheme { return { schemeName: "", diff --git a/fission/src/systems/input/InputSchemeManager.ts b/fission/src/systems/input/InputSchemeManager.ts index 23a5ede69d..4e2d66d73c 100644 --- a/fission/src/systems/input/InputSchemeManager.ts +++ b/fission/src/systems/input/InputSchemeManager.ts @@ -129,20 +129,6 @@ class InputSchemeManager { PreferencesSystem.setGlobalPreference("InputSchemes", customizedSchemes) PreferencesSystem.savePreferences() } - - /** Returns a copy of a scheme without references to the original in any way */ - public static copyScheme(scheme: InputScheme): InputScheme { - const copiedInputs: Input[] = [] - scheme.inputs.forEach(i => copiedInputs.push(i.getCopy())) - - return { - schemeName: scheme.schemeName, - descriptiveName: scheme.descriptiveName, - customized: scheme.customized, - usesGamepad: scheme.usesGamepad, - inputs: copiedInputs, - } - } } export default InputSchemeManager diff --git a/fission/src/systems/input/InputSystem.ts b/fission/src/systems/input/InputSystem.ts index 3a396f8968..509f46760e 100644 --- a/fission/src/systems/input/InputSystem.ts +++ b/fission/src/systems/input/InputSystem.ts @@ -9,28 +9,34 @@ export type ModifierState = { } export const EmptyModifierState: ModifierState = { ctrl: false, alt: false, shift: false, meta: false } -// Represents any input +/** Represents any user input */ abstract class Input { public inputName: string + /** @param {string} inputName - The name given to this input to identify it's function. */ constructor(inputName: string) { this.inputName = inputName } - // Returns the current value of the input. Range depends on input type + /** @returns {number} a number between -1 and 1 for this input. */ abstract getValue(useGamepad: boolean): number - - // Creates a copy to avoid modifying the default inputs by reference - abstract getCopy(): Input } -// A single button +/** Represents any user input that is a single true/false button. */ class ButtonInput extends Input { public keyCode: string public keyModifiers: ModifierState public gamepadButton: number + /** + * All optional params will remain unassigned if not value is given. This can be assigned later by the user through the configuration panel. + * + * @param {string} inputName - The name given to this input to identify it's function. + * @param {string} [keyCode] - The keyboard button for this input if a gamepad is not used. + * @param {number} [gamepadButton] - The gamepad button for this input if a gamepad is used. + * @param {ModifierState} [keyModifiers] - The key modifier state for the keyboard input. + */ public constructor(inputName: string, keyCode?: string, gamepadButton?: number, keyModifiers?: ModifierState) { super(inputName) this.keyCode = keyCode ?? "" @@ -38,7 +44,10 @@ class ButtonInput extends Input { this.gamepadButton = gamepadButton ?? -1 } - // Returns 1 if pressed and 0 if not pressed + /** + * @param useGamepad Looks at the gamepad if true and the keyboard if false. + * @returns 1 if pressed, 0 if not pressed or not found. + */ getValue(useGamepad: boolean): number { // Gamepad button input if (useGamepad) { @@ -48,13 +57,9 @@ class ButtonInput extends Input { // Keyboard button input return InputSystem.isKeyPressed(this.keyCode, this.keyModifiers) ? 1 : 0 } - - getCopy(): Input { - return new ButtonInput(this.inputName, this.keyCode, this.gamepadButton, this.keyModifiers) - } } -// An axis between two buttons (-1 to 1) +/** Represents any user input that is an axis between -1 and 1. Can be a gamepad axis, two gamepad buttons, or two keyboard buttons. */ class AxisInput extends Input { public posKeyCode: string public posKeyModifiers: ModifierState @@ -67,6 +72,20 @@ class AxisInput extends Input { public posGamepadButton: number public negGamepadButton: number + /** + * All optional params will remain unassigned if not value is given. This can be assigned later by the user through the configuration panel. + * + * @param {string} inputName - The name given to this input to identify it's function. + * @param {string} [posKeyCode] - The keyboard input that corresponds to a positive input value (1). + * @param {string} [negKeyCode] - The keyboard input that corresponds to a negative input value (-1). + * @param {number} [gamepadAxisNumber] - The gamepad axis that this input looks at if the scheme is set to use a gamepad. + * @param {boolean} [joystickInverted] - Inverts the input if a gamepad axis is used. + * @param {boolean} [useGamepadButtons] - If this is true and the scheme is set to use a gamepad, this axis will be between two buttons on the controller. + * @param {number} [posGamepadButton] - The gamepad button that corresponds to a positive input value (1). + * @param {number} [negGamepadButton] - The gamepad button that corresponds to a negative input value (-1). + * @param {ModifierState} [posKeyModifiers] - The key modifier state for the positive keyboard input. + * @param {ModifierState} [negKeyModifiers] - The key modifier state for the negative keyboard input. + */ public constructor( inputName: string, posKeyCode?: string, @@ -94,11 +113,14 @@ class AxisInput extends Input { this.negGamepadButton = negGamepadButton ?? -1 } - // For keyboard: returns 1 if positive pressed, -1 if negative pressed, or 0 if none or both are pressed - // For gamepad axis: returns a range between -1 and 1 with a deadband in the middle + /** + * @param useGamepad Looks at the gamepad if true and the keyboard if false. + * @returns {number} KEYBOARD: 1 if positive pressed, -1 if negative pressed, or 0 if none or both are pressed. + * @returns {number} GAMEPAD: a number between -1 and 1 with a deadband in the middle. + */ getValue(useGamepad: boolean): number { - // Gamepad joystick axis if (useGamepad) { + // Gamepad joystick axis if (!this.useGamepadButtons) return InputSystem.getGamepadAxis(this.gamepadAxisNumber) * (this.joystickInverted ? -1 : 1) @@ -115,41 +137,28 @@ class AxisInput extends Input { (InputSystem.isKeyPressed(this.negKeyCode, this.negKeyModifiers) ? 1 : 0) ) } - - getCopy(): Input { - return new AxisInput( - this.inputName, - this.posKeyCode, - this.negKeyCode, - this.gamepadAxisNumber, - this.joystickInverted, - this.useGamepadButtons, - this.posGamepadButton, - this.negGamepadButton, - this.posKeyModifiers, - this.negKeyModifiers - ) - } } +/** + * The input system listens for and records key presses and joystick positions to be used by robots. + * It also maps robot behaviors (such as an arcade drivetrain or an arm) to specific keys through customizable input schemes. + */ class InputSystem extends WorldSystem { public static currentModifierState: ModifierState - // A list of keys currently being pressed + /** The keys currently being pressed. */ private static _keysPressed: { [key: string]: boolean } = {} private static _gpIndex: number | null public static gamepad: Gamepad | null - // The scheme most recently selected in the controls modal - public static selectedScheme: InputScheme | undefined - - // Maps a brain index to a certain input scheme + /** Maps a brain index to an input scheme. */ public static brainIndexSchemeMap: Map = new Map() constructor() { super() + // Initialize input events this.handleKeyDown = this.handleKeyDown.bind(this) document.addEventListener("keydown", this.handleKeyDown) @@ -162,19 +171,31 @@ class InputSystem extends WorldSystem { this.gamepadDisconnected = this.gamepadDisconnected.bind(this) window.addEventListener("gamepaddisconnected", this.gamepadDisconnected) + // Initialize an event that's triggered when the user exits/enters the page document.addEventListener("visibilitychange", () => { if (document.hidden) this.clearKeyData() }) + + // Disable gesture inputs on track pad to zoom into UI + window.addEventListener( + "wheel", + function (e) { + if (e.ctrlKey) { + e.preventDefault() // Prevent the zoom + } + }, + { passive: false } + ) } public Update(_: number): void { - InputSystem // Fetch current gamepad information if (InputSystem._gpIndex == null) InputSystem.gamepad = null else InputSystem.gamepad = navigator.getGamepads()[InputSystem._gpIndex] if (!document.hasFocus()) this.clearKeyData() + // Update the current modifier state to be checked against target stats when getting input values InputSystem.currentModifierState = { ctrl: InputSystem.isKeyPressed("ControlLeft") || InputSystem.isKeyPressed("ControlRight"), alt: InputSystem.isKeyPressed("AltLeft") || InputSystem.isKeyPressed("AltRight"), @@ -190,21 +211,22 @@ class InputSystem extends WorldSystem { window.removeEventListener("gamepaddisconnected", this.gamepadDisconnected) } - // Called when any key is first pressed + /** Called when any key is first pressed */ private handleKeyDown(event: KeyboardEvent) { InputSystem._keysPressed[event.code] = true } - // Called when any key is released + /* Called when any key is released */ private handleKeyUp(event: KeyboardEvent) { InputSystem._keysPressed[event.code] = false } + /** Clears all stored key data when the user leaves the page. */ private clearKeyData() { for (const keyCode in InputSystem._keysPressed) delete InputSystem._keysPressed[keyCode] } - // Called once when a gamepad is first connected + /* Called once when a gamepad is first connected */ private gamepadConnected(event: GamepadEvent) { console.log( "Gamepad connected at index %d: %s. %d buttons, %d axes.", @@ -217,14 +239,18 @@ class InputSystem extends WorldSystem { InputSystem._gpIndex = event.gamepad.index } - // Called once when a gamepad is first disconnected + /* Called once when a gamepad is first disconnected */ private gamepadDisconnected(event: GamepadEvent) { console.log("Gamepad disconnected from index %d: %s", event.gamepad.index, event.gamepad.id) InputSystem._gpIndex = null } - // Returns true if the given key is currently down + /** + * @param {string} key - The keycode of the target key. + * @param {ModifierState} modifiers - The target modifier state. Assumed to be no modifiers if undefined. + * @returns {boolean} True if the key is pressed or false otherwise. + */ public static isKeyPressed(key: string, modifiers?: ModifierState): boolean { if (modifiers != null && !InputSystem.compareModifiers(InputSystem.currentModifierState, modifiers)) return false @@ -232,7 +258,11 @@ class InputSystem extends WorldSystem { return !!InputSystem._keysPressed[key] } - // If an input exists, return it's value + /** + * @param {string} inputName The name of the function of the input. + * @param {number} brainIndex The robot brain index for this input. Used to map to a control scheme. + * @returns {number} A number between -1 and 1 based on the current state of the input. + */ public static getInput(inputName: string, brainIndex: number): number { const targetScheme = InputSystem.brainIndexSchemeMap.get(brainIndex) @@ -243,8 +273,12 @@ class InputSystem extends WorldSystem { return targetInput.getValue(targetScheme.usesGamepad) } - // Returns true if two modifier states are identical - private static compareModifiers(state1: ModifierState, state2: ModifierState): boolean { + /** + * @param {ModifierState} state1 Any key modifier state. + * @param {ModifierState} state2 Any key modifier state. + * @returns {boolean} True if the modifier states are identical and false otherwise. + */ + public static compareModifiers(state1: ModifierState, state2: ModifierState): boolean { if (!state1 || !state2) return false return ( @@ -255,7 +289,10 @@ class InputSystem extends WorldSystem { ) } - // Returns a number between -1 and 1 with a deadband + /** + * @param {number} axisNumber The joystick axis index. Must be an integer. + * @returns {number} A number between -1 and 1 based on the position of this axis or 0 if no gamepad is connected or the axis is not found. + */ public static getGamepadAxis(axisNumber: number): number { if (InputSystem.gamepad == null) return 0 @@ -267,7 +304,11 @@ class InputSystem extends WorldSystem { return Math.abs(value) < 0.15 ? 0 : value } - // Returns true if a gamepad is connected and a certain button is pressed + /** + * + * @param {number} buttonNumber - The gamepad button index. Must be an integer. + * @returns {boolean} True if the button is pressed, false if not, a gamepad isn't connected, or the button can't be found. + */ public static isGamepadButtonPressed(buttonNumber: number): boolean { if (InputSystem.gamepad == null) return false diff --git a/fission/src/systems/physics/Mechanism.ts b/fission/src/systems/physics/Mechanism.ts index bf6502b4f5..709b57cc37 100644 --- a/fission/src/systems/physics/Mechanism.ts +++ b/fission/src/systems/physics/Mechanism.ts @@ -6,8 +6,11 @@ import { mirabuf } from "@/proto/mirabuf" export interface MechanismConstraint { parentBody: Jolt.BodyID childBody: Jolt.BodyID - constraint: Jolt.Constraint + primaryConstraint: Jolt.Constraint + maxVelocity: number info?: mirabuf.IInfo + extraConstraints: Jolt.Constraint[] + extraBodies: Jolt.BodyID[] } class Mechanism { @@ -17,6 +20,7 @@ class Mechanism { public stepListeners: Array public layerReserve: LayerReserve | undefined public controllable: boolean + public ghostBodies: Array public constructor( rootBody: string, @@ -29,6 +33,7 @@ class Mechanism { this.constraints = [] this.stepListeners = [] this.controllable = controllable + this.ghostBodies = [] this.layerReserve = layerReserve } @@ -43,6 +48,8 @@ class Mechanism { public GetBodyByNodeId(nodeId: string) { return this.nodeToBody.get(nodeId) } + + public DisablePhysics() {} } export default Mechanism diff --git a/fission/src/systems/physics/PhysicsSystem.ts b/fission/src/systems/physics/PhysicsSystem.ts index 42c43f213a..5e8dfc3189 100644 --- a/fission/src/systems/physics/PhysicsSystem.ts +++ b/fission/src/systems/physics/PhysicsSystem.ts @@ -23,9 +23,14 @@ import { OnContactValidateData, PhysicsEvent, } from "./ContactEvents" +import PreferencesSystem from "../preferences/PreferencesSystem" export type JoltBodyIndexAndSequence = number +export const PAUSE_REF_ASSEMBLY_SPAWNING = "assembly-spawning" +export const PAUSE_REF_ASSEMBLY_CONFIG = "assembly-config" +export const PAUSE_REF_ASSEMBLY_MOVE = "assembly-move" + /** * Layers used for determining enabled/disabled collisions. */ @@ -51,6 +56,11 @@ const MAX_SUBSTEPS = 6 const STANDARD_SUB_STEPS = 4 const TIMESTEP_ADJUSTMENT = 0.0001 +const SIGNIFICANT_FRICTION_THRESHOLD = 0.05 + +const MAX_ROBOT_MASS = 250.0 +const MAX_GP_MASS = 10.0 + let lastDeltaT = STANDARD_SIMULATION_PERIOD export function GetLastDeltaT(): number { return lastDeltaT @@ -58,9 +68,15 @@ export function GetLastDeltaT(): number { // Friction constants const FLOOR_FRICTION = 0.7 +const DEFAULT_FRICTION = 0.7 const SUSPENSION_MIN_FACTOR = 0.1 const SUSPENSION_MAX_FACTOR = 0.3 +const DEFAULT_PHYSICAL_MATERIAL_KEY = "default" + +// Motor constant +const VELOCITY_DEFAULT = 30 + /** * The PhysicsSystem handles all Jolt Physics interactions within Synthesis. * This system can create physical representations of objects such as Robots, @@ -75,12 +91,12 @@ class PhysicsSystem extends WorldSystem { private _physicsEventQueue: PhysicsEvent[] = [] - private _pauseCounter = 0 + private _pauseSet = new Set() private _bodyAssociations: Map public get isPaused(): boolean { - return this._pauseCounter > 0 + return this._pauseSet.size > 0 } /** @@ -103,11 +119,14 @@ class PhysicsSystem extends WorldSystem { this.SetUpContactListener(this._joltPhysSystem) this._joltPhysSystem.SetGravity(new JOLT.Vec3(0, -9.8, 0)) + this._joltPhysSystem.GetPhysicsSettings().mDeterministicSimulation = false + this._joltPhysSystem.GetPhysicsSettings().mSpeculativeContactDistance = 0.06 + this._joltPhysSystem.GetPhysicsSettings().mPenetrationSlop = 0.005 const ground = this.CreateBox( new THREE.Vector3(5.0, 0.5, 5.0), undefined, - new THREE.Vector3(0.0, -2.05, 0.0), + new THREE.Vector3(0.0, -0.5, 0.0), undefined ) ground.SetFriction(FLOOR_FRICTION) @@ -142,28 +161,28 @@ class PhysicsSystem extends WorldSystem { /** * Holds a pause. * - * The pause works off of a request counter. + * @param ref String to reference your hold. */ - public HoldPause() { - this._pauseCounter++ + public HoldPause(ref: string) { + this._pauseSet.add(ref) } /** * Forces all holds on the pause to be released. */ public ForceUnpause() { - this._pauseCounter = 0 + this._pauseSet.clear() } /** * Releases a pause. * - * The pause works off of a request counter. + * @param ref String to reference your hold. + * + * @returns Whether or not your hold was successfully removed. */ - public ReleasePause() { - if (this._pauseCounter > 0) { - this._pauseCounter-- - } + public ReleasePause(ref: string): boolean { + return this._pauseSet.delete(ref) } /** @@ -172,9 +191,7 @@ class PhysicsSystem extends WorldSystem { * @param bodyId */ public DisablePhysicsForBody(bodyId: Jolt.BodyID) { - if (!this.IsBodyAdded(bodyId)) { - return - } + if (!this.IsBodyAdded(bodyId)) return this._joltBodyInterface.DeactivateBody(bodyId) @@ -182,14 +199,12 @@ class PhysicsSystem extends WorldSystem { } /** - * Enabing physics for a single body + * Enables physics for a single body * * @param bodyId */ public EnablePhysicsForBody(bodyId: Jolt.BodyID) { - if (!this.IsBodyAdded(bodyId)) { - return - } + if (!this.IsBodyAdded(bodyId)) return this._joltBodyInterface.ActivateBody(bodyId) this.GetBody(bodyId).SetIsSensor(false) @@ -228,9 +243,8 @@ class PhysicsSystem extends WorldSystem { mass ? JOLT.EMotionType_Dynamic : JOLT.EMotionType_Static, mass ? LAYER_GENERAL_DYNAMIC : LAYER_FIELD ) - if (mass) { - creationSettings.mMassPropertiesOverride.mMass = mass - } + if (mass) creationSettings.mMassPropertiesOverride.mMass = mass + const body = this._joltBodyInterface.CreateBody(creationSettings) JOLT.destroy(pos) JOLT.destroy(rot) @@ -264,9 +278,8 @@ class PhysicsSystem extends WorldSystem { mass ? JOLT.EMotionType_Dynamic : JOLT.EMotionType_Static, mass ? LAYER_GENERAL_DYNAMIC : LAYER_FIELD ) - if (mass) { - creationSettings.mMassPropertiesOverride.mMass = mass - } + if (mass) creationSettings.mMassPropertiesOverride.mMass = mass + const body = this._joltBodyInterface.CreateBody(creationSettings) JOLT.destroy(pos) JOLT.destroy(rot) @@ -291,9 +304,8 @@ class PhysicsSystem extends WorldSystem { * @returns Resulting shape. */ public CreateConvexHull(points: Float32Array, density: number = 1.0): Jolt.ShapeResult { - if (points.length % 3) { - throw new Error(`Invalid size of points: ${points.length}`) - } + if (points.length % 3) throw new Error(`Invalid size of points: ${points.length}`) + const settings = new JOLT.ConvexHullShapeSettings() settings.mPoints.clear() settings.mPoints.reserve(points.length / 3.0) @@ -321,23 +333,21 @@ class PhysicsSystem extends WorldSystem { */ public CreateJointsFromParser(parser: MirabufParser, mechanism: Mechanism) { const jointData = parser.assembly.data!.joints! - for (const [jGuid, jInst] of Object.entries(jointData.jointInstances!) as [ - string, - mirabuf.joint.JointInstance, - ][]) { - if (jGuid == GROUNDED_JOINT_ID) continue + const joints = Object.entries(jointData.jointInstances!) as [string, mirabuf.joint.JointInstance][] + joints.forEach(([jGuid, jInst]) => { + if (jGuid == GROUNDED_JOINT_ID) return const rnA = parser.partToNodeMap.get(jInst.parentPart!) const rnB = parser.partToNodeMap.get(jInst.childPart!) if (!rnA || !rnB) { console.warn(`Skipping joint '${jInst.info!.name!}'. Couldn't find associated rigid nodes.`) - continue + return } else if (rnA.id == rnB.id) { console.warn( `Skipping joint '${jInst.info!.name!}'. Jointing the same parts. Likely in issue with Fusion Design structure.` ) - continue + return } const jDef = parser.assembly.data!.joints!.jointDefinitions![jInst.jointReference!]! as mirabuf.joint.Joint @@ -345,68 +355,91 @@ class PhysicsSystem extends WorldSystem { const bodyIdB = mechanism.GetBodyByNodeId(rnB.id) if (!bodyIdA || !bodyIdB) { console.warn(`Skipping joint '${jInst.info!.name!}'. Failed to find rigid nodes' associated bodies.`) - continue + return } const bodyA = this.GetBody(bodyIdA) const bodyB = this.GetBody(bodyIdB) - const constraints: Jolt.Constraint[] = [] - let listener: Jolt.PhysicsStepListener | undefined = undefined + // Motor velocity and acceleration. Prioritizes preferences then mirabuf. + const prefMotors = PreferencesSystem.getRobotPreferences(parser.assembly.info?.name ?? "").motors + const prefMotor = prefMotors ? prefMotors.filter(x => x.name == jInst.info?.name) : undefined + const miraMotor = jointData.motorDefinitions![jDef.motorReference] + + let maxVel = VELOCITY_DEFAULT + let maxForce + if (prefMotor && prefMotor[0]) { + maxVel = prefMotor[0].maxVelocity + maxForce = prefMotor[0].maxForce + } else if (miraMotor && miraMotor.simpleMotor) { + maxVel = miraMotor.simpleMotor.maxVelocity ?? VELOCITY_DEFAULT + maxForce = miraMotor.simpleMotor.stallTorque + } + + let listener: Jolt.PhysicsStepListener | null = null + + const addConstraint = (c: Jolt.Constraint): void => { + mechanism.AddConstraint({ + parentBody: bodyIdA, + childBody: bodyIdB, + primaryConstraint: c, + maxVelocity: maxVel ?? VELOCITY_DEFAULT, + info: jInst.info ?? undefined, // remove possibility for null + extraConstraints: [], + extraBodies: [], + }) + } switch (jDef.jointMotionType!) { case mirabuf.joint.JointMotion.REVOLUTE: if (this.IsWheel(jDef)) { - if (parser.directedGraph.GetAdjacencyList(rnA.id).length > 0) { - const res = this.CreateWheelConstraint( - jInst, - jDef, - bodyA, - bodyB, - parser.assembly.info!.version! - ) - constraints.push(res[0]) - constraints.push(res[1]) - listener = res[2] - } else { - const res = this.CreateWheelConstraint( - jInst, - jDef, - bodyB, - bodyA, - parser.assembly.info!.version! - ) - constraints.push(res[0]) - constraints.push(res[1]) - listener = res[2] - } - } else { - constraints.push( - this.CreateHingeConstraint(jInst, jDef, bodyA, bodyB, parser.assembly.info!.version!) + const preferences = PreferencesSystem.getRobotPreferences(parser.assembly.info?.name ?? "") + if (preferences.driveVelocity > 0) maxVel = preferences.driveVelocity + if (preferences.driveAcceleration > 0) maxForce = preferences.driveAcceleration + + const [bodyOne, bodyTwo] = parser.directedGraph.GetAdjacencyList(rnA.id).length + ? [bodyA, bodyB] + : [bodyB, bodyA] + + const res = this.CreateWheelConstraint( + jInst, + jDef, + maxForce ?? 1.5, + bodyOne, + bodyTwo, + parser.assembly.info!.version! ) + addConstraint(res[0]) + addConstraint(res[1]) + listener = res[2] + + break } + + addConstraint( + this.CreateHingeConstraint( + jInst, + jDef, + maxForce ?? 50, + bodyA, + bodyB, + parser.assembly.info!.version! + ) + ) + break + case mirabuf.joint.JointMotion.SLIDER: - constraints.push(this.CreateSliderConstraint(jInst, jDef, bodyA, bodyB)) + addConstraint(this.CreateSliderConstraint(jInst, jDef, maxForce ?? 200, bodyA, bodyB)) + break + case mirabuf.joint.JointMotion.BALL: + this.CreateBallConstraint(jInst, jDef, bodyA, bodyB, mechanism) break default: console.debug("Unsupported joint detected. Skipping...") break } - - if (constraints.length > 0) { - constraints.forEach(x => - mechanism.AddConstraint({ - parentBody: bodyIdA, - childBody: bodyIdB, - constraint: x, - info: jInst.info ?? undefined, // remove possibility for null - }) - ) - } - if (listener) { - mechanism.AddStepListener(listener) - } - } + if (listener) mechanism.AddStepListener(listener) + }) } /** @@ -422,6 +455,7 @@ class PhysicsSystem extends WorldSystem { private CreateHingeConstraint( jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, + torque: number, bodyA: Jolt.Body, bodyB: Jolt.Body, versionNum: number @@ -443,20 +477,17 @@ class PhysicsSystem extends WorldSystem { const rotationalFreedom = jointDefinition.rotational!.rotationalFreedom! const miraAxis = rotationalFreedom.axis! as mirabuf.Vector3 - let axis: Jolt.Vec3 // No scaling, these are unit vectors - if (versionNum < 5) { - axis = new JOLT.Vec3(miraAxis.x ? -miraAxis.x : 0, miraAxis.y ?? 0, miraAxis.z! ?? 0) - } else { - axis = new JOLT.Vec3(miraAxis.x! ?? 0, miraAxis.y! ?? 0, miraAxis.z! ?? 0) - } + const miraAxisX = (versionNum < 5 ? -miraAxis.x : miraAxis.x) ?? 0 + const axis = new JOLT.Vec3(miraAxisX, miraAxis.y! ?? 0, miraAxis.z! ?? 0) + hingeConstraintSettings.mHingeAxis1 = hingeConstraintSettings.mHingeAxis2 = axis.Normalized() hingeConstraintSettings.mNormalAxis1 = hingeConstraintSettings.mNormalAxis2 = getPerpendicular( hingeConstraintSettings.mHingeAxis1 ) // Some values that are meant to be exactly PI are perceived as being past it, causing unexpected behavior. - // This safety check caps the values to be within [-PI, PI] wth minimal difference in precision. + // This safety check caps the values to be within [-PI, PI] with minimal difference in precision. const piSafetyCheck = (v: number) => Math.min(3.14158, Math.max(-3.14158, v)) if ( @@ -471,7 +502,11 @@ class PhysicsSystem extends WorldSystem { hingeConstraintSettings.mLimitsMax = -lower } + hingeConstraintSettings.mMotorSettings.mMaxTorqueLimit = torque + hingeConstraintSettings.mMotorSettings.mMinTorqueLimit = -torque + const constraint = hingeConstraintSettings.Create(bodyA, bodyB) + this._constraints.push(constraint) this._joltPhysSystem.AddConstraint(constraint) return constraint @@ -490,6 +525,7 @@ class PhysicsSystem extends WorldSystem { private CreateSliderConstraint( jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, + maxForce: number, bodyA: Jolt.Body, bodyB: Jolt.Body ): Jolt.Constraint { @@ -535,6 +571,9 @@ class PhysicsSystem extends WorldSystem { sliderConstraintSettings.mLimitsMin = -halfRange } + sliderConstraintSettings.mMotorSettings.mMaxForceLimit = maxForce + sliderConstraintSettings.mMotorSettings.mMinForceLimit = -maxForce + const constraint = sliderConstraintSettings.Create(bodyA, bodyB) this._constraints.push(constraint) @@ -546,6 +585,7 @@ class PhysicsSystem extends WorldSystem { public CreateWheelConstraint( jointInstance: mirabuf.joint.JointInstance, jointDefinition: mirabuf.joint.Joint, + maxAcc: number, bodyMain: Jolt.Body, bodyWheel: Jolt.Body, versionNum: number @@ -565,14 +605,10 @@ class PhysicsSystem extends WorldSystem { const rotationalFreedom = jointDefinition.rotational!.rotationalFreedom! - const miraAxis = rotationalFreedom.axis! as mirabuf.Vector3 - let axis: Jolt.Vec3 // No scaling, these are unit vectors - if (versionNum < 5) { - axis = new JOLT.Vec3(miraAxis.x ? -miraAxis.x : 0, miraAxis.y ?? 0, miraAxis.z ?? 0) - } else { - axis = new JOLT.Vec3(miraAxis.x ?? 0, miraAxis.y ?? 0, miraAxis.z ?? 0) - } + const miraAxis = rotationalFreedom.axis! as mirabuf.Vector3 + const miraAxisX: number = (versionNum < 5 ? -miraAxis.x : miraAxis.x) ?? 0 + const axis: Jolt.Vec3 = new JOLT.Vec3(miraAxisX, miraAxis.y ?? 0, miraAxis.z ?? 0) const bounds = bodyWheel.GetShape().GetLocalBounds() const radius = (bounds.mMax.GetY() - bounds.mMin.GetY()) / 2.0 @@ -592,8 +628,11 @@ class PhysicsSystem extends WorldSystem { vehicleSettings.mWheels.clear() vehicleSettings.mWheels.push_back(wheelSettings) + // Other than maxTorque, these controller settings are not being used as of now + // because ArcadeDriveBehavior goes directly to the WheelDrivers. + // maxTorque is only used as communication for WheelDriver to get maxAcceleration const controllerSettings = new JOLT.WheeledVehicleControllerSettings() - controllerSettings.mEngine.mMaxTorque = 1500.0 + controllerSettings.mEngine.mMaxTorque = maxAcc controllerSettings.mTransmission.mClutchStrength = 10.0 controllerSettings.mTransmission.mGearRatios.clear() controllerSettings.mTransmission.mGearRatios.push_back(2) @@ -611,6 +650,15 @@ class PhysicsSystem extends WorldSystem { const listener = new JOLT.VehicleConstraintStepListener(vehicleConstraint) this._joltPhysSystem.AddStepListener(listener) + // const callbacks = new JOLT.VehicleConstraintCallbacksJS() + // callbacks.GetCombinedFriction = (_wheelIndex, _tireFrictionDirection, tireFriction, _body2Ptr, _subShapeID2) => { + // return tireFriction + // } + // callbacks.OnPreStepCallback = (_vehicle, _stepContext) => { }; + // callbacks.OnPostCollideCallback = (_vehicle, _stepContext) => { }; + // callbacks.OnPostStepCallback = (_vehicle, _stepContext) => { }; + // callbacks.SetVehicleConstraint(vehicleConstraint) + this._joltPhysSystem.AddConstraint(vehicleConstraint) this._joltPhysSystem.AddConstraint(fixedConstraint) @@ -618,12 +666,221 @@ class PhysicsSystem extends WorldSystem { return [fixedConstraint, vehicleConstraint, listener] } - private IsWheel(jDef: mirabuf.joint.Joint) { - return ( - jDef.info!.name! != "grounded" && - jDef.userData && - (new Map(Object.entries(jDef.userData.data!)).get("wheel") ?? "false") == "true" - ) + private CreateBallConstraint( + jointInstance: mirabuf.joint.JointInstance, + jointDefinition: mirabuf.joint.Joint, + bodyA: Jolt.Body, + bodyB: Jolt.Body, + mechanism: Mechanism + ): void { + const jointOrigin = jointDefinition.origin + ? MirabufVector3_JoltVec3(jointDefinition.origin as mirabuf.Vector3) + : new JOLT.Vec3(0, 0, 0) + // TODO: Offset transformation for robot builder. + const jointOriginOffset = jointInstance.offset + ? MirabufVector3_JoltVec3(jointInstance.offset as mirabuf.Vector3) + : new JOLT.Vec3(0, 0, 0) + + const anchorPoint = jointOrigin.Add(jointOriginOffset) + + const pitchDof = jointDefinition.custom!.dofs!.at(0) + const yawDof = jointDefinition.custom!.dofs!.at(1) + const rollDof = jointDefinition.custom!.dofs!.at(2) + const pitchAxis = new JOLT.Vec3(pitchDof?.axis?.x ?? 0, pitchDof?.axis?.y ?? 0, pitchDof?.axis?.z ?? 0) + const yawAxis = new JOLT.Vec3(yawDof?.axis?.x ?? 0, yawDof?.axis?.y ?? 0, yawDof?.axis?.z ?? 0) + const rollAxis = new JOLT.Vec3(rollDof?.axis?.x ?? 0, rollDof?.axis?.y ?? 0, rollDof?.axis?.z ?? 0) + + const constraints: { axis: Jolt.Vec3; friction: number; value: number; upper?: number; lower?: number }[] = [] + + if (!pitchDof?.limits || (pitchDof.limits.upper ?? 0) - (pitchDof.limits.lower ?? 0) > 0.001) { + constraints.push({ + axis: pitchAxis, + friction: 0.0, + value: pitchDof?.value ?? 0, + upper: pitchDof?.limits ? pitchDof.limits.upper ?? 0 : undefined, + lower: pitchDof?.limits ? pitchDof.limits.lower ?? 0 : undefined, + }) + } + + if (!yawDof?.limits || (yawDof.limits.upper ?? 0) - (yawDof.limits.lower ?? 0) > 0.001) { + constraints.push({ + axis: yawAxis, + friction: 0.0, + value: yawDof?.value ?? 0, + upper: yawDof?.limits ? yawDof.limits.upper ?? 0 : undefined, + lower: yawDof?.limits ? yawDof.limits.lower ?? 0 : undefined, + }) + } + + if (!rollDof?.limits || (rollDof.limits.upper ?? 0) - (rollDof.limits.lower ?? 0) > 0.001) { + constraints.push({ + axis: rollAxis, + friction: 0.0, + value: rollDof?.value ?? 0, + upper: rollDof?.limits ? rollDof.limits.upper ?? 0 : undefined, + lower: rollDof?.limits ? rollDof.limits.lower ?? 0 : undefined, + }) + } + + let bodyStart = bodyB + let bodyNext = bodyA + if (constraints.length > 1) { + bodyNext = this.CreateGhostBody(anchorPoint) + this._joltBodyInterface.AddBody(bodyNext.GetID(), JOLT.EActivation_Activate) + mechanism.ghostBodies.push(bodyNext.GetID()) + } + for (let i = 0; i < constraints.length; ++i) { + const c = constraints[i] + const hingeSettings = new JOLT.HingeConstraintSettings() + hingeSettings.mMaxFrictionTorque = c.friction + hingeSettings.mPoint1 = hingeSettings.mPoint2 = anchorPoint + hingeSettings.mHingeAxis1 = hingeSettings.mHingeAxis2 = c.axis.Normalized() + hingeSettings.mNormalAxis1 = hingeSettings.mNormalAxis2 = getPerpendicular(hingeSettings.mHingeAxis1) + if (c.upper && c.lower) { + // Some values that are meant to be exactly PI are perceived as being past it, causing unexpected behavior. + // This safety check caps the values to be within [-PI, PI] wth minimal difference in precision. + const piSafetyCheck = (v: number) => Math.min(3.14158, Math.max(-3.14158, v)) + + const currentPos = piSafetyCheck(c.value) + const upper = piSafetyCheck(c.upper) - currentPos + const lower = piSafetyCheck(c.lower) - currentPos + + hingeSettings.mLimitsMin = -upper + hingeSettings.mLimitsMax = -lower + } + + const hingeConstraint = hingeSettings.Create(bodyStart, bodyNext) + this._joltPhysSystem.AddConstraint(hingeConstraint) + this._constraints.push(hingeConstraint) + bodyStart = bodyNext + if (i == constraints.length - 2) { + bodyNext = bodyA + } else { + bodyNext = this.CreateGhostBody(anchorPoint) + this._joltBodyInterface.AddBody(bodyNext.GetID(), JOLT.EActivation_Activate) + mechanism.ghostBodies.push(bodyNext.GetID()) + } + } + } + + // TODO: Ball socket joints should try to be reduced to the shoulder joint equivalent for Jolt (SwingTwistConstraint) + // private CreateBallBadAgainConstraint( + // jointInstance: mirabuf.joint.JointInstance, + // jointDefinition: mirabuf.joint.Joint, + // bodyA: Jolt.Body, + // bodyB: Jolt.Body, + // mechanism: Mechanism, + // ): void { + + // const jointOrigin = jointDefinition.origin + // ? MirabufVector3_JoltVec3(jointDefinition.origin as mirabuf.Vector3) + // : new JOLT.Vec3(0, 0, 0) + // // TODO: Offset transformation for robot builder. + // const jointOriginOffset = jointInstance.offset + // ? MirabufVector3_JoltVec3(jointInstance.offset as mirabuf.Vector3) + // : new JOLT.Vec3(0, 0, 0) + + // const anchorPoint = jointOrigin.Add(jointOriginOffset) + + // const pitchDof = jointDefinition.custom!.dofs!.at(0) + // const yawDof = jointDefinition.custom!.dofs!.at(1) + // const rollDof = jointDefinition.custom!.dofs!.at(2) + // const pitchAxis = new JOLT.Vec3(pitchDof?.axis?.x ?? 0, pitchDof?.axis?.y ?? 0, pitchDof?.axis?.z ?? 0) + // const yawAxis = new JOLT.Vec3(yawDof?.axis?.x ?? 0, yawDof?.axis?.y ?? 0, yawDof?.axis?.z ?? 0) + // const rollAxis = new JOLT.Vec3(rollDof?.axis?.x ?? 0, rollDof?.axis?.y ?? 0, rollDof?.axis?.z ?? 0) + + // console.debug(`Anchor Point: ${joltVec3ToString(anchorPoint)}`) + // console.debug(`Pitch Axis: ${joltVec3ToString(pitchAxis)} ${pitchDof?.limits ? `[${pitchDof.limits.lower!.toFixed(3)}, ${pitchDof.limits.upper!.toFixed(3)}]` : ''}`) + // console.debug(`Yaw Axis: ${joltVec3ToString(yawAxis)} ${yawDof?.limits ? `[${yawDof.limits.lower!.toFixed(3)}, ${yawDof.limits.upper!.toFixed(3)}]` : ''}`) + // console.debug(`Roll Axis: ${joltVec3ToString(rollAxis)} ${rollDof?.limits ? `[${rollDof.limits.lower!.toFixed(3)}, ${rollDof.limits.upper!.toFixed(3)}]` : ''}`) + + // const constraints: { axis: Jolt.Vec3, friction: number, value: number, upper?: number, lower?: number }[] = [] + + // if (pitchDof?.limits && (pitchDof.limits.upper ?? 0) - (pitchDof.limits.lower ?? 0) < 0.001) { + // console.debug('Pitch Fixed') + // } else { + // constraints.push({ + // axis: pitchAxis, + // friction: 0.0, + // value: pitchDof?.value ?? 0, + // upper: pitchDof?.limits ? pitchDof.limits.upper ?? 0 : undefined, + // lower: pitchDof?.limits ? pitchDof.limits.lower ?? 0 : undefined + // }) + // } + + // if (yawDof?.limits && (yawDof.limits.upper ?? 0) - (yawDof.limits.lower ?? 0) < 0.001) { + // console.debug('Yaw Fixed') + // } else { + // constraints.push({ + // axis: yawAxis, + // friction: 0.0, + // value: yawDof?.value ?? 0, + // upper: yawDof?.limits ? yawDof.limits.upper ?? 0 : undefined, + // lower: yawDof?.limits ? yawDof.limits.lower ?? 0 : undefined + // }) + // } + + // if (rollDof?.limits && (rollDof.limits.upper ?? 0) - (rollDof.limits.lower ?? 0) < 0.001) { + // console.debug('Roll Fixed') + // } else { + // constraints.push({ + // axis: rollAxis, + // friction: 0.0, + // value: rollDof?.value ?? 0, + // upper: rollDof?.limits ? rollDof.limits.upper ?? 0 : undefined, + // lower: rollDof?.limits ? rollDof.limits.lower ?? 0 : undefined + // }) + // } + + // let bodyStart = bodyB + // let bodyNext = bodyA + // if (constraints.length > 1) { + // console.debug('Starting with Ghost Body') + // bodyNext = this.CreateGhostBody(anchorPoint) + // this._joltBodyInterface.AddBody(bodyNext.GetID(), JOLT.EActivation_Activate) + // mechanism.ghostBodies.push(bodyNext.GetID()) + // } + // for (let i = 0; i < constraints.length; ++i) { + // console.debug(`Constraint ${i}`) + // const c = constraints[i] + // const hingeSettings = new JOLT.HingeConstraintSettings() + // hingeSettings.mMaxFrictionTorque = c.friction; + // hingeSettings.mPoint1 = hingeSettings.mPoint2 = anchorPoint + // hingeSettings.mHingeAxis1 = hingeSettings.mHingeAxis2 = c.axis.Normalized() + // hingeSettings.mNormalAxis1 = hingeSettings.mNormalAxis2 = getPerpendicular( + // hingeSettings.mHingeAxis1 + // ) + // if (c.upper && c.lower) { + // // Some values that are meant to be exactly PI are perceived as being past it, causing unexpected behavior. + // // This safety check caps the values to be within [-PI, PI] wth minimal difference in precision. + // const piSafetyCheck = (v: number) => Math.min(3.14158, Math.max(-3.14158, v)) + + // const currentPos = piSafetyCheck(c.value) + // const upper = piSafetyCheck(c.upper) - currentPos + // const lower = piSafetyCheck(c.lower) - currentPos + + // hingeSettings.mLimitsMin = -upper + // hingeSettings.mLimitsMax = -lower + // } + + // const hingeConstraint = hingeSettings.Create(bodyStart, bodyNext) + // this._joltPhysSystem.AddConstraint(hingeConstraint) + // this._constraints.push(hingeConstraint) + // bodyStart = bodyNext + // if (i == constraints.length - 2) { + // bodyNext = bodyA + // console.debug('Finishing with Body A') + // } else { + // console.debug('New Ghost Body') + // bodyNext = this.CreateGhostBody(anchorPoint) + // this._joltBodyInterface.AddBody(bodyNext.GetID(), JOLT.EActivation_Activate) + // mechanism.ghostBodies.push(bodyNext.GetID()) + // } + // } + // } + + private IsWheel(jDef: mirabuf.joint.Joint): boolean { + return (jDef.info?.name !== "grounded" && (jDef.userData?.data?.wheel ?? "false") === "true") ?? false } /** @@ -641,11 +898,28 @@ class PhysicsSystem extends WorldSystem { const reservedLayer: number | undefined = layerReserve?.layer - filterNonPhysicsNodes([...parser.rigidNodes.values()], parser.assembly).forEach(rn => { + const nonPhysicsNodes = filterNonPhysicsNodes([...parser.rigidNodes.values()], parser.assembly) + + const massMod = (() => { + let assemblyMass = 0 + nonPhysicsNodes.forEach(x => (assemblyMass += x.mass)) + + return parser.assembly.dynamic && assemblyMass > MAX_ROBOT_MASS ? MAX_ROBOT_MASS / assemblyMass : 1 + })() + + nonPhysicsNodes.forEach(rn => { const compoundShapeSettings = new JOLT.StaticCompoundShapeSettings() let shapesAdded = 0 let totalMass = 0 + + type FrictionPairing = { + dynamic: number + static: number + weight: number + } + const frictionAccum: FrictionPairing[] = [] + const comAccum = new mirabuf.Vector3() const minBounds = new JOLT.Vec3(1000000.0, 1000000.0, 1000000.0) @@ -659,49 +933,80 @@ class PhysicsSystem extends WorldSystem { rn.parts.forEach(partId => { const partInstance = parser.assembly.data!.parts!.partInstances![partId]! - if ( - partInstance.skipCollider == null || - partInstance == undefined || - partInstance.skipCollider == false - ) { - const partDefinition = - parser.assembly.data!.parts!.partDefinitions![partInstance.partDefinitionReference!]! - - const partShapeResult = rn.isDynamic - ? this.CreateConvexShapeSettingsFromPart(partDefinition) - : this.CreateConcaveShapeSettingsFromPart(partDefinition) - - if (partShapeResult) { - const [shapeSettings, partMin, partMax] = partShapeResult - - const transform = ThreeMatrix4_JoltMat44(parser.globalTransforms.get(partId)!) - const translation = transform.GetTranslation() - const rotation = transform.GetQuaternion() - compoundShapeSettings.AddShape(translation, rotation, shapeSettings, 0) - shapesAdded++ - - this.UpdateMinMaxBounds(transform.Multiply3x3(partMin), minBounds, maxBounds) - this.UpdateMinMaxBounds(transform.Multiply3x3(partMax), minBounds, maxBounds) - - JOLT.destroy(partMin) - JOLT.destroy(partMax) - JOLT.destroy(transform) - - if ( - partDefinition.physicalData && - partDefinition.physicalData.com && - partDefinition.physicalData.mass - ) { - const mass = partDefinition.massOverride - ? partDefinition.massOverride! - : partDefinition.physicalData.mass! - totalMass += mass - comAccum.x += (partDefinition.physicalData.com.x! * mass) / 100.0 - comAccum.y += (partDefinition.physicalData.com.y! * mass) / 100.0 - comAccum.z += (partDefinition.physicalData.com.z! * mass) / 100.0 - } + if (partInstance.skipCollider) return + + const partDefinition = + parser.assembly.data!.parts!.partDefinitions![partInstance.partDefinitionReference!]! + + const partShapeResult = rn.isDynamic + ? this.CreateConvexShapeSettingsFromPart(partDefinition) + : this.CreateConcaveShapeSettingsFromPart(partDefinition) + // const partShapeResult = this.CreateConvexShapeSettingsFromPart(partDefinition) + + if (!partShapeResult) return + + const [shapeSettings, partMin, partMax] = partShapeResult + + const transform = ThreeMatrix4_JoltMat44(parser.globalTransforms.get(partId)!) + const translation = transform.GetTranslation() + const rotation = transform.GetQuaternion() + compoundShapeSettings.AddShape(translation, rotation, shapeSettings, 0) + shapesAdded++ + + this.UpdateMinMaxBounds(transform.Multiply3x3(partMin), minBounds, maxBounds) + this.UpdateMinMaxBounds(transform.Multiply3x3(partMax), minBounds, maxBounds) + + JOLT.destroy(partMin) + JOLT.destroy(partMax) + JOLT.destroy(transform) + + const physicalMaterial = + parser.assembly.data!.materials!.physicalMaterials![ + partInstance.physicalMaterial ?? DEFAULT_PHYSICAL_MATERIAL_KEY + ] + + if (physicalMaterial) { + let frictionOverride: number | undefined = + partDefinition?.frictionOverride == null ? undefined : partDefinition?.frictionOverride + if ((partDefinition?.frictionOverride ?? 0.0) < SIGNIFICANT_FRICTION_THRESHOLD) { + frictionOverride = undefined } + + if ( + (physicalMaterial.dynamicFriction ?? 0.0) < SIGNIFICANT_FRICTION_THRESHOLD || + (physicalMaterial.staticFriction ?? 0.0) < SIGNIFICANT_FRICTION_THRESHOLD + ) { + physicalMaterial.dynamicFriction = DEFAULT_FRICTION + physicalMaterial.staticFriction = DEFAULT_FRICTION + } + + // TODO: Consider using roughness as dynamic friction. + const frictionPairing: FrictionPairing = { + dynamic: frictionOverride ?? physicalMaterial.dynamicFriction!, + static: frictionOverride ?? physicalMaterial.staticFriction!, + weight: partDefinition.physicalData?.area ?? 1.0, + } + frictionAccum.push(frictionPairing) + } else { + const frictionPairing: FrictionPairing = { + dynamic: DEFAULT_FRICTION, + static: DEFAULT_FRICTION, + weight: partDefinition.physicalData?.area ?? 1.0, + } + frictionAccum.push(frictionPairing) } + + if (!partDefinition.physicalData?.com || !partDefinition.physicalData.mass) return + + const mass = partDefinition.massOverride + ? partDefinition.massOverride! + : partDefinition.physicalData.mass! + + totalMass += mass + + comAccum.x += (partDefinition.physicalData.com.x! * mass) / 100.0 + comAccum.y += (partDefinition.physicalData.com.y! * mass) / 100.0 + comAccum.z += (partDefinition.physicalData.com.z! * mass) / 100.0 }) if (shapesAdded > 0) { @@ -714,7 +1019,12 @@ class PhysicsSystem extends WorldSystem { const shape = shapeResult.Get() if (rn.isDynamic) { - shape.GetMassProperties().mMass = totalMass == 0.0 ? 1 : totalMass + if (rn.isGamePiece) { + const mass = totalMass == 0.0 ? 1 : Math.min(totalMass, MAX_GP_MASS) + shape.GetMassProperties().mMass = mass + } else { + shape.GetMassProperties().mMass = totalMass == 0.0 ? 1 : totalMass * massMod + } } const bodySettings = new JOLT.BodyCreationSettings( @@ -729,6 +1039,22 @@ class PhysicsSystem extends WorldSystem { body.SetAllowSleeping(false) rnToBodies.set(rn.id, body.GetID()) + // Set Friction Here + let staticFriction = 0.0 + let dynamicFriction = 0.0 + let weightSum = 0.0 + frictionAccum.forEach(pairing => { + staticFriction += pairing.static * pairing.weight + dynamicFriction += pairing.dynamic * pairing.weight + weightSum += pairing.weight + }) + staticFriction /= weightSum == 0.0 ? 1.0 : weightSum + dynamicFriction /= weightSum == 0.0 ? 1.0 : weightSum + + // I guess this is an okay substitute. + const friction = (staticFriction + dynamicFriction) / 2.0 + body.SetFriction(friction) + // Little testing components this._bodies.push(body.GetID()) body.SetRestitution(0.4) @@ -748,7 +1074,7 @@ class PhysicsSystem extends WorldSystem { */ private CreateConvexShapeSettingsFromPart( partDefinition: mirabuf.IPartDefinition - ): [Jolt.ShapeSettings, Jolt.Vec3, Jolt.Vec3] | undefined | null { + ): [Jolt.ShapeSettings, Jolt.Vec3, Jolt.Vec3] | undefined { const settings = new JOLT.ConvexHullShapeSettings() const min = new JOLT.Vec3(1000000.0, 1000000.0, 1000000.0) @@ -756,14 +1082,14 @@ class PhysicsSystem extends WorldSystem { const points = settings.mPoints partDefinition.bodies!.forEach(body => { - if (body.triangleMesh && body.triangleMesh.mesh && body.triangleMesh.mesh.verts) { - const vertArr = body.triangleMesh.mesh.verts - for (let i = 0; i < body.triangleMesh.mesh.verts.length; i += 3) { - const vert = MirabufFloatArr_JoltVec3(vertArr, i) - points.push_back(vert) - this.UpdateMinMaxBounds(vert, min, max) - JOLT.destroy(vert) - } + const verts = body.triangleMesh?.mesh?.verts + if (!verts) return + + for (let i = 0; i < verts.length; i += 3) { + const vert = MirabufFloatArr_JoltVec3(verts, i) + points.push_back(vert) + this.UpdateMinMaxBounds(vert, min, max) + JOLT.destroy(vert) } }) @@ -772,9 +1098,9 @@ class PhysicsSystem extends WorldSystem { JOLT.destroy(min) JOLT.destroy(max) return - } else { - return [settings, min, max] } + + return [settings, min, max] } /** @@ -785,10 +1111,10 @@ class PhysicsSystem extends WorldSystem { */ private CreateConcaveShapeSettingsFromPart( partDefinition: mirabuf.IPartDefinition - ): [Jolt.ShapeSettings, Jolt.Vec3, Jolt.Vec3] | undefined | null { + ): [Jolt.ShapeSettings, Jolt.Vec3, Jolt.Vec3] | undefined { const settings = new JOLT.MeshShapeSettings() - settings.mMaxTrianglesPerLeaf = 8 + settings.mMaxTrianglesPerLeaf = 4 settings.mTriangleVertices = new JOLT.VertexList() settings.mIndexedTriangles = new JOLT.IndexedTriangleList() @@ -803,6 +1129,7 @@ class PhysicsSystem extends WorldSystem { const vertArr = body.triangleMesh?.mesh?.verts const indexArr = body.triangleMesh?.mesh?.indices if (!vertArr || !indexArr) return + for (let i = 0; i < vertArr.length; i += 3) { const vert = MirabufFloatArr_JoltFloat3(vertArr, i) settings.mTriangleVertices.push_back(vert) @@ -821,10 +1148,10 @@ class PhysicsSystem extends WorldSystem { JOLT.destroy(min) JOLT.destroy(max) return - } else { - settings.Sanitize() - return [settings, min, max] } + + settings.Sanitize() + return [settings, min, max] } /** @@ -851,12 +1178,10 @@ class PhysicsSystem extends WorldSystem { .GetNarrowPhaseQuery() .CastRay(ray, raySettings, collector, bp_filter, object_filter, body_filter, shape_filter) - if (collector.HadHit()) { - const hitPoint = ray.GetPointOnRay(collector.mHit.mFraction) - return { data: collector.mHit, point: hitPoint, ray: ray } - } + if (!collector.HadHit()) return undefined - return undefined + const hitPoint = ray.GetPointOnRay(collector.mHit.mFraction) + return { data: collector.mHit, point: hitPoint, ray: ray } } /** @@ -879,7 +1204,7 @@ class PhysicsSystem extends WorldSystem { /** * Destroys bodies. * - * @param bodies Bodies to destroy. + * @param bodies Bodies to destroy. */ public DestroyBodies(...bodies: Jolt.Body[]) { bodies.forEach(x => { @@ -900,11 +1225,15 @@ class PhysicsSystem extends WorldSystem { this._joltPhysSystem.RemoveStepListener(x) }) mech.constraints.forEach(x => { - this._joltPhysSystem.RemoveConstraint(x.constraint) + this._joltPhysSystem.RemoveConstraint(x.primaryConstraint) }) mech.nodeToBody.forEach(x => { this._joltBodyInterface.RemoveBody(x) - // this._joltBodyInterface.DestroyBody(x); + this._joltBodyInterface.DestroyBody(x) + }) + mech.ghostBodies.forEach(x => { + this._joltBodyInterface.RemoveBody(x) + this._joltBodyInterface.DestroyBody(x) }) } @@ -913,7 +1242,7 @@ class PhysicsSystem extends WorldSystem { } public Update(deltaT: number): void { - if (this._pauseCounter > 0) { + if (this._pauseSet.size > 0) { return } @@ -931,7 +1260,10 @@ class PhysicsSystem extends WorldSystem { this._physicsEventQueue = [] } - public Destroy(): void { + /* + * Destroys PhysicsSystem and frees all objects + */ + public Destroy() { this._constraints.forEach(x => { this._joltPhysSystem.RemoveConstraint(x) // JOLT.destroy(x); @@ -947,6 +1279,29 @@ class PhysicsSystem extends WorldSystem { JOLT.destroy(this._joltPhysSystem.GetContactListener()) } + private CreateGhostBody(position: Jolt.Vec3) { + const size = new JOLT.Vec3(0.05, 0.05, 0.05) + const shape = new JOLT.BoxShape(size) + JOLT.destroy(size) + + const rot = new JOLT.Quat(0, 0, 0, 1) + const creationSettings = new JOLT.BodyCreationSettings( + shape, + position, + rot, + JOLT.EMotionType_Dynamic, + LAYER_GHOST + ) + creationSettings.mMassPropertiesOverride.mMass = 0.01 + + const body = this._joltBodyInterface.CreateBody(creationSettings) + JOLT.destroy(rot) + JOLT.destroy(creationSettings) + + this._bodies.push(body.GetID()) + return body + } + /** * Creates a ghost object and a distance constraint that connects it to the given body * The ghost body is part of the LAYER_GHOST which doesn't interact with any other layer @@ -1000,7 +1355,7 @@ class PhysicsSystem extends WorldSystem { * @param id The id of the body * @param position The new position of the body */ - public SetBodyPosition(id: Jolt.BodyID, position: Jolt.Vec3, activate: boolean = true): void { + public SetBodyPosition(id: Jolt.BodyID, position: Jolt.RVec3, activate: boolean = true): void { if (!this.IsBodyAdded(id)) { return } @@ -1013,17 +1368,56 @@ class PhysicsSystem extends WorldSystem { } public SetBodyRotation(id: Jolt.BodyID, rotation: Jolt.Quat, activate: boolean = true): void { + if (!this.IsBodyAdded(id)) return + + this._joltBodyInterface.SetRotation( + id, + rotation, + activate ? JOLT.EActivation_Activate : JOLT.EActivation_DontActivate + ) + } + + public SetBodyPositionAndRotation( + id: Jolt.BodyID, + position: Jolt.RVec3, + rotation: Jolt.Quat, + activate: boolean = true + ): void { if (!this.IsBodyAdded(id)) { return } - this._joltBodyInterface.SetRotation( + this._joltBodyInterface.SetPositionAndRotation( id, + position, rotation, activate ? JOLT.EActivation_Activate : JOLT.EActivation_DontActivate ) } + public SetBodyPositionRotationAndVelocity( + id: Jolt.BodyID, + position: Jolt.RVec3, + rotation: Jolt.Quat, + linear: Jolt.Vec3, + angular: Jolt.Vec3, + activate: boolean = true + ): void { + if (!this.IsBodyAdded(id)) { + return + } + + this._joltBodyInterface.SetPositionAndRotation( + id, + position, + rotation, + activate ? JOLT.EActivation_Activate : JOLT.EActivation_DontActivate + ) + + this._joltBodyInterface.SetLinearVelocity(id, linear) + this._joltBodyInterface.SetAngularVelocity(id, angular) + } + /** * Exposes SetShape method on the _joltBodyInterface * Sets the shape of the body @@ -1039,9 +1433,7 @@ class PhysicsSystem extends WorldSystem { massProperties: boolean, activationMode: Jolt.EActivation ): void { - if (!this.IsBodyAdded(id)) { - return - } + if (!this.IsBodyAdded(id)) return this._joltBodyInterface.SetShape(id, shape, massProperties, activationMode) } @@ -1131,10 +1523,10 @@ export class LayerReserve { } public Release(): void { - if (!this._isReleased) { - RobotLayers.push(this._layer) - this._isReleased = true - } + if (this._isReleased) return + + RobotLayers.push(this._layer) + this._isReleased = true } } @@ -1149,12 +1541,13 @@ function SetupCollisionFiltering(settings: Jolt.JoltSettings) { // Enable Field layer collisions objectFilter.EnableCollision(LAYER_GENERAL_DYNAMIC, LAYER_GENERAL_DYNAMIC) objectFilter.EnableCollision(LAYER_FIELD, LAYER_GENERAL_DYNAMIC) - for (let i = 0; i < RobotLayers.length; i++) { - objectFilter.EnableCollision(LAYER_FIELD, RobotLayers[i]) - objectFilter.EnableCollision(LAYER_GENERAL_DYNAMIC, RobotLayers[i]) - } + RobotLayers.forEach(layer => { + objectFilter.EnableCollision(LAYER_FIELD, layer) + objectFilter.EnableCollision(LAYER_GENERAL_DYNAMIC, layer) + }) // Enable Collisions between other robots + for (let i = 0; i < RobotLayers.length - 1; i++) { for (let j = i + 1; j < RobotLayers.length; j++) { objectFilter.EnableCollision(RobotLayers[i], RobotLayers[j]) @@ -1164,10 +1557,7 @@ function SetupCollisionFiltering(settings: Jolt.JoltSettings) { const BP_LAYER_FIELD = new JOLT.BroadPhaseLayer(LAYER_FIELD) const BP_LAYER_GENERAL_DYNAMIC = new JOLT.BroadPhaseLayer(LAYER_GENERAL_DYNAMIC) - const bpRobotLayers = new Array(RobotLayers.length) - for (let i = 0; i < bpRobotLayers.length; i++) { - bpRobotLayers[i] = new JOLT.BroadPhaseLayer(RobotLayers[i]) - } + const bpRobotLayers = RobotLayers.map(layer => new JOLT.BroadPhaseLayer(layer)) const COUNT_BROAD_PHASE_LAYERS = 2 + RobotLayers.length @@ -1175,9 +1565,9 @@ function SetupCollisionFiltering(settings: Jolt.JoltSettings) { bpInterface.MapObjectToBroadPhaseLayer(LAYER_FIELD, BP_LAYER_FIELD) bpInterface.MapObjectToBroadPhaseLayer(LAYER_GENERAL_DYNAMIC, BP_LAYER_GENERAL_DYNAMIC) - for (let i = 0; i < bpRobotLayers.length; i++) { - bpInterface.MapObjectToBroadPhaseLayer(RobotLayers[i], bpRobotLayers[i]) - } + bpRobotLayers.forEach((bpRobot, i) => { + bpInterface.MapObjectToBroadPhaseLayer(RobotLayers[i], bpRobot) + }) settings.mObjectLayerPairFilter = objectFilter settings.mBroadPhaseLayerInterface = bpInterface @@ -1207,9 +1597,7 @@ function getPerpendicular(vec: Jolt.Vec3): Jolt.Vec3 { } function tryGetPerpendicular(vec: Jolt.Vec3, toCheck: Jolt.Vec3): Jolt.Vec3 | undefined { - if (Math.abs(Math.abs(vec.Dot(toCheck)) - 1.0) < 0.0001) { - return undefined - } + if (Math.abs(Math.abs(vec.Dot(toCheck)) - 1.0) < 0.0001) return undefined const a = vec.Dot(toCheck) return new JOLT.Vec3( diff --git a/fission/src/systems/preferences/PreferenceTypes.ts b/fission/src/systems/preferences/PreferenceTypes.ts index 2520405fa1..a1ea8ac352 100644 --- a/fission/src/systems/preferences/PreferenceTypes.ts +++ b/fission/src/systems/preferences/PreferenceTypes.ts @@ -1,8 +1,9 @@ +import { SimConfigData } from "@/ui/panels/simulation/SimConfigShared" import { InputScheme } from "../input/InputSchemeManager" import { Vector3Tuple } from "three" +/** Names of all global preferences. */ export type GlobalPreference = - | "ScreenMode" | "QualitySettings" | "ZoomSensitivity" | "PitchSensitivity" @@ -13,13 +14,18 @@ export type GlobalPreference = | "InputSchemes" | "RenderSceneTags" | "RenderScoreboard" + | "SubsystemGravity" + | "SimAutoReconnect" export const RobotPreferencesKey: string = "Robots" export const FieldPreferencesKey: string = "Fields" +/** + * Default values for GlobalPreferences as a fallback if they are not configured by the user. + * Every global preference should have a default value. + */ export const DefaultGlobalPreferences: { [key: string]: unknown } = { - ScreenMode: "Windowed", - QualitySettings: "High", + QualitySettings: "High" as QualitySetting, ZoomSensitivity: 15, PitchSensitivity: 10, YawSensitivity: 3, @@ -29,8 +35,12 @@ export const DefaultGlobalPreferences: { [key: string]: unknown } = { InputSchemes: [], RenderSceneTags: true, RenderScoreboard: true, + SubsystemGravity: false, + SimAutoReconnect: false, } +export type QualitySetting = "Low" | "Medium" | "High" + export type IntakePreferences = { deltaTransformation: number[] zoneDiameter: number @@ -43,10 +53,42 @@ export type EjectorPreferences = { parentNode: string | undefined } +/** The behavior types that can be sequenced. */ +export type BehaviorType = "Elevator" | "Arm" + +/** Data for sequencing and inverting elevator and behaviors. */ +export type SequentialBehaviorPreferences = { + jointIndex: number + parentJointIndex: number | undefined + type: BehaviorType + inverted: boolean +} + +/** Default preferences for a joint with not parent specified and inverted set to false. */ +export function DefaultSequentialConfig(index: number, type: BehaviorType): SequentialBehaviorPreferences { + return { + jointIndex: index, + parentJointIndex: undefined, + type: type, + inverted: false, + } +} + export type RobotPreferences = { inputsSchemes: InputScheme[] + motors: MotorPreferences[] intake: IntakePreferences ejector: EjectorPreferences + driveVelocity: number + driveAcceleration: number + sequentialConfig?: SequentialBehaviorPreferences[] + simConfig?: SimConfigData +} + +export type MotorPreferences = { + name: string + maxVelocity: number + maxForce: number } export type Alliance = "red" | "blue" @@ -63,6 +105,7 @@ export type ScoringZonePreferences = { } export type FieldPreferences = { + // TODO: implement this defaultSpawnLocation: Vector3Tuple scoringZones: ScoringZonePreferences[] } @@ -70,6 +113,7 @@ export type FieldPreferences = { export function DefaultRobotPreferences(): RobotPreferences { return { inputsSchemes: [], + motors: [], intake: { deltaTransformation: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], zoneDiameter: 0.5, @@ -80,6 +124,8 @@ export function DefaultRobotPreferences(): RobotPreferences { ejectorVelocity: 1, parentNode: undefined, }, + driveVelocity: 0, + driveAcceleration: 0, } } diff --git a/fission/src/systems/preferences/PreferencesSystem.ts b/fission/src/systems/preferences/PreferencesSystem.ts index 8933a44bd6..74a7d24607 100644 --- a/fission/src/systems/preferences/PreferencesSystem.ts +++ b/fission/src/systems/preferences/PreferencesSystem.ts @@ -9,10 +9,15 @@ import { RobotPreferencesKey, } from "./PreferenceTypes" +/** An event that's triggered when a preference is changed. */ export class PreferenceEvent extends Event { public prefName: GlobalPreference public prefValue: unknown + /** + * @param {GlobalPreference} prefName - The name of the preference that has just been updated. + * @param {unknown} prefValue - The new value this preference was set to. + */ constructor(prefName: GlobalPreference, prefValue: unknown) { super("preferenceChanged") this.prefName = prefName @@ -20,6 +25,7 @@ export class PreferenceEvent extends Event { } } +/** The preference system handles loading, saving, and updating all user managed data saved in local storage. */ class PreferencesSystem { private static _preferences: { [key: string]: unknown } private static _localStorageKey = "Preferences" @@ -29,14 +35,6 @@ class PreferencesSystem { window.addEventListener("preferenceChanged", callback as EventListener) } - /** Sets a global preference to be a value of a specific type */ - public static setGlobalPreference(key: GlobalPreference, value: T) { - if (this._preferences == undefined) this.loadPreferences() - - window.dispatchEvent(new PreferenceEvent(key, value)) - this._preferences[key] = value - } - /** Gets any preference from the preferences map */ private static getPreference(key: string): T | undefined { if (this._preferences == undefined) this.loadPreferences() @@ -44,7 +42,12 @@ class PreferencesSystem { return this._preferences[key] as T } - /** Gets a global preference, or it's default value if it does not exist in the preferences map */ + /** + * Gets a global preference, or it's default value if it does not exist in the preferences map + * + * @param {GlobalPreference} key - The name of the preference to get. + * @returns {T} The value of this preference casted to type T. + */ public static getGlobalPreference(key: GlobalPreference): T { const customPref = this.getPreference(key) if (customPref != undefined) return customPref @@ -55,7 +58,23 @@ class PreferencesSystem { throw new Error("Preference '" + key + "' is not assigned a default!") } - /** Gets a RobotPreferences object for a robot of a specific mira name */ + /** + * Sets a global preference to be a value of a specific type + * + * @param {GlobalPreference} key - The name of the preference to set. + * @param {T} value - The value to set the preference to. + */ + public static setGlobalPreference(key: GlobalPreference, value: T) { + if (this._preferences == undefined) this.loadPreferences() + + window.dispatchEvent(new PreferenceEvent(key, value)) + this._preferences[key] = value + } + + /** + * @param {string} miraName - The name of the robot assembly to get preference for. + * @returns {RobotPreferences} Robot preferences found for the given robot, or default robot preferences if none are found. + */ public static getRobotPreferences(miraName: string): RobotPreferences { const allRoboPrefs = this.getAllRobotPreferences() @@ -68,7 +87,13 @@ class PreferencesSystem { return allRoboPrefs[miraName] } - /** Gets preferences for every robot in local storage */ + /** Sets the RobotPreferences object for the robot of a specific mira name */ + public static setRobotPreferences(miraName: string, value: RobotPreferences) { + const allRoboPrefs = this.getAllRobotPreferences() + allRoboPrefs[miraName] = value + } + + /** @returns Preferences for every robot that was found in local storage. */ public static getAllRobotPreferences(): { [key: string]: RobotPreferences } { let allRoboPrefs = this.getPreference<{ [key: string]: RobotPreferences }>(RobotPreferencesKey) @@ -80,7 +105,10 @@ class PreferencesSystem { return allRoboPrefs } - /** Gets a FieldPreferences object for a robot of a specific mira name */ + /** + * @param {string} miraName - The name of the field assembly to get preference for. + * @returns {FieldPreferences} Field preferences found for the given field, or default field preferences if none are found. + */ public static getFieldPreferences(miraName: string): FieldPreferences { const allFieldPrefs = this.getAllFieldPreferences() @@ -93,7 +121,7 @@ class PreferencesSystem { return allFieldPrefs[miraName] } - /** Gets preferences for every robot in local storage */ + /** @returns Preferences for every field that was found in local storage. */ public static getAllFieldPreferences(): { [key: string]: FieldPreferences } { let allFieldPrefs = this.getPreference<{ [key: string]: FieldPreferences }>(FieldPreferencesKey) @@ -105,7 +133,7 @@ class PreferencesSystem { return allFieldPrefs } - /** Load all preferences from local storage */ + /** Loads all preferences from local storage. */ public static loadPreferences() { const loadedPrefs = window.localStorage.getItem(this._localStorageKey) @@ -122,7 +150,7 @@ class PreferencesSystem { } } - /** Save all preferences to local storage */ + /** Saves all preferences to local storage. */ public static savePreferences() { if (this._preferences == undefined) { console.log("Preferences not loaded!") @@ -139,7 +167,7 @@ class PreferencesSystem { window.localStorage.setItem(this._localStorageKey, prefsString) } - /** Remove all preferences from local storage */ + /** Removes all preferences from local storage. */ public static clearPreferences() { window.localStorage.removeItem(this._localStorageKey) this._preferences = {} diff --git a/fission/src/systems/scene/CameraControls.ts b/fission/src/systems/scene/CameraControls.ts new file mode 100644 index 0000000000..fb1ac41500 --- /dev/null +++ b/fission/src/systems/scene/CameraControls.ts @@ -0,0 +1,234 @@ +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import * as THREE from "three" +import ScreenInteractionHandler, { + InteractionEnd, + InteractionMove, + InteractionStart, + PRIMARY_MOUSE_INTERACTION, + SECONDARY_MOUSE_INTERACTION, +} from "./ScreenInteractionHandler" + +export type CameraControlsType = "Orbit" + +export abstract class CameraControls { + private _controlsType: CameraControlsType + + public abstract set enabled(val: boolean) + public abstract get enabled(): boolean + + public get controlsType() { + return this._controlsType + } + + public constructor(controlsType: CameraControlsType) { + this._controlsType = controlsType + } + + public abstract update(deltaT: number): void + + public abstract dispose(): void +} + +interface SphericalCoords { + theta: number + phi: number + r: number +} + +type PointerType = -1 | 0 | 1 | 2 + +const CO_MAX_ZOOM = 40.0 +const CO_MIN_ZOOM = 0.1 +const CO_MAX_PHI = Math.PI / 2.1 +const CO_MIN_PHI = -Math.PI / 2.1 + +const CO_SENSITIVITY_ZOOM = 4.0 +const CO_SENSITIVITY_PHI = 0.5 +const CO_SENSITIVITY_THETA = 0.5 + +const CO_DEFAULT_ZOOM = 3.5 +const CO_DEFAULT_PHI = -Math.PI / 6.0 +const CO_DEFAULT_THETA = -Math.PI / 4.0 + +const DEG2RAD = Math.PI / 180.0 + +/** + * Creates a pseudo frustum of the perspective camera to scale the mouse movement to something relative to the scenes dimensions and scale + * + * @param camera Main Camera + * @param distanceFromFocus Distance from the focus point + * @param originalMovement Original movement of the mouse across the screen + * @returns Augmented movement to scale to the scenes relative dimensions + */ +function augmentMovement( + camera: THREE.Camera, + distanceFromFocus: number, + originalMovement: [number, number] +): [number, number] { + const aspect = (camera as THREE.PerspectiveCamera)?.aspect ?? 1.0 + // const aspect = 1.0 + const fov: number | undefined = (camera as THREE.PerspectiveCamera)?.getEffectiveFOV() + if (fov) { + const res: [number, number] = [ + (2 * + distanceFromFocus * + Math.tan(Math.min((Math.PI * 0.9) / 2, (DEG2RAD * fov * aspect) / 2)) * + originalMovement[0]) / + window.innerWidth, + (2 * distanceFromFocus * Math.tan((DEG2RAD * fov) / 2) * originalMovement[1]) / window.innerHeight, + ] + return res + } else { + return originalMovement + } +} + +export class CustomOrbitControls extends CameraControls { + private _enabled = true + + private _mainCamera: THREE.Camera + + private _activePointerType: PointerType + private _nextCoords: SphericalCoords + private _coords: SphericalCoords + private _focus: THREE.Matrix4 + + private _focusProvider: MirabufSceneObject | undefined + public locked: boolean + + private _interactionHandler: ScreenInteractionHandler + + public set enabled(val: boolean) { + this._enabled = val + } + public get enabled(): boolean { + return this._enabled + } + + public set focusProvider(provider: MirabufSceneObject | undefined) { + this._focusProvider = provider + } + public get focusProvider() { + return this._focusProvider + } + + public constructor(mainCamera: THREE.Camera, interactionHandler: ScreenInteractionHandler) { + super("Orbit") + + this._mainCamera = mainCamera + this._interactionHandler = interactionHandler + + this.locked = false + + this._nextCoords = { theta: CO_DEFAULT_THETA, phi: CO_DEFAULT_PHI, r: CO_DEFAULT_ZOOM } + this._coords = { theta: CO_DEFAULT_THETA, phi: CO_DEFAULT_PHI, r: CO_DEFAULT_ZOOM } + this._activePointerType = -1 + + // Identity + this._focus = new THREE.Matrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) + + this._interactionHandler.interactionStart = e => this.interactionStart(e) + this._interactionHandler.interactionEnd = e => this.interactionEnd(e) + this._interactionHandler.interactionMove = e => this.interactionMove(e) + } + + public interactionEnd(end: InteractionEnd) { + /** + * If Pointer is already down, and the button that is being + * released is the primary button, make Pointer not be down + */ + if (end.interactionType == this._activePointerType) { + this._activePointerType = -1 + } + } + + public interactionStart(start: InteractionStart) { + // If primary button, make Pointer be down + if (this._activePointerType < start.interactionType) { + switch (start.interactionType) { + case PRIMARY_MOUSE_INTERACTION: + this._activePointerType = PRIMARY_MOUSE_INTERACTION + break + case SECONDARY_MOUSE_INTERACTION: + this._activePointerType = SECONDARY_MOUSE_INTERACTION + break + default: + break + } + } + } + + public interactionMove(move: InteractionMove) { + if (move.movement) { + if (this._activePointerType == PRIMARY_MOUSE_INTERACTION) { + // Add the movement of the mouse to the _currentPos + this._nextCoords.theta -= move.movement[0] + this._nextCoords.phi -= move.movement[1] + } else if (this._activePointerType == SECONDARY_MOUSE_INTERACTION && !this.locked) { + this._focusProvider = undefined + + const orientation = new THREE.Quaternion().setFromEuler(this._mainCamera.rotation) + + const augmentedMovement = augmentMovement(this._mainCamera, this._coords.r, [ + move.movement[0], + move.movement[1], + ]) + + const pan = new THREE.Vector3(-augmentedMovement[0], augmentedMovement[1], 0).applyQuaternion( + orientation + ) + const newPos = new THREE.Vector3().setFromMatrixPosition(this._focus) + newPos.add(pan) + this._focus.setPosition(newPos) + } + } + + if (move.scale) { + this._nextCoords.r += move.scale + } + } + + public update(deltaT: number): void { + deltaT = Math.max(1.0 / 60.0, Math.min(1 / 144.0, deltaT)) + + if (this.enabled) this._focusProvider?.LoadFocusTransform(this._focus) + + // Generate delta of spherical coordinates + const omega: SphericalCoords = this.enabled + ? { + theta: this._nextCoords.theta - this._coords.theta, + phi: this._nextCoords.phi - this._coords.phi, + r: this._nextCoords.r - this._coords.r, + } + : { theta: 0, phi: 0, r: 0 } + + this._coords.theta += omega.theta * deltaT * CO_SENSITIVITY_THETA + this._coords.phi += omega.phi * deltaT * CO_SENSITIVITY_PHI + this._coords.r += omega.r * deltaT * CO_SENSITIVITY_ZOOM * Math.pow(this._coords.r, 1.4) + + this._coords.phi = Math.min(CO_MAX_PHI, Math.max(CO_MIN_PHI, this._coords.phi)) + this._coords.r = Math.min(CO_MAX_ZOOM, Math.max(CO_MIN_ZOOM, this._coords.r)) + + const deltaTransform = new THREE.Matrix4() + .makeTranslation(0, 0, this._coords.r) + .premultiply( + new THREE.Matrix4().makeRotationFromEuler( + new THREE.Euler(this._coords.phi, this._coords.theta, 0, "YXZ") + ) + ) + + if (this.locked && this._focusProvider) { + deltaTransform.premultiply(this._focus) + } else { + const focusPosition = new THREE.Matrix4().copyPosition(this._focus) + deltaTransform.premultiply(focusPosition) + } + + this._mainCamera.position.setFromMatrixPosition(deltaTransform) + this._mainCamera.rotation.setFromRotationMatrix(deltaTransform) + + this._nextCoords = { theta: this._coords.theta, phi: this._coords.phi, r: this._coords.r } + } + + public dispose(): void {} +} diff --git a/fission/src/systems/scene/GizmoSceneObject.ts b/fission/src/systems/scene/GizmoSceneObject.ts new file mode 100644 index 0000000000..44dede21b8 --- /dev/null +++ b/fission/src/systems/scene/GizmoSceneObject.ts @@ -0,0 +1,254 @@ +import * as THREE from "three" +import SceneObject from "./SceneObject" +import { TransformControls } from "three/examples/jsm/controls/TransformControls.js" +import InputSystem from "../input/InputSystem" +import World from "../World" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import { Object3D, PerspectiveCamera } from "three" +import { ThreeQuaternion_JoltQuat, JoltMat44_ThreeMatrix4, ThreeVector3_JoltVec3 } from "@/util/TypeConversions" +import { RigidNodeId } from "@/mirabuf/MirabufParser" + +export type GizmoMode = "translate" | "rotate" | "scale" + +class GizmoSceneObject extends SceneObject { + private _gizmo: TransformControls + private _obj: Object3D + private _forceUpdate: boolean = false + + private _parentObject?: MirabufSceneObject + private _relativeTransformations?: Map + + private _mainCamera: PerspectiveCamera + + private _size: number + + /** @returns the instance of the transform gizmo itself */ + public get gizmo() { + return this._gizmo + } + + /** @returns Object3D that is attached to transform gizmo */ + public get obj() { + return this._obj + } + + /** @returns true if gizmo is currently being dragged */ + public get isDragging() { + return this._gizmo.dragging + } + + /** @returns the id of the parent scene object */ + public get parentObjectId() { + return this._parentObject?.id + } + + public constructor( + mode: GizmoMode, + size: number, + obj?: THREE.Mesh, + parentObject?: MirabufSceneObject, + postGizmoCreation?: (gizmo: GizmoSceneObject) => void + ) { + super() + + this._obj = obj ?? new THREE.Mesh() + this._parentObject = parentObject + this._mainCamera = World.SceneRenderer.mainCamera + + this._size = size + + this._gizmo = new TransformControls(World.SceneRenderer.mainCamera, World.SceneRenderer.renderer.domElement) + this._gizmo.setMode(mode) + + World.SceneRenderer.RegisterGizmoSceneObject(this) + + postGizmoCreation?.(this) + + if (this._parentObject) { + this._relativeTransformations = new Map() + const gizmoTransformInv = this._obj.matrix.clone().invert() + + /** Due to the limited math functionality exposed to JS for Jolt, we need everything in ThreeJS. */ + this._parentObject.mirabufInstance.parser.rigidNodes.forEach(rn => { + const jBodyId = this._parentObject!.mechanism.GetBodyByNodeId(rn.id) + if (!jBodyId) return + + const worldTransform = JoltMat44_ThreeMatrix4(World.PhysicsSystem.GetBody(jBodyId).GetWorldTransform()) + const relativeTransform = worldTransform.premultiply(gizmoTransformInv) + this._relativeTransformations!.set(rn.id, relativeTransform) + }) + } + } + + public Setup(): void { + // adding the mesh and gizmo to the scene + World.SceneRenderer.AddObject(this._obj) + World.SceneRenderer.AddObject(this._gizmo) + + // forcing the gizmo to rotate and transform with the object + this._gizmo.setSpace("local") + this._gizmo.attach(this._obj) + + this._gizmo.addEventListener("dragging-changed", (event: { target: TransformControls; value: unknown }) => { + // disable orbit controls when dragging the transform gizmo + const gizmoDragging = World.SceneRenderer.IsAnyGizmoDragging() + World.SceneRenderer.currentCameraControls.enabled = !event.value && !gizmoDragging + + const isShift = InputSystem.isKeyPressed("ShiftRight") || InputSystem.isKeyPressed("ShiftLeft") + const isAlt = InputSystem.isKeyPressed("AltRight") || InputSystem.isKeyPressed("AltLeft") + + switch (event.target.mode) { + case "translate": { + // snap if alt is pressed + event.target.translationSnap = isAlt ? 0.1 : null + + // disable other gizmos when translating + const gizmos = [...World.SceneRenderer.gizmosOnMirabuf.values()] + gizmos.forEach(obj => { + if (obj.gizmo.object === event.target.object && obj.gizmo.mode !== "translate") { + obj.gizmo.dragging = false + obj.gizmo.enabled = !event.value + return + } + }) + break + } + case "rotate": { + // snap if alt is pressed + event.target.rotationSnap = isAlt ? Math.PI * (1.0 / 12.0) : null + + // disable scale gizmos added to the same object + const gizmos = [...World.SceneRenderer.gizmosOnMirabuf.values()] + gizmos.forEach(obj => { + if ( + obj.gizmo.mode === "scale" && + event.target !== obj.gizmo && + obj.gizmo.object === event.target.object + ) { + obj.gizmo.dragging = false + obj.gizmo.enabled = !event.value + return + } + }) + break + } + case "scale": { + // snap if alt is pressed + event.target.setScaleSnap(isAlt ? 0.1 : null) + + // scale uniformly if shift is pressed + event.target.axis = isShift ? "XYZE" : null + break + } + default: { + console.error("Invalid gizmo state") + break + } + } + }) + } + + public Update(): void { + this._gizmo.updateMatrixWorld() + + if (!this.gizmo.object) { + console.error("No object added to gizmo") + return + } + + // updating the size of the gizmo based on the distance from the camera + const mainCameraFovRadians = (Math.PI * (this._mainCamera.fov * 0.5)) / 180 + this._gizmo.setSize( + (this._size / this._mainCamera.position.distanceTo(this.gizmo.object!.position)) * + Math.tan(mainCameraFovRadians) * + 1.9 + ) + + /** Translating the obj changes to the mirabuf scene object */ + if (this._parentObject) { + this._parentObject.DisablePhysics() + if (this.isDragging || this._forceUpdate) { + this._forceUpdate = false + this._parentObject.mirabufInstance.parser.rigidNodes.forEach(rn => { + this.UpdateNodeTransform(rn.id) + }) + this._parentObject.UpdateMeshTransforms() + } + } + } + + public Dispose(): void { + this._gizmo.detach() + this._parentObject?.EnablePhysics() + World.SceneRenderer.RemoveObject(this._obj) + World.SceneRenderer.RemoveObject(this._gizmo) + + this._relativeTransformations?.clear() + } + + /** changes the mode of the gizmo */ + public SetMode(mode: GizmoMode) { + this._gizmo.setMode(mode) + } + + /** + * Updates a given node to follow the gizmo. + * + * @param rnId Target node to update. + */ + public UpdateNodeTransform(rnId: RigidNodeId) { + if (!this._parentObject || !this._relativeTransformations || !this._relativeTransformations.has(rnId)) return + + const jBodyId = this._parentObject.mechanism.GetBodyByNodeId(rnId) + if (!jBodyId) return + + const relativeTransform = this._relativeTransformations.get(rnId)! + const worldTransform = relativeTransform.clone().premultiply(this._obj.matrix) + const position = new THREE.Vector3(0, 0, 0) + const rotation = new THREE.Quaternion(0, 0, 0, 1) + worldTransform.decompose(position, rotation, new THREE.Vector3(1, 1, 1)) + + World.PhysicsSystem.SetBodyPositionAndRotation( + jBodyId, + ThreeVector3_JoltVec3(position), + ThreeQuaternion_JoltQuat(rotation) + ) + } + + /** + * Updates the gizmos location. + * + * @param gizmoTransformation Transform for the gizmo to take on. + */ + public SetTransform(gizmoTransformation: THREE.Matrix4) { + // Super hacky, prolly has something to do with how the transform controls update the attached object. + const position = new THREE.Vector3(0, 0, 0) + const rotation = new THREE.Quaternion(0, 0, 0, 1) + const scale = new THREE.Vector3(1, 1, 1) + gizmoTransformation.decompose(position, rotation, scale) + this._obj.matrix.compose(position, rotation, scale) + + this._obj.position.setFromMatrixPosition(gizmoTransformation) + this._obj.rotation.setFromRotationMatrix(gizmoTransformation) + + this._forceUpdate = true + } + + public SetRotation(rotation: THREE.Quaternion) { + const position = new THREE.Vector3(0, 0, 0) + const scale = new THREE.Vector3(1, 1, 1) + this._obj.matrix.decompose(position, new THREE.Quaternion(0, 0, 0, 1), scale) + this._obj.matrix.compose(position, rotation, scale) + + this._obj.rotation.setFromQuaternion(rotation) + + this._forceUpdate = true + } + + /** @return true if gizmo is attached to mirabufSceneObject */ + public HasParent(): boolean { + return this._parentObject !== undefined + } +} + +export default GizmoSceneObject diff --git a/fission/src/systems/scene/SceneRenderer.ts b/fission/src/systems/scene/SceneRenderer.ts index 34e1f19e04..745545ce81 100644 --- a/fission/src/systems/scene/SceneRenderer.ts +++ b/fission/src/systems/scene/SceneRenderer.ts @@ -1,24 +1,32 @@ import * as THREE from "three" -import SceneObject from "./SceneObject" import WorldSystem from "../WorldSystem" - -import { TransformControls } from "three/examples/jsm/controls/TransformControls.js" -import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" +import SceneObject from "./SceneObject" +import GizmoSceneObject from "./GizmoSceneObject" import { EdgeDetectionMode, EffectComposer, EffectPass, RenderPass, SMAAEffect } from "postprocessing" - -import vertexShader from "@/shaders/vertex.glsl" import fragmentShader from "@/shaders/fragment.glsl" +import vertexShader from "@/shaders/vertex.glsl" import { Theme } from "@/ui/ThemeContext" -import InputSystem from "../input/InputSystem" import Jolt from "@barclah/jolt-physics" +import { CameraControls, CameraControlsType, CustomOrbitControls } from "@/systems/scene/CameraControls" +import ScreenInteractionHandler, { InteractionEnd } from "./ScreenInteractionHandler" import { PixelSpaceCoord, SceneOverlayEvent, SceneOverlayEventKey } from "@/ui/components/SceneOverlayEvents" -import {} from "@/ui/components/SceneOverlayEvents" import PreferencesSystem from "../preferences/PreferencesSystem" +import { CSM } from "three/examples/jsm/csm/CSM.js" +import World from "../World" +import { ThreeVector3_JoltVec3 } from "@/util/TypeConversions" +import { RigidNodeAssociate } from "@/mirabuf/MirabufSceneObject" +import { ContextData, ContextSupplierEvent } from "@/ui/components/ContextMenuData" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import { Global_OpenPanel } from "@/ui/components/GlobalUIControls" const CLEAR_COLOR = 0x121212 const GROUND_COLOR = 0x4066c7 +const STANDARD_ASPECT = 16.0 / 9.0 +const STANDARD_CAMERA_FOV_X = 110.0 +const STANDARD_CAMERA_FOV_Y = STANDARD_CAMERA_FOV_X / STANDARD_ASPECT + let nextSceneObjectId = 1 class SceneRenderer extends WorldSystem { @@ -31,9 +39,12 @@ class SceneRenderer extends WorldSystem { private _antiAliasPass: EffectPass private _sceneObjects: Map + private _gizmosOnMirabuf: Map // maps of all the gizmos that are attached to a mirabuf scene object + + private _cameraControls: CameraControls - private _orbitControls: OrbitControls - private _transformControls: Map // maps all rendered transform controls to their size + private _light: THREE.DirectionalLight | CSM | undefined + private _screenInteractionHandler: ScreenInteractionHandler public get sceneObjects() { return this._sceneObjects @@ -51,13 +62,25 @@ class SceneRenderer extends WorldSystem { return this._renderer } + public get currentCameraControls(): CameraControls { + return this._cameraControls + } + + /** + * Collection that maps Mirabuf objects to active GizmoSceneObjects + */ + public get gizmosOnMirabuf() { + return this._gizmosOnMirabuf + } + public constructor() { super() this._sceneObjects = new Map() - this._transformControls = new Map() + this._gizmosOnMirabuf = new Map() - this._mainCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) + const aspect = window.innerWidth / window.innerHeight + this._mainCamera = new THREE.PerspectiveCamera(STANDARD_CAMERA_FOV_Y, aspect, 0.1, 1000) this._mainCamera.position.set(-2.5, 2, 2.5) this._scene = new THREE.Scene() @@ -75,29 +98,14 @@ class SceneRenderer extends WorldSystem { this._renderer.shadowMap.type = THREE.PCFSoftShadowMap this._renderer.setSize(window.innerWidth, window.innerHeight) - const directionalLight = new THREE.DirectionalLight(0xffffff, 3.0) - directionalLight.position.set(-1.0, 3.0, 2.0) - directionalLight.castShadow = true - this._scene.add(directionalLight) + // Adding the lighting uisng quality preferences + this.ChangeLighting(PreferencesSystem.getGlobalPreference("QualitySettings")) - const shadowMapSize = Math.min(4096, this._renderer.capabilities.maxTextureSize) - const shadowCamSize = 15 - console.debug(`Shadow Map Size: ${shadowMapSize}`) - - directionalLight.shadow.camera.top = shadowCamSize - directionalLight.shadow.camera.bottom = -shadowCamSize - directionalLight.shadow.camera.left = -shadowCamSize - directionalLight.shadow.camera.right = shadowCamSize - directionalLight.shadow.mapSize = new THREE.Vector2(shadowMapSize, shadowMapSize) - directionalLight.shadow.blurSamples = 16 - directionalLight.shadow.normalBias = 0.01 - directionalLight.shadow.bias = 0.0 - - const ambientLight = new THREE.AmbientLight(0xffffff, 0.1) + const ambientLight = new THREE.AmbientLight(0xffffff, 0.3) this._scene.add(ambientLight) const ground = new THREE.Mesh(new THREE.BoxGeometry(10, 1, 10), this.CreateToonMaterial(GROUND_COLOR)) - ground.position.set(0.0, -2.0, 0.0) + ground.position.set(0.0, -0.5, 0.0) ground.receiveShadow = true ground.castShadow = true this._scene.add(ground) @@ -129,14 +137,35 @@ class SceneRenderer extends WorldSystem { this._composer.addPass(this._antiAliasPass) // Orbit controls - this._orbitControls = new OrbitControls(this._mainCamera, this._renderer.domElement) - this._orbitControls.update() + this._screenInteractionHandler = new ScreenInteractionHandler(this._renderer.domElement) + this._screenInteractionHandler.contextMenu = e => this.OnContextMenu(e) + + this._cameraControls = new CustomOrbitControls(this._mainCamera, this._screenInteractionHandler) + } + + public SetCameraControls(controlsType: CameraControlsType) { + this._cameraControls.dispose() + switch (controlsType) { + case "Orbit": + this._cameraControls = new CustomOrbitControls(this._mainCamera, this._screenInteractionHandler) + break + } } public UpdateCanvasSize() { - this._renderer.setSize(window.innerWidth, window.innerHeight) + this._renderer.setSize(window.innerWidth, window.innerHeight, true) + + const vec = new THREE.Vector2(0, 0) + this._renderer.getSize(vec) // No idea why height would be zero, but just incase. - this._mainCamera.aspect = window.innerWidth / window.innerHeight + this._mainCamera.aspect = window.innerHeight > 0 ? window.innerWidth / window.innerHeight : 1.0 + + if (this._mainCamera.aspect < STANDARD_ASPECT) { + this._mainCamera.fov = STANDARD_CAMERA_FOV_Y + } else { + this._mainCamera.fov = STANDARD_CAMERA_FOV_X / this._mainCamera.aspect + } + this._mainCamera.updateProjectionMatrix() } @@ -145,26 +174,96 @@ class SceneRenderer extends WorldSystem { obj.Update() }) - this._skybox.position.copy(this._mainCamera.position) + this._mainCamera.updateMatrixWorld() - const mainCameraFovRadians = (Math.PI * (this._mainCamera.fov * 0.5)) / 180 - this._transformControls.forEach((size, tc) => { - tc.setSize( - (size / this._mainCamera.position.distanceTo(tc.object!.position)) * - Math.tan(mainCameraFovRadians) * - 1.9 - ) - }) + // updating the CSM light if it is enabled + if (this._light instanceof CSM) this._light.update() + + this._skybox.position.copy(this._mainCamera.position) // Update the tags each frame if they are enabled in preferences if (PreferencesSystem.getGlobalPreference("RenderSceneTags")) new SceneOverlayEvent(SceneOverlayEventKey.UPDATE) + this._screenInteractionHandler.update(deltaT) + this._cameraControls.update(deltaT) + this._composer.render(deltaT) + // this._renderer.render(this._scene, this._mainCamera) } public Destroy(): void { this.RemoveAllSceneObjects() + this._screenInteractionHandler.dispose() + } + + /** + * Changes the quality of lighting between cascading shadows and directional lights + * + * @param quality: string representing the quality of lighting - "Low", "Medium", "High" + */ + public ChangeLighting(quality: string): void { + // removing the previous lighting method + if (this._light instanceof THREE.DirectionalLight) { + this._scene.remove(this._light) + } else if (this._light instanceof CSM) { + this._light.dispose() + this._light.remove() + } + + // setting the shadow map size + const shadowMapSize = Math.min(4096, this._renderer.capabilities.maxTextureSize) + + // setting the light to a basic directional light + if (quality === "Low" || quality === "Medium") { + const shadowCamSize = 15 + + this._light = new THREE.DirectionalLight(0xffffff, 5.0) + this._light.position.set(-1.0, 3.0, 2.0) + this._light.castShadow = true + this._light.shadow.camera.top = shadowCamSize + this._light.shadow.camera.bottom = -shadowCamSize + this._light.shadow.camera.left = -shadowCamSize + this._light.shadow.camera.right = shadowCamSize + this._light.shadow.mapSize = new THREE.Vector2(shadowMapSize, shadowMapSize) + this._light.shadow.blurSamples = 16 + this._light.shadow.bias = 0.0 + this._light.shadow.normalBias = 0.01 + this._scene.add(this._light) + } else if (quality === "High") { + // setting light to cascading shadows + this._light = new CSM({ + parent: this._scene, + camera: this._mainCamera, + cascades: 4, + lightDirection: new THREE.Vector3(1.0, -3.0, -2.0).normalize(), + lightIntensity: 5, + shadowMapSize: shadowMapSize, + mode: "custom", + maxFar: 30, + shadowBias: -0.00001, + customSplitsCallback: (cascades: number, near: number, far: number, breaks: number[]) => { + const blend = 0.7 + for (let i = 1; i < cascades; i++) { + const uniformFactor = (near + ((far - near) * i) / cascades) / far + const logarithmicFactor = (near * (far / near) ** (i / cascades)) / far + const combinedFactor = uniformFactor * (1 - blend) + logarithmicFactor * blend + + breaks.push(combinedFactor) + } + + breaks.push(1) + }, + }) + + // setting up the materials for all objects in the scene + this._light.fade = true + this._scene.children.forEach(child => { + if (child instanceof THREE.Mesh) { + if (this._light instanceof CSM) this._light.setupMaterial(child.material) + } + }) + } } public RegisterSceneObject(obj: T): number { @@ -175,13 +274,29 @@ class SceneRenderer extends WorldSystem { return id } + /** Registers gizmos that are attached to a parent mirabufsceneobject */ + public RegisterGizmoSceneObject(obj: GizmoSceneObject): number { + if (obj.HasParent()) this._gizmosOnMirabuf.set(obj.parentObjectId!, obj) + return this.RegisterSceneObject(obj) + } + public RemoveAllSceneObjects() { this._sceneObjects.forEach(obj => obj.Dispose()) + this._gizmosOnMirabuf.clear() this._sceneObjects.clear() } public RemoveSceneObject(id: number) { const obj = this._sceneObjects.get(id) + + // If the object is a mirabuf object, remove the gizmo as well + if (obj instanceof MirabufSceneObject) { + const objGizmo = this._gizmosOnMirabuf.get(id) + if (this._gizmosOnMirabuf.delete(id)) objGizmo!.Dispose() + } else if (obj instanceof GizmoSceneObject && obj.HasParent()) { + this._gizmosOnMirabuf.delete(obj.parentObjectId!) + } + if (this._sceneObjects.delete(id)) { obj!.Dispose() } @@ -190,6 +305,7 @@ class SceneRenderer extends WorldSystem { public CreateSphere(radius: number, material?: THREE.Material | undefined): THREE.Mesh { const geo = new THREE.SphereGeometry(radius) if (material) { + if (this._light instanceof CSM) this._light.setupMaterial(material) return new THREE.Mesh(geo, material) } else { return new THREE.Mesh(geo, this.CreateToonMaterial()) @@ -213,10 +329,13 @@ class SceneRenderer extends WorldSystem { } const gradientMap = new THREE.DataTexture(colors, colors.length, 1, format) gradientMap.needsUpdate = true - return new THREE.MeshToonMaterial({ + const material = new THREE.MeshToonMaterial({ color: color, + shadowSide: THREE.DoubleSide, gradientMap: gradientMap, }) + if (this._light instanceof CSM) this._light.setupMaterial(material) + return material } /** @@ -249,7 +368,7 @@ class SceneRenderer extends WorldSystem { return [(window.innerWidth * (screenSpace.x + 1.0)) / 2.0, (window.innerHeight * (1.0 - screenSpace.y)) / 2.0] } - /** + /** * Updates the skybox colors based on the current theme * @param currentTheme: current theme from ThemeContext.useTheme() @@ -263,83 +382,9 @@ class SceneRenderer extends WorldSystem { } } - /** - * Attach new transform gizmo to Mesh - * - * @param obj Mesh to attach gizmo to - * @param mode Transform mode (translate, rotate, scale) - * @param size Size of the gizmo - * @returns void - */ - public AddTransformGizmo(obj: THREE.Object3D, mode: "translate" | "rotate" | "scale" = "translate", size: number) { - const transformControl = new TransformControls(this._mainCamera, this._renderer.domElement) - transformControl.setMode(mode) - transformControl.attach(obj) - - // allowing the transform gizmos to rotate with the object - transformControl.space = "local" - - transformControl.addEventListener( - "dragging-changed", - (event: { target: TransformControls; value: unknown }) => { - const isAnyGizmoDragging = Array.from(this._transformControls.keys()).some(gizmo => gizmo.dragging) - if (!event.value && !isAnyGizmoDragging) { - this._orbitControls.enabled = true // enable orbit controls when not dragging another transform gizmo - } else if (!event.value && isAnyGizmoDragging) { - this._orbitControls.enabled = false // disable orbit controls when dragging another transform gizmo - } else { - this._orbitControls.enabled = !event.value // disable orbit controls when dragging transform gizmo - } - - if (event.target.mode === "translate") { - this._transformControls.forEach((_size, tc) => { - // disable other transform gizmos when translating - if (tc.object === event.target.object && tc.mode !== "translate") { - tc.dragging = false - tc.enabled = !event.value - return - } - }) - } else if ( - event.target.mode === "scale" && - (InputSystem.isKeyPressed("ShiftRight") || InputSystem.isKeyPressed("ShiftLeft")) - ) { - // scale uniformly if shift is pressed - transformControl.axis = "XYZE" - } else if (event.target.mode === "rotate") { - // scale on all axes - this._transformControls.forEach((_size, tc) => { - // disable scale transform gizmo when scaling - if (tc.mode === "scale" && tc !== event.target && tc.object === event.target.object) { - tc.dragging = false - tc.enabled = !event.value - return - } - }) - } - } - ) - - this._transformControls.set(transformControl, size) - this._scene.add(transformControl) - - return transformControl - } - - /** - * Remove transform gizmos from Mesh - * - * @param obj Mesh to remove gizmo from - * @returns void - */ - public RemoveTransformGizmos(obj: THREE.Object3D) { - this._transformControls.forEach((_, tc) => { - if (tc.object === obj) { - tc.detach() - this._scene.remove(tc) - this._transformControls.delete(tc) - } - }) + /** returns whether any gizmos are being currently dragged */ + public IsAnyGizmoDragging(): boolean { + return [...this._gizmosOnMirabuf.values()].some(obj => obj.gizmo.dragging) } /** @@ -359,6 +404,52 @@ class SceneRenderer extends WorldSystem { public RemoveObject(obj: THREE.Object3D) { this._scene.remove(obj) } + + /** + * Sets up the threejs material for cascading shadows if the CSM is enabled + * + * @param material + */ + public SetupMaterial(material: THREE.Material) { + if (this._light instanceof CSM) this._light.setupMaterial(material) + } + + /** + * Context Menu handler for the scene canvas. + * + * @param e Mouse event data. + */ + public OnContextMenu(e: InteractionEnd) { + // Cast ray into physics scene. + const origin = World.SceneRenderer.mainCamera.position + + const worldSpace = World.SceneRenderer.PixelToWorldSpace(e.position[0], e.position[1]) + const dir = worldSpace.sub(origin).normalize().multiplyScalar(40.0) + + const res = World.PhysicsSystem.RayCast(ThreeVector3_JoltVec3(origin), ThreeVector3_JoltVec3(dir)) + + // Use any associations to determine ContextData. + let miraSupplierData: ContextData | undefined = undefined + if (res) { + const assoc = World.PhysicsSystem.GetBodyAssociation(res.data.mBodyID) as RigidNodeAssociate + if (assoc?.sceneObject) { + miraSupplierData = assoc.sceneObject.getSupplierData() + } + } + + // All else fails, present default options. + if (!miraSupplierData) { + miraSupplierData = { title: "The Scene", items: [] } + miraSupplierData.items.push({ + name: "Add", + func: () => { + Global_OpenPanel?.("import-mirabuf") + }, + }) + } + + ContextSupplierEvent.Dispatch(miraSupplierData, e.position) + } } export default SceneRenderer diff --git a/fission/src/systems/scene/ScreenInteractionHandler.ts b/fission/src/systems/scene/ScreenInteractionHandler.ts new file mode 100644 index 0000000000..9e080ed70e --- /dev/null +++ b/fission/src/systems/scene/ScreenInteractionHandler.ts @@ -0,0 +1,348 @@ +export const PRIMARY_MOUSE_INTERACTION = 0 +export const MIDDLE_MOUSE_INTERACTION = 1 +export const SECONDARY_MOUSE_INTERACTION = 2 +export const TOUCH_INTERACTION = 3 +export const TOUCH_DOUBLE_INTERACTION = 4 + +export type InteractionType = -1 | 0 | 1 | 2 | 3 | 4 + +export interface InteractionStart { + interactionType: InteractionType + position: [number, number] +} + +export interface InteractionMove { + interactionType: InteractionType + scale?: number + movement?: [number, number] +} + +export interface InteractionEnd { + interactionType: InteractionType + position: [number, number] +} + +/** + * Handler for all screen interactions with Mouse, Pen, and Touch controls. + */ +class ScreenInteractionHandler { + private _primaryTouch: number | undefined + private _secondaryTouch: number | undefined + private _primaryTouchPosition: [number, number] | undefined + private _secondaryTouchPosition: [number, number] | undefined + private _movementThresholdMet: boolean = false + private _doubleTapInteraction: boolean = false + private _pointerPosition: [number, number] | undefined + + private _lastPinchSeparation: number | undefined + private _lastPinchPosition: [number, number] | undefined + + private _pointerMove: (ev: PointerEvent) => void + private _wheelMove: (ev: WheelEvent) => void + private _pointerDown: (ev: PointerEvent) => void + private _pointerUp: (ev: PointerEvent) => void + private _contextMenu: (ev: MouseEvent) => unknown + private _touchMove: (ev: TouchEvent) => void + + private _domElement: HTMLElement + + public interactionStart: ((i: InteractionStart) => void) | undefined + public interactionEnd: ((i: InteractionEnd) => void) | undefined + public interactionMove: ((i: InteractionMove) => void) | undefined + public contextMenu: ((i: InteractionEnd) => void) | undefined + + /** + * Caculates the distance between the primary and secondary touch positions. + * + * @returns Distance in pixels. Undefined if primary or secondary touch positions are undefined. + */ + public get pinchSeparation(): number | undefined { + if (this._primaryTouchPosition == undefined || this._secondaryTouchPosition == undefined) { + return undefined + } + + const diff = [ + this._primaryTouchPosition[0] - this._secondaryTouchPosition[0], + this._primaryTouchPosition[1] - this._secondaryTouchPosition[1], + ] + return Math.sqrt(diff[0] ** 2 + diff[1] ** 2) + } + + /** + * Gets the midpoint between the primary and secondary touch positions. + * + * @returns Midpoint between primary and secondary touch positions. Undefined if touch positions are undefined. + */ + public get pinchPosition(): [number, number] | undefined { + if (this._primaryTouchPosition == undefined || this._secondaryTouchPosition == undefined) { + return undefined + } + + return [ + (this._primaryTouchPosition[0] + this._secondaryTouchPosition[0]) / 2.0, + (this._primaryTouchPosition[1] + this._secondaryTouchPosition[1]) / 2.0, + ] + } + + /** + * Adds event listeners to dom element and wraps interaction events around original dom events. + * + * @param domElement Element to attach events to. Generally canvas for our application. + */ + public constructor(domElement: HTMLElement) { + this._domElement = domElement + + this._pointerMove = e => this.pointerMove(e) + this._wheelMove = e => this.wheelMove(e) + this._pointerDown = e => this.pointerDown(e) + this._pointerUp = e => this.pointerUp(e) + this._contextMenu = e => e.preventDefault() + this._touchMove = e => e.preventDefault() + + this._domElement.addEventListener("pointermove", this._pointerMove) + this._domElement.addEventListener( + "wheel", + e => { + if (e.ctrlKey) { + e.preventDefault() + } else { + this._wheelMove(e) + } + }, + { passive: false } + ) + this._domElement.addEventListener("contextmenu", this._contextMenu) + this._domElement.addEventListener("pointerdown", this._pointerDown) + this._domElement.addEventListener("pointerup", this._pointerUp) + this._domElement.addEventListener("pointercancel", this._pointerUp) + this._domElement.addEventListener("pointerleave", this._pointerUp) + + this._domElement.addEventListener("touchmove", this._touchMove) + } + + /** + * Disposes attached event handlers on the selected dom element. + */ + public dispose() { + this._domElement.removeEventListener("pointermove", this._pointerMove) + this._domElement.removeEventListener("wheel", this._wheelMove) + this._domElement.removeEventListener("contextmenu", this._contextMenu) + this._domElement.removeEventListener("pointerdown", this._pointerDown) + this._domElement.removeEventListener("pointerup", this._pointerUp) + this._domElement.removeEventListener("pointercancel", this._pointerUp) + this._domElement.removeEventListener("pointerleave", this._pointerUp) + + this._domElement.removeEventListener("touchmove", this._touchMove) + } + + /** + * This method intercepts pointer move events and translates them into interaction move events accordingly. Pen and mouse movements have + * very minimal parsing, while touch movements are split into two categories. Either you have only a primary touch on the screen, in which + * it has, again, very minimal parsing. However, if there is a secondary touch, it simply updates the tracked positions, without dispatching + * any events. The touches positions are then translated into pinch and pan movements inside the update method. + * + * Pointer movements need to move half the recorded pointers width or height (depending on direction of movement) in order to begin updating + * the position data and dispatch events. + * + * @param e Pointer Event data. + */ + private pointerMove(e: PointerEvent) { + if (!this.interactionMove) { + return + } + + if (e.pointerType == "mouse" || e.pointerType == "pen") { + if (this._pointerPosition && !this._movementThresholdMet) { + const delta = [ + Math.abs(e.clientX - this._pointerPosition![0]), + Math.abs(e.clientY - this._pointerPosition![1]), + ] + if (delta[0] > window.innerWidth * 0.01 || delta[1] > window.innerHeight * 0.01) { + this._movementThresholdMet = true + } else { + return + } + } + + this._pointerPosition = [e.movementX, e.movementY] + + this.interactionMove({ interactionType: e.button as InteractionType, movement: [e.movementX, e.movementY] }) + } else { + if (e.pointerId == this._primaryTouch) { + if (!this._movementThresholdMet) { + if (this.checkMovementThreshold(this._primaryTouchPosition!, e)) { + this._movementThresholdMet = true + } else { + return + } + } + + this._primaryTouchPosition = [e.clientX, e.clientY] + + if (this._secondaryTouch == undefined) { + this.interactionMove({ + interactionType: PRIMARY_MOUSE_INTERACTION, + movement: [e.movementX, e.movementY], + }) + } + } else if (e.pointerId == this._secondaryTouch) { + if (!this._movementThresholdMet) { + if (this.checkMovementThreshold(this._secondaryTouchPosition!, e)) { + this._movementThresholdMet = true + } else { + return + } + } + + this._secondaryTouchPosition = [e.clientX, e.clientY] + } + } + } + + /** + * Intercepts wheel events and passes them along via the interaction move event. + * + * @param e Wheel event data. + */ + private wheelMove(e: WheelEvent) { + if (!this.interactionMove) { + return + } + + this.interactionMove({ interactionType: -1, scale: e.deltaY * 0.01 }) + } + + /** + * The primary role of update within screen interaction handler is to parse the double touches on the screen into + * pinch and pan movement, then dispatch the data via the interaction move events. + * + * @param _ Unused deltaT variable. + */ + public update(_: number) { + if (this._secondaryTouch != undefined && this._movementThresholdMet) { + // Calculate current pinch position and separation + const pinchSep = this.pinchSeparation! + const pinchPos = this.pinchPosition! + + // If previous ones exist, determine delta and send events + if (this._lastPinchPosition != undefined && this._lastPinchSeparation != undefined) { + this.interactionMove?.({ + interactionType: SECONDARY_MOUSE_INTERACTION, + scale: (pinchSep - this._lastPinchSeparation) * -0.03, + }) + + this.interactionMove?.({ + interactionType: SECONDARY_MOUSE_INTERACTION, + movement: [pinchPos[0] - this._lastPinchPosition[0], pinchPos[1] - this._lastPinchPosition[1]], + }) + } + + // Load current into last + this._lastPinchSeparation = pinchSep + this._lastPinchPosition = pinchPos + } + } + + private pointerDown(e: PointerEvent) { + if (!this.interactionStart) { + return + } + + if (e.pointerType == "touch") { + if (this._primaryTouch == undefined) { + this._primaryTouch = e.pointerId + this._primaryTouchPosition = [e.clientX, e.clientY] + this._movementThresholdMet = false + this.interactionStart({ + interactionType: PRIMARY_MOUSE_INTERACTION, + position: this._primaryTouchPosition, + }) + } else if (this._secondaryTouch == undefined) { + this._secondaryTouch = e.pointerId + this._secondaryTouchPosition = [e.clientX, e.clientY] + this._doubleTapInteraction = true + + this._lastPinchSeparation = undefined + this._lastPinchPosition = undefined + + this.interactionStart({ + interactionType: SECONDARY_MOUSE_INTERACTION, + position: this._secondaryTouchPosition, + }) + } + } else { + if (e.button >= 0 && e.button <= 2) { + this._movementThresholdMet = false + this._pointerPosition = [e.clientX, e.clientY] + this.interactionStart({ + interactionType: e.button as InteractionType, + position: [e.clientX, e.clientY], + }) + } + } + } + + private pointerUp(e: PointerEvent) { + if (!this.interactionEnd) { + return + } + + if (e.pointerType == "touch") { + if (e.pointerId == this._primaryTouch) { + this._primaryTouch = this._secondaryTouch + this._secondaryTouch = undefined + if (this._primaryTouch != undefined) { + this.interactionEnd({ + interactionType: SECONDARY_MOUSE_INTERACTION, + position: [e.clientX, e.clientY], + }) + } else { + this.interactionEnd({ + interactionType: PRIMARY_MOUSE_INTERACTION, + position: [e.clientX, e.clientY], + }) + if (this._doubleTapInteraction && !this._movementThresholdMet && this.contextMenu) { + this.contextMenu({ + interactionType: -1, + position: this.pinchPosition!, + }) + } + this._doubleTapInteraction = false + } + // Reset continuous tracking + } else if (e.pointerId == this._secondaryTouch) { + this._secondaryTouch = undefined + this.interactionEnd({ + interactionType: SECONDARY_MOUSE_INTERACTION, + position: [e.clientX, e.clientY], + }) + } + } else { + if (e.button >= 0 && e.button <= 2) { + const end: InteractionEnd = { + interactionType: e.button as InteractionType, + position: [e.clientX, e.clientY], + } + this.interactionEnd(end) + if (e.button == SECONDARY_MOUSE_INTERACTION && !this._movementThresholdMet && this.contextMenu) { + this.contextMenu(end) + } + } + } + } + + /** + * Checks if a given position has moved from the origin given a specified threshold. + * + * @param origin Origin to move away from. + * @param ptr Pointer data. + * @returns True if latest is outside of the box around origin with sides the length of thresholds * 2. + */ + private checkMovementThreshold(origin: [number, number], ptr: PointerEvent): boolean { + const delta = [Math.abs(ptr.clientX - origin[0]), Math.abs(ptr.clientY - origin[1])] + + return delta[0] > ptr.width / 2.0 || delta[1] > ptr.height / 2.0 + } +} + +export default ScreenInteractionHandler diff --git a/fission/src/systems/simulation/Brain.ts b/fission/src/systems/simulation/Brain.ts index a047d65671..f8923574aa 100644 --- a/fission/src/systems/simulation/Brain.ts +++ b/fission/src/systems/simulation/Brain.ts @@ -1,10 +1,18 @@ import Mechanism from "../physics/Mechanism" +export type BrainType = "synthesis" | "wpilib" | "unknown" + abstract class Brain { protected _mechanism: Mechanism - constructor(mechansim: Mechanism) { - this._mechanism = mechansim + private _brainType: BrainType + public get brainType() { + return this._brainType + } + + constructor(mechanism: Mechanism, brainType: BrainType) { + this._mechanism = mechanism + this._brainType = brainType } public abstract Update(deltaT: number): void diff --git a/fission/src/systems/simulation/Nora.ts b/fission/src/systems/simulation/Nora.ts new file mode 100644 index 0000000000..08f22832ef --- /dev/null +++ b/fission/src/systems/simulation/Nora.ts @@ -0,0 +1,54 @@ +/** + * To build input validation into the node editor, I had to + * make this poor man's type system. Please make it better. + * + * We should be able to assign identifiers to the types and + * probably have more in-tune mechanisms for handling the + * junction situations. Right now its kinda patched together + * with the averaging function setup I have below. + */ + +export enum NoraTypes { + Number = "num", + Number2 = "(num,num)", + Number3 = "(num,num,num)", + Unknown = "unknown", +} + +export type NoraNumber = number +export type NoraNumber2 = [NoraNumber, NoraNumber] +export type NoraNumber3 = [NoraNumber, NoraNumber, NoraNumber] +export type NoraUnknown = unknown + +export type NoraType = NoraNumber | NoraNumber2 | NoraNumber3 | NoraUnknown + +// Needed? +// export function constructNoraType(...types: NoraTypes[]): NoraTypes { +// return `[${types.join(",")}]` as NoraTypes +// } + +export function deconstructNoraType(type: NoraTypes): NoraTypes[] | undefined { + if (type.charAt(0) != "(" || type.charAt(type.length - 1) != ")") return undefined + return type.substring(1, type.length - 1).split(",") as NoraTypes[] +} + +export function isNoraDeconstructable(type: NoraTypes): boolean { + return type.charAt(0) == "(" && type.charAt(type.length - 1) == ")" +} + +const averageFuncMap: { [k in NoraTypes]: ((...many: NoraType[]) => NoraType) | undefined } = { + [NoraTypes.Number]: function (...many: NoraType[]): NoraType { + return many.reduce((prev, next) => (prev += next as NoraNumber), 0) + }, + [NoraTypes.Number2]: undefined, + [NoraTypes.Number3]: undefined, + [NoraTypes.Unknown]: undefined, +} + +export function noraAverageFunc(type: NoraTypes): ((...many: NoraType[]) => NoraType) | undefined { + return averageFuncMap[type] +} + +export function hasNoraAverageFunc(type: NoraTypes): boolean { + return averageFuncMap[type] != undefined +} diff --git a/fission/src/systems/simulation/SimulationSystem.ts b/fission/src/systems/simulation/SimulationSystem.ts index f0feeba8b2..34aa587030 100644 --- a/fission/src/systems/simulation/SimulationSystem.ts +++ b/fission/src/systems/simulation/SimulationSystem.ts @@ -2,8 +2,8 @@ import JOLT from "@/util/loading/JoltSyncLoader" import Mechanism from "../physics/Mechanism" import WorldSystem from "../WorldSystem" import Brain from "./Brain" -import Driver from "./driver/Driver" -import Stimulus from "./stimulus/Stimulus" +import Driver, { DriverType, makeDriverID } from "./driver/Driver" +import Stimulus, { makeStimulusID, StimulusType } from "./stimulus/Stimulus" import HingeDriver from "./driver/HingeDriver" import WheelDriver from "./driver/WheelDriver" import SliderDriver from "./driver/SliderDriver" @@ -11,6 +11,10 @@ import HingeStimulus from "./stimulus/HingeStimulus" import WheelRotationStimulus from "./stimulus/WheelStimulus" import SliderStimulus from "./stimulus/SliderStimulus" import ChassisStimulus from "./stimulus/ChassisStimulus" +import IntakeDriver from "./driver/IntakeDriver" +import World from "../World" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import EjectorDriver from "./driver/EjectorDriver" class SimulationSystem extends WorldSystem { private _simMechanisms: Map @@ -63,47 +67,72 @@ class SimulationLayer { private _mechanism: Mechanism private _brain?: Brain - private _drivers: Driver[] - private _stimuli: Stimulus[] + private _drivers: Map + private _stimuli: Map public get brain() { return this._brain } public get drivers() { - return this._drivers + return [...this._drivers.values()] } public get stimuli() { - return this._stimuli + return [...this._stimuli.values()] } constructor(mechanism: Mechanism) { this._mechanism = mechanism + const assembly = [...World.SceneRenderer.sceneObjects.values()].find( + x => (x as MirabufSceneObject).mechanism == mechanism + ) as MirabufSceneObject + // Generate standard drivers and stimuli - this._drivers = [] - this._stimuli = [] + this._drivers = new Map() + this._stimuli = new Map() this._mechanism.constraints.forEach(x => { - if (x.constraint.GetSubType() == JOLT.EConstraintSubType_Hinge) { - const hinge = JOLT.castObject(x.constraint, JOLT.HingeConstraint) - const driver = new HingeDriver(hinge, x.info) - this._drivers.push(driver) - const stim = new HingeStimulus(hinge) - this._stimuli.push(stim) - } else if (x.constraint.GetSubType() == JOLT.EConstraintSubType_Vehicle) { - const vehicle = JOLT.castObject(x.constraint, JOLT.VehicleConstraint) - const driver = new WheelDriver(vehicle, x.info) - this._drivers.push(driver) - const stim = new WheelRotationStimulus(vehicle.GetWheel(0)) - this._stimuli.push(stim) - } else if (x.constraint.GetSubType() == JOLT.EConstraintSubType_Slider) { - const slider = JOLT.castObject(x.constraint, JOLT.SliderConstraint) - const driver = new SliderDriver(slider, x.info) - this._drivers.push(driver) - const stim = new SliderStimulus(slider) - this._stimuli.push(stim) + if (x.primaryConstraint.GetSubType() == JOLT.EConstraintSubType_Hinge) { + const hinge = JOLT.castObject(x.primaryConstraint, JOLT.HingeConstraint) + const driver = new HingeDriver(makeDriverID(x), hinge, x.maxVelocity, x.info) + this._drivers.set(JSON.stringify(driver.id), driver) + const stim = new HingeStimulus(makeStimulusID(x), hinge, x.info) + this._stimuli.set(JSON.stringify(stim.id), stim) + } else if (x.primaryConstraint.GetSubType() == JOLT.EConstraintSubType_Vehicle) { + const vehicle = JOLT.castObject(x.primaryConstraint, JOLT.VehicleConstraint) + const driver = new WheelDriver(makeDriverID(x), vehicle, x.maxVelocity, x.info) + this._drivers.set(JSON.stringify(driver.id), driver) + const stim = new WheelRotationStimulus(makeStimulusID(x), vehicle.GetWheel(0), x.info) + this._stimuli.set(JSON.stringify(stim.id), stim) + } else if (x.primaryConstraint.GetSubType() == JOLT.EConstraintSubType_Slider) { + const slider = JOLT.castObject(x.primaryConstraint, JOLT.SliderConstraint) + const driver = new SliderDriver(makeDriverID(x), slider, x.maxVelocity, x.info) + this._drivers.set(JSON.stringify(driver.id), driver) + const stim = new SliderStimulus(makeStimulusID(x), slider, x.info) + this._stimuli.set(JSON.stringify(stim.id), stim) } }) - this._stimuli.push(new ChassisStimulus(mechanism.nodeToBody.get(mechanism.rootBody)!)) + + const chassisStim = new ChassisStimulus( + { type: StimulusType.Stim_ChassisAccel, guid: "CHASSIS_GUID" }, + mechanism.nodeToBody.get(mechanism.rootBody)!, + { GUID: "CHASSIS_GUID", name: "Chassis" } + ) + this._stimuli.set(JSON.stringify(chassisStim.id), chassisStim) + + if (assembly) { + const intakeDriv = new IntakeDriver({ type: DriverType.Driv_Intake, guid: "INTAKE_GUID" }, assembly, { + GUID: "INTAKE_GUID", + name: "Intake", + }) + const ejectorDriv = new EjectorDriver({ type: DriverType.Driv_Ejector, guid: "EJECTOR_GUID" }, assembly, { + GUID: "EJECTOR_GUID", + name: "Ejector", + }) + this._drivers.set(JSON.stringify(ejectorDriv.id), ejectorDriv) + this._drivers.set(JSON.stringify(intakeDriv.id), intakeDriv) + } else { + console.debug("No Assembly found with given mechanism, skipping intake and ejector...") + } } public Update(deltaT: number) { @@ -117,9 +146,15 @@ class SimulationLayer { this._brain = brain - if (this._brain) { - this._brain.Enable() - } + if (this._brain) this._brain.Enable() + } + + public GetStimuli(id: string) { + return this._stimuli.get(id) + } + + public GetDriver(id: string) { + return this._drivers.get(id) } } diff --git a/fission/src/systems/simulation/behavior/synthesis/ArcadeDriveBehavior.ts b/fission/src/systems/simulation/behavior/synthesis/ArcadeDriveBehavior.ts index 6851ad4305..2ebd1530f2 100644 --- a/fission/src/systems/simulation/behavior/synthesis/ArcadeDriveBehavior.ts +++ b/fission/src/systems/simulation/behavior/synthesis/ArcadeDriveBehavior.ts @@ -8,8 +8,9 @@ class ArcadeDriveBehavior extends Behavior { private rightWheels: WheelDriver[] private _brainIndex: number - private _driveSpeed = 30 - private _turnSpeed = 30 + public get wheels(): WheelDriver[] { + return this.leftWheels.concat(this.rightWheels) + } constructor( leftWheels: WheelDriver[], @@ -26,19 +27,19 @@ class ArcadeDriveBehavior extends Behavior { } // Sets the drivetrains target linear and rotational velocity - private DriveSpeeds(linearVelocity: number, rotationVelocity: number) { - const leftSpeed = linearVelocity + rotationVelocity - const rightSpeed = linearVelocity - rotationVelocity + private DriveSpeeds(driveInput: number, turnInput: number) { + const leftDirection = Math.min(1, Math.max(-1, driveInput + turnInput)) + const rightDirection = Math.min(1, Math.max(-1, driveInput - turnInput)) - this.leftWheels.forEach(wheel => (wheel.targetWheelSpeed = leftSpeed)) - this.rightWheels.forEach(wheel => (wheel.targetWheelSpeed = rightSpeed)) + this.leftWheels.forEach(wheel => (wheel.accelerationDirection = leftDirection)) + this.rightWheels.forEach(wheel => (wheel.accelerationDirection = rightDirection)) } public Update(_: number): void { - const driveInput = InputSystem.getInput("arcadeDrive", this._brainIndex) - const turnInput = InputSystem.getInput("arcadeTurn", this._brainIndex) - - this.DriveSpeeds(driveInput * this._driveSpeed, turnInput * this._turnSpeed) + this.DriveSpeeds( + InputSystem.getInput("arcadeDrive", this._brainIndex), + InputSystem.getInput("arcadeTurn", this._brainIndex) + ) } } diff --git a/fission/src/systems/simulation/behavior/synthesis/GamepieceManipBehavior.ts b/fission/src/systems/simulation/behavior/synthesis/GamepieceManipBehavior.ts new file mode 100644 index 0000000000..bacee33a53 --- /dev/null +++ b/fission/src/systems/simulation/behavior/synthesis/GamepieceManipBehavior.ts @@ -0,0 +1,26 @@ +import Behavior from "@/systems/simulation/behavior/Behavior" +import InputSystem from "@/systems/input/InputSystem" +import EjectorDriver from "../../driver/EjectorDriver" +import IntakeDriver from "../../driver/IntakeDriver" + +class GamepieceManipBehavior extends Behavior { + private _brainIndex: number + + private _ejector: EjectorDriver + private _intake: IntakeDriver + + constructor(ejector: EjectorDriver, intake: IntakeDriver, brainIndex: number) { + super([ejector, intake], []) + + this._brainIndex = brainIndex + this._ejector = ejector + this._intake = intake + } + + public Update(_: number): void { + this._ejector.value = InputSystem.getInput("eject", this._brainIndex) + this._intake.value = InputSystem.getInput("intake", this._brainIndex) + } +} + +export default GamepieceManipBehavior diff --git a/fission/src/systems/simulation/behavior/synthesis/GenericArmBehavior.ts b/fission/src/systems/simulation/behavior/synthesis/GenericArmBehavior.ts index a9d19c5c03..eb98b7346a 100644 --- a/fission/src/systems/simulation/behavior/synthesis/GenericArmBehavior.ts +++ b/fission/src/systems/simulation/behavior/synthesis/GenericArmBehavior.ts @@ -1,30 +1,29 @@ -import HingeDriver from "@/systems/simulation/driver/HingeDriver" -import HingeStimulus from "@/systems/simulation/stimulus/HingeStimulus" -import Behavior from "@/systems/simulation/behavior/Behavior" -import InputSystem from "@/systems/input/InputSystem" +import { SequentialBehaviorPreferences } from "@/systems/preferences/PreferenceTypes" +import HingeDriver from "../../driver/HingeDriver" +import HingeStimulus from "../../stimulus/HingeStimulus" +import SequenceableBehavior from "./SequenceableBehavior" -class GenericArmBehavior extends Behavior { +class GenericArmBehavior extends SequenceableBehavior { private _hingeDriver: HingeDriver - private _inputName: string - private _brainIndex: number - private _rotationalSpeed = 6 + public get hingeDriver(): HingeDriver { + return this._hingeDriver + } - constructor(hingeDriver: HingeDriver, hingeStimulus: HingeStimulus, jointIndex: number, brainIndex: number) { - super([hingeDriver], [hingeStimulus]) + constructor( + hingeDriver: HingeDriver, + hingeStimulus: HingeStimulus, + jointIndex: number, + brainIndex: number, + sequentialConfig: SequentialBehaviorPreferences | undefined + ) { + super(jointIndex, brainIndex, [hingeDriver], [hingeStimulus], sequentialConfig) this._hingeDriver = hingeDriver - this._inputName = "joint " + jointIndex - this._brainIndex = brainIndex - } - - // Sets the arms target rotational velocity - rotateArm(rotationalVelocity: number) { - this._hingeDriver.targetVelocity = rotationalVelocity } - public Update(_: number): void { - this.rotateArm(InputSystem.getInput(this._inputName, this._brainIndex) * this._rotationalSpeed) + applyInput = (direction: number) => { + this._hingeDriver.accelerationDirection = direction } } diff --git a/fission/src/systems/simulation/behavior/synthesis/GenericElevatorBehavior.ts b/fission/src/systems/simulation/behavior/synthesis/GenericElevatorBehavior.ts index fb8b237d16..b325ff54cf 100644 --- a/fission/src/systems/simulation/behavior/synthesis/GenericElevatorBehavior.ts +++ b/fission/src/systems/simulation/behavior/synthesis/GenericElevatorBehavior.ts @@ -1,30 +1,29 @@ -import SliderDriver from "@/systems/simulation/driver/SliderDriver" -import SliderStimulus from "@/systems/simulation/stimulus/SliderStimulus" -import Behavior from "@/systems/simulation/behavior/Behavior" -import InputSystem from "@/systems/input/InputSystem" +import { SequentialBehaviorPreferences } from "@/systems/preferences/PreferenceTypes" +import SliderDriver from "../../driver/SliderDriver" +import SliderStimulus from "../../stimulus/SliderStimulus" +import SequenceableBehavior from "./SequenceableBehavior" -class GenericElevatorBehavior extends Behavior { +class GenericElevatorBehavior extends SequenceableBehavior { private _sliderDriver: SliderDriver - private _inputName: string - private _brainIndex: number - private _linearSpeed = 2.5 + public get sliderDriver(): SliderDriver { + return this._sliderDriver + } - constructor(sliderDriver: SliderDriver, sliderStimulus: SliderStimulus, jointIndex: number, brainIndex: number) { - super([sliderDriver], [sliderStimulus]) + constructor( + sliderDriver: SliderDriver, + sliderStimulus: SliderStimulus, + jointIndex: number, + brainIndex: number, + sequentialConfig: SequentialBehaviorPreferences | undefined + ) { + super(jointIndex, brainIndex, [sliderDriver], [sliderStimulus], sequentialConfig) this._sliderDriver = sliderDriver - this._inputName = "joint " + jointIndex - this._brainIndex = brainIndex - } - - // Changes the elevators target position - moveElevator(linearVelocity: number) { - this._sliderDriver.targetVelocity = linearVelocity } - public Update(_: number): void { - this.moveElevator(InputSystem.getInput(this._inputName, this._brainIndex) * this._linearSpeed) + applyInput = (direction: number) => { + this._sliderDriver.accelerationDirection = direction } } diff --git a/fission/src/systems/simulation/behavior/synthesis/SequenceableBehavior.ts b/fission/src/systems/simulation/behavior/synthesis/SequenceableBehavior.ts new file mode 100644 index 0000000000..3da51b26b4 --- /dev/null +++ b/fission/src/systems/simulation/behavior/synthesis/SequenceableBehavior.ts @@ -0,0 +1,40 @@ +import { SequentialBehaviorPreferences } from "@/systems/preferences/PreferenceTypes" +import Driver from "../../driver/Driver" +import Stimulus from "../../stimulus/Stimulus" +import Behavior from "../Behavior" +import InputSystem from "@/systems/input/InputSystem" + +abstract class SequenceableBehavior extends Behavior { + private _jointIndex: number + private _brainIndex: number + private _sequentialConfig: SequentialBehaviorPreferences | undefined + + public get jointIndex(): number { + return this._jointIndex + } + + constructor( + jointIndex: number, + brainIndex: number, + drivers: Driver[], + stimuli: Stimulus[], + sequentialConfig: SequentialBehaviorPreferences | undefined + ) { + super(drivers, stimuli) + + this._jointIndex = jointIndex + this._brainIndex = brainIndex + this._sequentialConfig = sequentialConfig + } + + abstract applyInput: (velocity: number) => void + + public Update(_: number): void { + const inputName = "joint " + (this._sequentialConfig?.parentJointIndex ?? this._jointIndex) + const inverted = this._sequentialConfig?.inverted ?? false + + this.applyInput(InputSystem.getInput(inputName, this._brainIndex) * (inverted ? -1 : 1)) + } +} + +export default SequenceableBehavior diff --git a/fission/src/systems/simulation/driver/Driver.ts b/fission/src/systems/simulation/driver/Driver.ts index a6fbdb39f3..f00e277455 100644 --- a/fission/src/systems/simulation/driver/Driver.ts +++ b/fission/src/systems/simulation/driver/Driver.ts @@ -1,17 +1,71 @@ import { mirabuf } from "@/proto/mirabuf" +import { MechanismConstraint } from "@/systems/physics/Mechanism" +import JOLT from "@/util/loading/JoltSyncLoader" +import { NoraType, NoraTypes } from "../Nora" +import { SimReceiver } from "../wpilib_brain/SimDataFlow" -abstract class Driver { +export enum DriverType { + Driv_Hinge = "Driv_Hinge", + Driv_Wheel = "Driv_Wheel", + Driv_Slider = "Driv_Slider", + Driv_Intake = "Driv_Intake", + Driv_Ejector = "Driv_Ejector", + Driv_Unknown = "Driv_Unknown", +} + +export type DriverID = { + type: DriverType + name?: string + guid: string +} + +export function makeDriverID(constraint: MechanismConstraint): DriverID { + let driverType: DriverType = DriverType.Driv_Unknown + switch (constraint.primaryConstraint.GetSubType()) { + case JOLT.EConstraintSubType_Hinge: + driverType = DriverType.Driv_Hinge + break + case JOLT.EConstraintSubType_Slider: + driverType = DriverType.Driv_Slider + break + case JOLT.EConstraintSubType_Vehicle: + driverType = DriverType.Driv_Wheel + break + } + + return { + type: driverType, + name: constraint.info?.name ?? undefined, + guid: constraint.info?.GUID ?? "unknown", + } +} + +abstract class Driver implements SimReceiver { + private _id: DriverID private _info?: mirabuf.IInfo - constructor(info?: mirabuf.IInfo) { + constructor(id: DriverID, info?: mirabuf.IInfo) { + this._id = id this._info = info } public abstract Update(deltaT: number): void + public get id() { + return this._id + } + + public get idStr() { + return JSON.stringify(this._id) + } + public get info() { return this._info } + + public abstract setReceiverValue(val: NoraType): void + public abstract getReceiverType(): NoraTypes + public abstract DisplayName(): string } export enum DriverControlMode { diff --git a/fission/src/systems/simulation/driver/EjectorDriver.ts b/fission/src/systems/simulation/driver/EjectorDriver.ts new file mode 100644 index 0000000000..b23fe12d2a --- /dev/null +++ b/fission/src/systems/simulation/driver/EjectorDriver.ts @@ -0,0 +1,33 @@ +import { mirabuf } from "@/proto/mirabuf" +import { NoraNumber, NoraTypes } from "../Nora" +import Driver, { DriverID } from "./Driver" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" + +class EjectorDriver extends Driver { + public value: number + + private _assembly: MirabufSceneObject + + public constructor(id: DriverID, assembly: MirabufSceneObject, info?: mirabuf.IInfo) { + super(id, info) + + this._assembly = assembly + this.value = 0.0 + } + + public Update(_deltaT: number): void { + this._assembly.ejectorActive = this.value > 0.5 + } + + public setReceiverValue(val: NoraNumber): void { + this.value = val + } + public getReceiverType(): NoraTypes { + return NoraTypes.Number + } + public DisplayName(): string { + return "Ejector" + } +} + +export default EjectorDriver diff --git a/fission/src/systems/simulation/driver/HingeDriver.ts b/fission/src/systems/simulation/driver/HingeDriver.ts index 29ab7fc6af..cf860a4acf 100644 --- a/fission/src/systems/simulation/driver/HingeDriver.ts +++ b/fission/src/systems/simulation/driver/HingeDriver.ts @@ -1,23 +1,30 @@ import Jolt from "@barclah/jolt-physics" -import Driver, { DriverControlMode } from "./Driver" +import Driver, { DriverControlMode, DriverID } from "./Driver" import { GetLastDeltaT } from "@/systems/physics/PhysicsSystem" import JOLT from "@/util/loading/JoltSyncLoader" import { mirabuf } from "@/proto/mirabuf" +import PreferencesSystem, { PreferenceEvent } from "@/systems/preferences/PreferencesSystem" +import { NoraNumber, NoraTypes } from "../Nora" + +const MAX_TORQUE_WITHOUT_GRAV = 100 class HingeDriver extends Driver { private _constraint: Jolt.HingeConstraint private _controlMode: DriverControlMode = DriverControlMode.Velocity - private _targetVelocity: number = 0.0 private _targetAngle: number + private _maxTorqueWithGrav: number = 0.0 + public accelerationDirection: number = 0.0 + public maxVelocity: number - public get targetVelocity(): number { - return this._targetVelocity - } - public set targetVelocity(radsPerSec: number) { - this._targetVelocity = radsPerSec + public get constraint(): Jolt.HingeConstraint { + return this._constraint } + private _prevAng: number = 0.0 + + private _gravityChange?: (event: PreferenceEvent) => void + public get targetAngle(): number { return this._targetAngle } @@ -25,13 +32,14 @@ class HingeDriver extends Driver { this._targetAngle = Math.max(this._constraint.GetLimitsMin(), Math.min(this._constraint.GetLimitsMax(), rads)) } - public set minTorqueLimit(nm: number) { - const motorSettings = this._constraint.GetMotorSettings() - motorSettings.mMinTorqueLimit = nm + public get maxForce() { + return this._constraint.GetMotorSettings().mMaxTorqueLimit } - public set maxTorqueLimit(nm: number) { + + public set maxForce(nm: number) { const motorSettings = this._constraint.GetMotorSettings() - motorSettings.mMaxTorqueLimit = nm + motorSettings.set_mMaxTorqueLimit(nm) + motorSettings.set_mMinTorqueLimit(-nm) } public get controlMode(): DriverControlMode { @@ -53,10 +61,12 @@ class HingeDriver extends Driver { } } - public constructor(constraint: Jolt.HingeConstraint, info?: mirabuf.IInfo) { - super(info) + public constructor(id: DriverID, constraint: Jolt.HingeConstraint, maxVelocity: number, info?: mirabuf.IInfo) { + super(id, info) this._constraint = constraint + this.maxVelocity = maxVelocity + this._targetAngle = this._constraint.GetCurrentAngle() const motorSettings = this._constraint.GetMotorSettings() const springSettings = motorSettings.mSpringSettings @@ -64,23 +74,55 @@ class HingeDriver extends Driver { // These values were selected based on the suggestions of the documentation for stiff control. springSettings.mFrequency = 20 * (1.0 / GetLastDeltaT()) springSettings.mDamping = 0.995 - motorSettings.mSpringSettings = springSettings - motorSettings.mMinTorqueLimit = -200.0 - motorSettings.mMaxTorqueLimit = 200.0 - this._targetAngle = this._constraint.GetCurrentAngle() + this._maxTorqueWithGrav = motorSettings.get_mMaxTorqueLimit() + if (!PreferencesSystem.getGlobalPreference("SubsystemGravity")) { + motorSettings.set_mMaxTorqueLimit(MAX_TORQUE_WITHOUT_GRAV) + motorSettings.set_mMinTorqueLimit(-MAX_TORQUE_WITHOUT_GRAV) + } this.controlMode = DriverControlMode.Velocity + + this._gravityChange = (event: PreferenceEvent) => { + if (event.prefName == "SubsystemGravity") { + const motorSettings = this._constraint.GetMotorSettings() + if (event.prefValue) { + motorSettings.set_mMaxTorqueLimit(this._maxTorqueWithGrav) + motorSettings.set_mMinTorqueLimit(-this._maxTorqueWithGrav) + } else { + motorSettings.set_mMaxTorqueLimit(MAX_TORQUE_WITHOUT_GRAV) + motorSettings.set_mMinTorqueLimit(-MAX_TORQUE_WITHOUT_GRAV) + } + } + } + + PreferencesSystem.addEventListener(this._gravityChange) } public Update(_: number): void { if (this._controlMode == DriverControlMode.Velocity) { - this._constraint.SetTargetAngularVelocity(this._targetVelocity) + this._constraint.SetTargetAngularVelocity(this.accelerationDirection * this.maxVelocity) } else if (this._controlMode == DriverControlMode.Position) { - this._constraint.SetTargetAngle(this._targetAngle) + let ang = this._targetAngle + + if (ang - this._prevAng < -this.maxVelocity) ang = this._prevAng - this.maxVelocity + if (ang - this._prevAng > this.maxVelocity) ang = this._prevAng + this.maxVelocity + this._constraint.SetTargetAngle(ang) } } + + public getReceiverType(): NoraTypes { + return NoraTypes.Number + } + + public setReceiverValue(val: NoraNumber): void { + this.accelerationDirection = val + } + + public DisplayName(): string { + return `${this.info?.name ?? "-"} [Hinge]` + } } export default HingeDriver diff --git a/fission/src/systems/simulation/driver/IntakeDriver.ts b/fission/src/systems/simulation/driver/IntakeDriver.ts new file mode 100644 index 0000000000..e8ead3b52d --- /dev/null +++ b/fission/src/systems/simulation/driver/IntakeDriver.ts @@ -0,0 +1,33 @@ +import { mirabuf } from "@/proto/mirabuf" +import { NoraNumber, NoraTypes } from "../Nora" +import Driver, { DriverID } from "./Driver" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" + +class IntakeDriver extends Driver { + public value: number + + private _assembly: MirabufSceneObject + + public constructor(id: DriverID, assembly: MirabufSceneObject, info?: mirabuf.IInfo) { + super(id, info) + + this._assembly = assembly + this.value = 0.0 + } + + public Update(_deltaT: number): void { + this._assembly.intakeActive = this.value > 0.5 + } + + public setReceiverValue(val: NoraNumber): void { + this.value = val + } + public getReceiverType(): NoraTypes { + return NoraTypes.Number + } + public DisplayName(): string { + return "Intake" + } +} + +export default IntakeDriver diff --git a/fission/src/systems/simulation/driver/SliderDriver.ts b/fission/src/systems/simulation/driver/SliderDriver.ts index dd442b38aa..e19d2aa3f2 100644 --- a/fission/src/systems/simulation/driver/SliderDriver.ts +++ b/fission/src/systems/simulation/driver/SliderDriver.ts @@ -1,23 +1,30 @@ import Jolt from "@barclah/jolt-physics" -import Driver, { DriverControlMode } from "./Driver" +import Driver, { DriverControlMode, DriverID } from "./Driver" import { GetLastDeltaT } from "@/systems/physics/PhysicsSystem" import JOLT from "@/util/loading/JoltSyncLoader" import { mirabuf } from "@/proto/mirabuf" +import PreferencesSystem, { PreferenceEvent } from "@/systems/preferences/PreferencesSystem" +import { NoraNumber, NoraTypes } from "../Nora" + +const MAX_FORCE_WITHOUT_GRAV = 500 class SliderDriver extends Driver { private _constraint: Jolt.SliderConstraint private _controlMode: DriverControlMode = DriverControlMode.Velocity - private _targetVelocity: number = 0.0 private _targetPosition: number = 0.0 + private _maxForceWithGrav: number = 0.0 + public accelerationDirection: number = 0.0 + public maxVelocity: number = 1.0 - public get targetVelocity(): number { - return this._targetVelocity - } - public set targetVelocity(radsPerSec: number) { - this._targetVelocity = radsPerSec + public get constraint(): Jolt.SliderConstraint { + return this._constraint } + private _prevPos: number = 0.0 + + private _gravityChange?: (event: PreferenceEvent) => void + public get targetPosition(): number { return this._targetPosition } @@ -28,13 +35,13 @@ class SliderDriver extends Driver { ) } - public set minForceLimit(newtons: number) { - const motorSettings = this._constraint.GetMotorSettings() - motorSettings.mMinForceLimit = newtons + public get maxForce(): number { + return this._constraint.GetMotorSettings().mMaxForceLimit } - public set maxForceLimit(newtons: number) { + public set maxForce(newtons: number) { const motorSettings = this._constraint.GetMotorSettings() - motorSettings.mMaxForceLimit = newtons + motorSettings.set_mMaxForceLimit(newtons) + motorSettings.set_mMinForceLimit(-newtons) } public get controlMode(): DriverControlMode { @@ -56,31 +63,65 @@ class SliderDriver extends Driver { } } - public constructor(constraint: Jolt.SliderConstraint, info?: mirabuf.IInfo) { - super(info) + public constructor(id: DriverID, constraint: Jolt.SliderConstraint, maxVelocity: number, info?: mirabuf.IInfo) { + super(id, info) this._constraint = constraint + this.maxVelocity = maxVelocity const motorSettings = this._constraint.GetMotorSettings() const springSettings = motorSettings.mSpringSettings springSettings.mFrequency = 20 * (1.0 / GetLastDeltaT()) springSettings.mDamping = 0.999 - motorSettings.mSpringSettings = springSettings - motorSettings.mMinForceLimit = -900.0 - motorSettings.mMaxForceLimit = 900.0 + + this._maxForceWithGrav = motorSettings.get_mMaxForceLimit() + if (!PreferencesSystem.getGlobalPreference("SubsystemGravity")) { + motorSettings.set_mMaxForceLimit(MAX_FORCE_WITHOUT_GRAV) + motorSettings.set_mMinForceLimit(-MAX_FORCE_WITHOUT_GRAV) + } this._constraint.SetMotorState(JOLT.EMotorState_Velocity) this.controlMode = DriverControlMode.Velocity + + this._gravityChange = (event: PreferenceEvent) => { + if (event.prefName == "SubsystemGravity") { + const motorSettings = this._constraint.GetMotorSettings() + if (event.prefValue) { + motorSettings.set_mMaxForceLimit(this._maxForceWithGrav) + motorSettings.set_mMinForceLimit(-this._maxForceWithGrav) + } else { + motorSettings.set_mMaxForceLimit(MAX_FORCE_WITHOUT_GRAV) + motorSettings.set_mMinForceLimit(-MAX_FORCE_WITHOUT_GRAV) + } + } + } + + PreferencesSystem.addEventListener(this._gravityChange) } public Update(_: number): void { if (this._controlMode == DriverControlMode.Velocity) { - this._constraint.SetTargetVelocity(this._targetVelocity) + this._constraint.SetTargetVelocity(this.accelerationDirection * this.maxVelocity) } else if (this._controlMode == DriverControlMode.Position) { - this._constraint.SetTargetPosition(this._targetPosition) + let pos = this._targetPosition + + if (pos - this._prevPos < -this.maxVelocity) pos = this._prevPos - this.maxVelocity + if (pos - this._prevPos > this.maxVelocity) pos = this._prevPos + this.maxVelocity + + this._constraint.SetTargetPosition(pos) } } + + public getReceiverType(): NoraTypes { + return NoraTypes.Number + } + public setReceiverValue(val: NoraNumber): void { + this.accelerationDirection = val + } + public DisplayName(): string { + return `${this.info?.name ?? "-"} [Slider]` + } } export default SliderDriver diff --git a/fission/src/systems/simulation/driver/WheelDriver.ts b/fission/src/systems/simulation/driver/WheelDriver.ts index 426e893f83..6b8ad6d4fd 100644 --- a/fission/src/systems/simulation/driver/WheelDriver.ts +++ b/fission/src/systems/simulation/driver/WheelDriver.ts @@ -1,11 +1,12 @@ import Jolt from "@barclah/jolt-physics" -import Driver from "./Driver" +import Driver, { DriverID } from "./Driver" import JOLT from "@/util/loading/JoltSyncLoader" import { SimType } from "../wpilib_brain/WPILibBrain" import { mirabuf } from "@/proto/mirabuf" +import { NoraNumber, NoraTypes } from "../Nora" -const LATERIAL_FRICTION = 0.6 -const LONGITUDINAL_FRICTION = 0.8 +const LATERIAL_FRICTION = 1.0 +const LONGITUDINAL_FRICTION = 1.0 class WheelDriver extends Driver { private _constraint: Jolt.VehicleConstraint @@ -14,13 +15,25 @@ class WheelDriver extends Driver { public device?: string private _reversed: boolean - private _targetWheelSpeed: number = 0.0 + public accelerationDirection: number = 0.0 + private _prevVel: number = 0.0 + public maxVelocity = 30.0 + private _maxAcceleration = 1.5 - public get targetWheelSpeed(): number { - return this._targetWheelSpeed + public _targetVelocity = () => { + let vel = this.accelerationDirection * (this._reversed ? -1 : 1) * this.maxVelocity + + if (vel - this._prevVel < -this._maxAcceleration) vel = this._prevVel - this._maxAcceleration + if (vel - this._prevVel > this._maxAcceleration) vel = this._prevVel + this._maxAcceleration + + return vel } - public set targetWheelSpeed(radsPerSec: number) { - this._targetWheelSpeed = radsPerSec + + public get maxForce(): number { + return this._maxAcceleration + } + public set maxForce(acc: number) { + this._maxAcceleration = acc } public get constraint(): Jolt.VehicleConstraint { @@ -28,15 +41,21 @@ class WheelDriver extends Driver { } public constructor( + id: DriverID, constraint: Jolt.VehicleConstraint, + maxVel: number, info?: mirabuf.IInfo, deviceType?: SimType, device?: string, reversed: boolean = false ) { - super(info) + super(id, info) this._constraint = constraint + this.maxVelocity = maxVel + const controller = JOLT.castObject(this._constraint.GetController(), JOLT.WheeledVehicleController) + this._maxAcceleration = controller.GetEngine().mMaxTorque + this._reversed = reversed this.deviceType = deviceType this.device = device @@ -46,12 +65,24 @@ class WheelDriver extends Driver { } public Update(_: number): void { - this._wheel.SetAngularVelocity(this._targetWheelSpeed * (this._reversed ? -1 : 1)) + const vel = this._targetVelocity() + this._wheel.SetAngularVelocity(vel) + this._prevVel = vel } public set reversed(val: boolean) { this._reversed = val } + + public getReceiverType(): NoraTypes { + return NoraTypes.Number + } + public setReceiverValue(val: NoraNumber): void { + this.accelerationDirection = val + } + public DisplayName(): string { + return `${this.info?.name ?? "-"} [Wheel]` + } } export default WheelDriver diff --git a/fission/src/systems/simulation/stimulus/ChassisStimulus.ts b/fission/src/systems/simulation/stimulus/ChassisStimulus.ts index fa59a64f1b..b06a55e0c5 100644 --- a/fission/src/systems/simulation/stimulus/ChassisStimulus.ts +++ b/fission/src/systems/simulation/stimulus/ChassisStimulus.ts @@ -1,6 +1,8 @@ import Jolt from "@barclah/jolt-physics" -import Stimulus from "./Stimulus" +import Stimulus, { StimulusID } from "./Stimulus" import World from "@/systems/World" +import { mirabuf } from "@/proto/mirabuf" +import { NoraNumber3, NoraTypes } from "../Nora" class ChassisStimulus extends Stimulus { private _body: Jolt.Body @@ -19,14 +21,24 @@ class ChassisStimulus extends Stimulus { return this._body.GetRotation().GetEulerAngles() } - public constructor(bodyId: Jolt.BodyID) { - super() + public constructor(id: StimulusID, bodyId: Jolt.BodyID, info?: mirabuf.IInfo) { + super(id, info) this._body = World.PhysicsSystem.GetBody(bodyId) this._mass = this._body.GetShape().GetMassProperties().mMass } public Update(_: number): void {} + + public getSupplierType(): NoraTypes { + return NoraTypes.Number3 + } + public getSupplierValue(): NoraNumber3 { + throw new Error("Method not implemented.") + } + public DisplayName(): string { + return "Chassis [Accel|Gyro]" + } } export default ChassisStimulus diff --git a/fission/src/systems/simulation/stimulus/EncoderStimulus.ts b/fission/src/systems/simulation/stimulus/EncoderStimulus.ts index bee93b4923..b6806c11b7 100644 --- a/fission/src/systems/simulation/stimulus/EncoderStimulus.ts +++ b/fission/src/systems/simulation/stimulus/EncoderStimulus.ts @@ -1,11 +1,12 @@ -import Stimulus from "./Stimulus" +import { mirabuf } from "@/proto/mirabuf" +import Stimulus, { StimulusID } from "./Stimulus" abstract class EncoderStimulus extends Stimulus { public abstract get positionValue(): number public abstract get velocityValue(): number - protected constructor() { - super() + protected constructor(id: StimulusID, info?: mirabuf.IInfo) { + super(id, info) } public abstract Update(_: number): void diff --git a/fission/src/systems/simulation/stimulus/HingeStimulus.ts b/fission/src/systems/simulation/stimulus/HingeStimulus.ts index bcb8464275..4b22c59e39 100644 --- a/fission/src/systems/simulation/stimulus/HingeStimulus.ts +++ b/fission/src/systems/simulation/stimulus/HingeStimulus.ts @@ -1,5 +1,8 @@ import Jolt from "@barclah/jolt-physics" import EncoderStimulus from "./EncoderStimulus" +import { mirabuf } from "@/proto/mirabuf" +import { StimulusID } from "./Stimulus" +import { NoraTypes, NoraNumber2 } from "../Nora" class HingeStimulus extends EncoderStimulus { private _accum: boolean = false @@ -25,8 +28,8 @@ class HingeStimulus extends EncoderStimulus { this._accum = shouldAccum } - public constructor(hinge: Jolt.HingeConstraint) { - super() + public constructor(id: StimulusID, hinge: Jolt.HingeConstraint, info?: mirabuf.IInfo) { + super(id, info) this._hinge = hinge } @@ -40,6 +43,16 @@ class HingeStimulus extends EncoderStimulus { public resetAccum() { this._hingeAngleAccum = 0.0 } + + public getSupplierType(): NoraTypes { + return NoraTypes.Number2 + } + public getSupplierValue(): NoraNumber2 { + return [this.positionValue, this.velocityValue] + } + public DisplayName(): string { + return `${this.info?.name ?? "-"} [Encoder]` + } } export default HingeStimulus diff --git a/fission/src/systems/simulation/stimulus/SliderStimulus.ts b/fission/src/systems/simulation/stimulus/SliderStimulus.ts index 0e66eb63b6..9bb2aefdd4 100644 --- a/fission/src/systems/simulation/stimulus/SliderStimulus.ts +++ b/fission/src/systems/simulation/stimulus/SliderStimulus.ts @@ -1,5 +1,8 @@ import Jolt from "@barclah/jolt-physics" import EncoderStimulus from "./EncoderStimulus" +import { mirabuf } from "@/proto/mirabuf" +import { StimulusID } from "./Stimulus" +import { NoraTypes, NoraNumber2 } from "../Nora" class SliderStimulus extends EncoderStimulus { private _slider: Jolt.SliderConstraint @@ -12,8 +15,8 @@ class SliderStimulus extends EncoderStimulus { return this._velocity } - public constructor(slider: Jolt.SliderConstraint) { - super() + public constructor(id: StimulusID, slider: Jolt.SliderConstraint, info?: mirabuf.IInfo) { + super(id, info) this._slider = slider } @@ -23,6 +26,16 @@ class SliderStimulus extends EncoderStimulus { this._velocity = (this._slider.GetCurrentPosition() - this._lastPosition) / deltaT this._lastPosition = this._slider.GetCurrentPosition() } + + public getSupplierType(): NoraTypes { + return NoraTypes.Number2 + } + public getSupplierValue(): NoraNumber2 { + return [this.positionValue, this.velocityValue] + } + public DisplayName(): string { + return `${this.info?.name ?? "-"} [Encoder]` + } } export default SliderStimulus diff --git a/fission/src/systems/simulation/stimulus/Stimulus.ts b/fission/src/systems/simulation/stimulus/Stimulus.ts index 5dd744cce8..7601d7cf73 100644 --- a/fission/src/systems/simulation/stimulus/Stimulus.ts +++ b/fission/src/systems/simulation/stimulus/Stimulus.ts @@ -1,5 +1,64 @@ -abstract class Stimulus { +import { mirabuf } from "@/proto/mirabuf" +import { MechanismConstraint } from "@/systems/physics/Mechanism" +import JOLT from "@/util/loading/JoltSyncLoader" +import { NoraType, NoraTypes } from "../Nora" +import { SimSupplier } from "../wpilib_brain/SimDataFlow" + +export enum StimulusType { + Stim_ChassisAccel = "Stim_ChassisAccel", + Stim_Encoder = "Stim_Encoder", + Stim_Unknown = "Stim_Unknown", +} + +export type StimulusID = { + type: StimulusType + name?: string + guid: string +} + +export function makeStimulusID(constraint: MechanismConstraint): StimulusID { + let stimulusType: StimulusType = StimulusType.Stim_Unknown + switch (constraint.primaryConstraint.GetSubType()) { + case JOLT.EConstraintSubType_Hinge: + case JOLT.EConstraintSubType_Slider: + case JOLT.EConstraintSubType_Vehicle: + stimulusType = StimulusType.Stim_Encoder + break + } + + return { + type: stimulusType, + name: constraint.info?.name ?? undefined, + guid: constraint.info?.GUID ?? "unknown", + } +} + +abstract class Stimulus implements SimSupplier { + private _id: StimulusID + private _info?: mirabuf.IInfo + + constructor(id: StimulusID, info?: mirabuf.IInfo) { + this._id = id + this._info = info + } + public abstract Update(deltaT: number): void + + public get id() { + return this._id + } + + public get idStr() { + return JSON.stringify(this._id) + } + + public get info() { + return this._info + } + + public abstract getSupplierType(): NoraTypes + public abstract getSupplierValue(): NoraType + public abstract DisplayName(): string } export default Stimulus diff --git a/fission/src/systems/simulation/stimulus/WheelStimulus.ts b/fission/src/systems/simulation/stimulus/WheelStimulus.ts index cfb6206085..7c9fd51624 100644 --- a/fission/src/systems/simulation/stimulus/WheelStimulus.ts +++ b/fission/src/systems/simulation/stimulus/WheelStimulus.ts @@ -1,5 +1,8 @@ import Jolt from "@barclah/jolt-physics" import EncoderStimulus from "./EncoderStimulus" +import { mirabuf } from "@/proto/mirabuf" +import { StimulusID } from "./Stimulus" +import { NoraTypes, NoraNumber2 } from "../Nora" /** * @@ -28,8 +31,8 @@ class WheelRotationStimulus extends EncoderStimulus { this._accum = shouldAccum } - public constructor(wheel: Jolt.Wheel) { - super() + public constructor(id: StimulusID, wheel: Jolt.Wheel, info?: mirabuf.IInfo) { + super(id, info) this._wheel = wheel } @@ -43,6 +46,16 @@ class WheelRotationStimulus extends EncoderStimulus { public resetAccum() { this._wheelRotationAccum = 0.0 } + + public getSupplierType(): NoraTypes { + return NoraTypes.Number2 + } + public getSupplierValue(): NoraNumber2 { + return [this.positionValue, this.velocityValue] + } + public DisplayName(): string { + return `${this.info?.name ?? "-"} [Encoder]` + } } export default WheelRotationStimulus diff --git a/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts b/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts index bdbd902102..7464008c15 100644 --- a/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts +++ b/fission/src/systems/simulation/synthesis_brain/SynthesisBrain.ts @@ -1,4 +1,3 @@ -import Mechanism from "@/systems/physics/Mechanism" import Brain from "../Brain" import Behavior from "../behavior/Behavior" import World from "@/systems/World" @@ -15,7 +14,12 @@ import SliderDriver from "../driver/SliderDriver" import SliderStimulus from "../stimulus/SliderStimulus" import GenericElevatorBehavior from "../behavior/synthesis/GenericElevatorBehavior" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" +import { DefaultSequentialConfig } from "@/systems/preferences/PreferenceTypes" import InputSystem from "@/systems/input/InputSystem" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import IntakeDriver from "../driver/IntakeDriver" +import EjectorDriver from "../driver/EjectorDriver" +import GamepieceManipBehavior from "../behavior/synthesis/GamepieceManipBehavior" class SynthesisBrain extends Brain { public static brainIndexMap = new Map() @@ -24,10 +28,23 @@ class SynthesisBrain extends Brain { private _simLayer: SimulationLayer private _assemblyName: string private _brainIndex: number + private _assembly: MirabufSceneObject // Tracks how many joins have been made with unique controls private _currentJointIndex = 1 + public get assemblyName(): string { + return this._assemblyName + } + + public get behaviors(): Behavior[] { + return this._behaviors + } + + // Tracks the number of each specific mira file spawned + public static numberRobotsSpawned: { [key: string]: number } = {} + + /** @returns {string} The name of the input scheme attached to this brain. */ public get inputSchemeName(): string { const scheme = InputSystem.brainIndexSchemeMap.get(this._brainIndex) if (scheme == undefined) return "Not Configured" @@ -35,16 +52,23 @@ class SynthesisBrain extends Brain { return scheme.schemeName } + /** @returns {number} The unique index used to identify this brain. */ public get brainIndex(): number { return this._brainIndex } - public constructor(mechanism: Mechanism, assemblyName: string) { - super(mechanism) + /** + * @param mechanism The mechanism this brain will control. + * @param assemblyName The name of the assembly that corresponds to the mechanism used for identification. + */ + public constructor(assembly: MirabufSceneObject, assemblyName: string) { + super(assembly.mechanism, "synthesis") - this._simLayer = World.SimulationSystem.GetSimulationLayer(mechanism)! + this._assembly = assembly + this._simLayer = World.SimulationSystem.GetSimulationLayer(assembly.mechanism)! this._assemblyName = assemblyName + // I'm not fixing this right now, but this is going to become an issue... this._brainIndex = SynthesisBrain.brainIndexMap.size SynthesisBrain.brainIndexMap.set(this._brainIndex, this) @@ -54,10 +78,11 @@ class SynthesisBrain extends Brain { } // Only adds controls to mechanisms that are controllable (ignores fields) - if (mechanism.controllable) { + if (assembly.mechanism.controllable) { this.configureArcadeDriveBehavior() this.configureArmBehaviors() this.configureElevatorBehaviors() + this.configureGamepieceManipBehavior() } else { this.configureField() } @@ -67,9 +92,13 @@ class SynthesisBrain extends Brain { public Update(deltaT: number): void { this._behaviors.forEach(b => b.Update(deltaT)) + + this._assembly.ejectorActive = InputSystem.getInput("eject", this._brainIndex) > 0.5 + this._assembly.intakeActive = InputSystem.getInput("intake", this._brainIndex) > 0.5 } public Disable(): void { + this.clearControls() this._behaviors = [] } @@ -77,7 +106,7 @@ class SynthesisBrain extends Brain { InputSystem.brainIndexSchemeMap.delete(this._brainIndex) } - // Creates an instance of ArcadeDriveBehavior and automatically configures it + /** Creates an instance of ArcadeDriveBehavior and automatically configures it. */ private configureArcadeDriveBehavior() { const wheelDrivers: WheelDriver[] = this._simLayer.drivers.filter( driver => driver instanceof WheelDriver @@ -88,8 +117,8 @@ class SynthesisBrain extends Brain { // Two body constraints are part of wheels and are used to determine which way a wheel is facing const fixedConstraints: Jolt.TwoBodyConstraint[] = this._mechanism.constraints - .filter(mechConstraint => mechConstraint.constraint instanceof JOLT.TwoBodyConstraint) - .map(mechConstraint => mechConstraint.constraint as Jolt.TwoBodyConstraint) + .filter(mechConstraint => mechConstraint.primaryConstraint instanceof JOLT.TwoBodyConstraint) + .map(mechConstraint => mechConstraint.primaryConstraint as Jolt.TwoBodyConstraint) const leftWheels: WheelDriver[] = [] const leftStimuli: WheelRotationStimulus[] = [] @@ -122,7 +151,7 @@ class SynthesisBrain extends Brain { ) } - // Creates instances of ArmBehavior and automatically configures them + /** Creates instances of ArmBehavior and automatically configures them. */ private configureArmBehaviors() { const hingeDrivers: HingeDriver[] = this._simLayer.drivers.filter( driver => driver instanceof HingeDriver @@ -132,14 +161,34 @@ class SynthesisBrain extends Brain { ) as HingeStimulus[] for (let i = 0; i < hingeDrivers.length; i++) { + let sequentialConfig = PreferencesSystem.getRobotPreferences(this._assemblyName).sequentialConfig?.find( + sc => sc.jointIndex == this._currentJointIndex + ) + + if (sequentialConfig == undefined) { + sequentialConfig = DefaultSequentialConfig(this._currentJointIndex, "Arm") + + if (PreferencesSystem.getRobotPreferences(this._assemblyName).sequentialConfig == undefined) + PreferencesSystem.getRobotPreferences(this._assemblyName).sequentialConfig = [] + + PreferencesSystem.getRobotPreferences(this._assemblyName).sequentialConfig?.push(sequentialConfig) + PreferencesSystem.savePreferences() + } + this._behaviors.push( - new GenericArmBehavior(hingeDrivers[i], hingeStimuli[i], this._currentJointIndex, this._brainIndex) + new GenericArmBehavior( + hingeDrivers[i], + hingeStimuli[i], + this._currentJointIndex, + this._brainIndex, + sequentialConfig + ) ) this._currentJointIndex++ } } - // Creates instances of ElevatorBehavior and automatically configures them + /** Creates instances of ElevatorBehavior and automatically configures them. */ private configureElevatorBehaviors() { const sliderDrivers: SliderDriver[] = this._simLayer.drivers.filter( driver => driver instanceof SliderDriver @@ -149,23 +198,59 @@ class SynthesisBrain extends Brain { ) as SliderStimulus[] for (let i = 0; i < sliderDrivers.length; i++) { + let sequentialConfig = PreferencesSystem.getRobotPreferences(this._assemblyName).sequentialConfig?.find( + sc => sc.jointIndex == this._currentJointIndex + ) + + if (sequentialConfig == undefined) { + sequentialConfig = DefaultSequentialConfig(this._currentJointIndex, "Elevator") + + if (PreferencesSystem.getRobotPreferences(this._assemblyName).sequentialConfig == undefined) + PreferencesSystem.getRobotPreferences(this._assemblyName).sequentialConfig = [] + + PreferencesSystem.getRobotPreferences(this._assemblyName).sequentialConfig?.push(sequentialConfig) + PreferencesSystem.savePreferences() + } + this._behaviors.push( new GenericElevatorBehavior( sliderDrivers[i], sliderStimuli[i], this._currentJointIndex, - this._brainIndex + this._brainIndex, + sequentialConfig ) ) this._currentJointIndex++ } } + private configureGamepieceManipBehavior() { + let intake: IntakeDriver | undefined = undefined + let ejector: EjectorDriver | undefined = undefined + this._simLayer.drivers.forEach(x => { + if (x instanceof IntakeDriver) { + intake = x + } else if (x instanceof EjectorDriver) { + ejector = x + } + }) + + if (!intake || !ejector) return + + this._behaviors.push(new GamepieceManipBehavior(ejector, intake, this._brainIndex)) + } + + /** Gets field preferences and handles any field specific configuration. */ private configureField() { PreferencesSystem.getFieldPreferences(this._assemblyName) /** Put any field configuration here */ } + + public static GetBrainIndex(assembly: MirabufSceneObject | undefined): number | undefined { + return (assembly?.brain as SynthesisBrain)?.brainIndex + } } export default SynthesisBrain diff --git a/fission/src/systems/simulation/wpilib_brain/SimDataFlow.ts b/fission/src/systems/simulation/wpilib_brain/SimDataFlow.ts new file mode 100644 index 0000000000..ab93391ded --- /dev/null +++ b/fission/src/systems/simulation/wpilib_brain/SimDataFlow.ts @@ -0,0 +1,70 @@ +import { NoraTypes, NoraType, NoraNumber } from "../Nora" + +export type SimSupplier = { + getSupplierType(): NoraTypes + getSupplierValue(): NoraType +} + +export type SimReceiver = { + getReceiverType(): NoraTypes + setReceiverValue(val: NoraType): void +} + +export type SimFlow = { + supplier: SimSupplier + receiver: SimReceiver +} + +export function validate(s: SimSupplier, r: SimReceiver): boolean { + return s.getSupplierType() === r.getReceiverType() +} + +export class SimSupplierAverage implements SimSupplier { + private _suppliers: SimSupplier[] + + public constructor(suppliers?: SimSupplier[]) { + if (!suppliers || suppliers.some(x => x.getSupplierType() != NoraTypes.Number)) { + this._suppliers = [] + } else { + this._suppliers = suppliers + } + } + + public AddSupplier(supplier: SimSupplier) { + if (supplier.getSupplierType() == NoraTypes.Number) { + this._suppliers.push(supplier) + } + } + + getSupplierType(): NoraTypes { + return NoraTypes.Number + } + getSupplierValue(): NoraNumber { + return this._suppliers.reduce((prev, next) => (prev += next.getSupplierValue() as NoraNumber), 0) + } +} + +export class SimReceiverDistribution implements SimReceiver { + private _receivers: SimReceiver[] + + public constructor(receivers?: SimReceiver[]) { + if (!receivers || receivers.some(x => x.getReceiverType() != NoraTypes.Number)) { + this._receivers = [] + } else { + this._receivers = receivers + } + } + + public AddReceiver(receiver: SimReceiver) { + if (receiver.getReceiverType() == NoraTypes.Number) { + this._receivers.push(receiver) + } + } + + getReceiverType(): NoraTypes { + return NoraTypes.Number + } + setReceiverValue(value: NoraNumber) { + this._receivers.forEach(x => x.setReceiverValue(value)) + } +} diff --git a/fission/src/systems/simulation/wpilib_brain/SimInput.ts b/fission/src/systems/simulation/wpilib_brain/SimInput.ts new file mode 100644 index 0000000000..b4776f03a3 --- /dev/null +++ b/fission/src/systems/simulation/wpilib_brain/SimInput.ts @@ -0,0 +1,165 @@ +import World from "@/systems/World" +import EncoderStimulus from "../stimulus/EncoderStimulus" +import { SimCANEncoder, SimGyro, SimAccel, SimDIO, SimAI } from "./WPILibBrain" +import Mechanism from "@/systems/physics/Mechanism" +import Jolt from "@barclah/jolt-physics" +import JOLT from "@/util/loading/JoltSyncLoader" +import { JoltQuat_ThreeQuaternion, JoltVec3_ThreeVector3 } from "@/util/TypeConversions" +import * as THREE from "three" + +export abstract class SimInput { + constructor(protected _device: string) {} + + public abstract Update(deltaT: number): void + + public get device(): string { + return this._device + } +} + +export class SimEncoderInput extends SimInput { + private _stimulus: EncoderStimulus + + constructor(device: string, stimulus: EncoderStimulus) { + super(device) + this._stimulus = stimulus + } + + public Update(_deltaT: number) { + SimCANEncoder.SetPosition(this._device, this._stimulus.positionValue) + SimCANEncoder.SetVelocity(this._device, this._stimulus.velocityValue) + } +} + +export class SimGyroInput extends SimInput { + private _robot: Mechanism + private _joltID?: Jolt.BodyID + private _joltBody?: Jolt.Body + + private static AXIS_X: Jolt.Vec3 = new JOLT.Vec3(1, 0, 0) + private static AXIS_Y: Jolt.Vec3 = new JOLT.Vec3(0, 1, 0) + private static AXIS_Z: Jolt.Vec3 = new JOLT.Vec3(0, 0, 1) + + constructor(device: string, robot: Mechanism) { + super(device) + this._robot = robot + this._joltID = this._robot.nodeToBody.get(this._robot.rootBody) + + if (this._joltID) this._joltBody = World.PhysicsSystem.GetBody(this._joltID) + } + + private GetAxis(axis: Jolt.Vec3): number { + return ((this._joltBody?.GetRotation().GetRotationAngle(axis) ?? 0) * 180) / Math.PI + } + + private GetX(): number { + return this.GetAxis(SimGyroInput.AXIS_X) + } + + private GetY(): number { + return this.GetAxis(SimGyroInput.AXIS_Y) + } + + private GetZ(): number { + return this.GetAxis(SimGyroInput.AXIS_Z) + } + + private GetAxisVelocity(axis: "x" | "y" | "z"): number { + const axes = this._joltBody?.GetAngularVelocity() + if (!axes) return 0 + + switch (axis) { + case "x": + return axes.GetX() + case "y": + return axes.GetY() + case "z": + return axes.GetZ() + } + } + + public Update(_deltaT: number) { + const x = this.GetX() + const y = this.GetY() + const z = this.GetZ() + + SimGyro.SetAngleX(this._device, x) + SimGyro.SetAngleY(this._device, y) + SimGyro.SetAngleZ(this._device, z) + SimGyro.SetRateX(this._device, this.GetAxisVelocity("x")) + SimGyro.SetRateY(this._device, this.GetAxisVelocity("y")) + SimGyro.SetRateZ(this._device, this.GetAxisVelocity("z")) + } +} + +export class SimAccelInput extends SimInput { + private _robot: Mechanism + private _joltID?: Jolt.BodyID + private _prevVel: THREE.Vector3 + + constructor(device: string, robot: Mechanism) { + super(device) + this._robot = robot + this._joltID = this._robot.nodeToBody.get(this._robot.rootBody) + this._prevVel = new THREE.Vector3(0, 0, 0) + } + + public Update(deltaT: number) { + if (!this._joltID) return + const body = World.PhysicsSystem.GetBody(this._joltID) + + const rot = JoltQuat_ThreeQuaternion(body.GetRotation()) + const mat = new THREE.Matrix4().makeRotationFromQuaternion(rot).transpose() + const newVel = JoltVec3_ThreeVector3(body.GetLinearVelocity()).applyMatrix4(mat) + + const x = (newVel.x - this._prevVel.x) / deltaT + const y = (newVel.y - this._prevVel.y) / deltaT + const z = (newVel.y - this._prevVel.y) / deltaT + + SimAccel.SetX(this._device, x) + SimAccel.SetY(this._device, y) + SimAccel.SetZ(this._device, z) + + this._prevVel = newVel + } +} + +export class SimDigitalInput extends SimInput { + private _valueSupplier: () => boolean + + /** + * Creates a Simulation Digital Input object. + * + * @param device Device ID + * @param valueSupplier Called each frame and returns what the value should be set to + */ + constructor(device: string, valueSupplier: () => boolean) { + super(device) + this._valueSupplier = valueSupplier + } + + private SetValue(value: boolean) { + SimDIO.SetValue(this._device, value) + } + + public GetValue(): boolean { + return SimDIO.GetValue(this._device) + } + + public Update(_deltaT: number) { + if (this._valueSupplier) this.SetValue(this._valueSupplier()) + } +} + +export class SimAnalogInput extends SimInput { + private _valueSupplier: () => number + + constructor(device: string, valueSupplier: () => number) { + super(device) + this._valueSupplier = valueSupplier + } + + public Update(_deltaT: number) { + SimAI.SetValue(this._device, this._valueSupplier()) + } +} diff --git a/fission/src/systems/simulation/wpilib_brain/SimOutput.ts b/fission/src/systems/simulation/wpilib_brain/SimOutput.ts new file mode 100644 index 0000000000..04e9205c7c --- /dev/null +++ b/fission/src/systems/simulation/wpilib_brain/SimOutput.ts @@ -0,0 +1,109 @@ +import Driver from "../driver/Driver" +import HingeDriver from "../driver/HingeDriver" +import SliderDriver from "../driver/SliderDriver" +import WheelDriver from "../driver/WheelDriver" +import { SimAO, SimCAN, SimDIO, SimPWM, SimType } from "./WPILibBrain" + +export abstract class SimOutput { + constructor(protected _name: string) {} + + public abstract Update(deltaT: number): void + + public get name(): string { + return this._name + } +} + +export abstract class SimOutputGroup extends SimOutput { + public ports: number[] + public drivers: Driver[] + public type: SimType + + public constructor(name: string, ports: number[], drivers: Driver[], type: SimType) { + super(name) + this.ports = ports + this.drivers = drivers + this.type = type + } + + public abstract Update(deltaT: number): void +} + +export class PWMOutputGroup extends SimOutputGroup { + public constructor(name: string, ports: number[], drivers: Driver[]) { + super(name, ports, drivers, SimType.PWM) + } + + public Update(_deltaT: number) { + const average = + this.ports.reduce((sum, port) => { + const speed = SimPWM.GetSpeed(`${port}`) ?? 0 + return sum + speed + }, 0) / this.ports.length + + this.drivers.forEach(d => { + if (d instanceof WheelDriver) { + d.accelerationDirection = average + } else if (d instanceof HingeDriver || d instanceof SliderDriver) { + d.accelerationDirection = average + } + d.Update(_deltaT) + }) + } +} + +export class CANOutputGroup extends SimOutputGroup { + public constructor(name: string, ports: number[], drivers: Driver[]) { + super(name, ports, drivers, SimType.CANMotor) + } + + public Update(deltaT: number): void { + const average = + this.ports.reduce((sum, port) => { + const device = SimCAN.GetDeviceWithID(port, SimType.CANMotor) + return sum + ((device?.get(" { + if (d instanceof WheelDriver) { + d.accelerationDirection = average + } else if (d instanceof HingeDriver || d instanceof SliderDriver) { + d.accelerationDirection = average + } + d.Update(deltaT) + }) + } +} + +export class SimDigitalOutput extends SimOutput { + /** + * Creates a Simulation Digital Input/Output object. + * + * @param device Device ID + */ + constructor(name: string) { + super(name) + } + + public SetValue(value: boolean) { + SimDIO.SetValue(this._name, value) + } + + public GetValue(): boolean { + return SimDIO.GetValue(this._name) + } + + public Update(_deltaT: number) {} +} + +export class SimAnalogOutput extends SimOutput { + public constructor(name: string) { + super(name) + } + + public GetVoltage(): number { + return SimAO.GetVoltage(this._name) + } + + public Update(_deltaT: number) {} +} diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts index 3d9ee7d65e..a08a321f6c 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts @@ -1,21 +1,51 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import Mechanism from "@/systems/physics/Mechanism" import Brain from "../Brain" +import Lazy from "@/util/Lazy.ts" import WPILibWSWorker from "./WPILibWSWorker?worker" import { SimulationLayer } from "../SimulationSystem" import World from "@/systems/World" -import Driver from "../driver/Driver" -const worker = new WPILibWSWorker() +import { SimAnalogOutput, SimDigitalOutput, SimOutput } from "./SimOutput" +import { SimAccelInput, SimAnalogInput, SimDigitalInput, SimGyroInput, SimInput } from "./SimInput" +import { Random } from "@/util/Random" +import { NoraNumber, NoraNumber2, NoraNumber3, NoraTypes } from "../Nora" +import { SimFlow, SimReceiver, SimSupplier, validate } from "./SimDataFlow" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import { SimConfig } from "@/ui/panels/simulation/SimConfigShared" +import SynthesisBrain from "../synthesis_brain/SynthesisBrain" +import PreferencesSystem from "@/systems/preferences/PreferencesSystem" + +const worker: Lazy = new Lazy(() => new WPILibWSWorker()) const PWM_SPEED = ">() +type DeviceName = string +type DeviceData = Map + +type SimMap = Map> +export const simMaps = new Map() + +let simBrain: WPILibBrain | undefined +export function setSimBrain(brain: WPILibBrain | undefined) { + if (brain && !simMaps.has(brain.assemblyName)) { + simMaps.set(brain.assemblyName, new Map()) + } + if (simBrain) worker.getValue().postMessage({ command: "disable" }) + simBrain = brain + if (simBrain) + worker.getValue().postMessage({ + command: "enable", + reconnect: PreferencesSystem.getGlobalPreference("SimAutoReconnect"), + }) +} + +export function hasSimBrain() { + return simBrain != undefined +} + +export function getSimMap(): SimMap | undefined { + if (!simBrain) return undefined + return simMaps.get(simBrain.assemblyName) +} export class SimGeneric { private constructor() {} + public static GetUnsafe(simType: SimType, device: string, field: string): T | undefined + public static GetUnsafe(simType: SimType, device: string, field: string, defaultValue: T): T + public static GetUnsafe(simType: SimType, device: string, field: string, defaultValue?: T): T | undefined { + const map = getSimMap()?.get(simType) + if (!map) { + // console.warn(`No '${simType}' devices found`) + return undefined + } + + const data = map.get(device) + if (!data) { + // console.warn(`No '${simType}' device '${device}' found`) + return undefined + } + + return (data.get(field) as T | undefined) ?? defaultValue + } + + public static Get(simType: SimType, device: string, field: string): T | undefined + public static Get(simType: SimType, device: string, field: string, defaultValue: T): T public static Get(simType: SimType, device: string, field: string, defaultValue?: T): T | undefined { const fieldType = GetFieldType(field) if (fieldType != FieldType.Read && fieldType != FieldType.Both) { @@ -51,45 +164,50 @@ export class SimGeneric { return undefined } - const map = simMap.get(simType) + const map = getSimMap()?.get(simType) if (!map) { - console.warn(`No '${simType}' devices found`) + // console.warn(`No '${simType}' devices found`) return undefined } const data = map.get(device) if (!data) { - console.warn(`No '${simType}' device '${device}' found`) + // console.warn(`No '${simType}' device '${device}' found`) return undefined } - return (data[field] as T | undefined) ?? defaultValue + return (data.get(field) as T | undefined) ?? defaultValue } - public static Set(simType: SimType, device: string, field: string, value: T): boolean { + public static Set( + simType: SimType, + device: string, + field: string, + value: T + ): boolean { const fieldType = GetFieldType(field) if (fieldType != FieldType.Write && fieldType != FieldType.Both) { console.warn(`Field '${field}' is not a write or both field type`) return false } - const map = simMap.get(simType) + const map = getSimMap()?.get(simType) if (!map) { - console.warn(`No '${simType}' devices found`) + // console.warn(`No '${simType}' devices found`) return false } const data = map.get(device) if (!data) { - console.warn(`No '${simType}' device '${device}' found`) + // console.warn(`No '${simType}' device '${device}' found`) return false } - const selectedData: any = {} + const selectedData: { [key: string]: number | boolean | string } = {} selectedData[field] = value + data.set(field, value) - data[field] = value - worker.postMessage({ + worker.getValue().postMessage({ command: "update", data: { type: simType, @@ -103,31 +221,64 @@ export class SimGeneric { } } +export class SimDriverStation { + private constructor() {} + + public static SetMatchTime(time: number) { + SimGeneric.Set(SimType.DriverStation, "", ">match_time", time) + } + + public static SetGameData(gameData: string) { + SimGeneric.Set(SimType.DriverStation, "", ">match_time", gameData) + } + + public static IsEnabled(): boolean { + return SimGeneric.GetUnsafe(SimType.DriverStation, "", ">enabled", false) + } + + public static SetMode(mode: RobotSimMode) { + SimGeneric.Set(SimType.DriverStation, "", ">enabled", mode != RobotSimMode.Disabled) + SimGeneric.Set(SimType.DriverStation, "", ">autonomous", mode == RobotSimMode.Auto) + } + + public static SetStation(station: AllianceStation) { + SimGeneric.Set(SimType.DriverStation, "", ">station", station) + } +} + export class SimPWM { private constructor() {} public static GetSpeed(device: string): number | undefined { - return SimGeneric.Get("PWM", device, PWM_SPEED, 0.0) + return SimDriverStation.IsEnabled() ? SimGeneric.Get(SimType.PWM, device, PWM_SPEED, 0.0) : 0.0 } public static GetPosition(device: string): number | undefined { - return SimGeneric.Get("PWM", device, PWM_POSITION, 0.0) + return SimGeneric.Get(SimType.PWM, device, PWM_POSITION, 0.0) + } + + public static GenSupplier(device: string): SimSupplier { + return { + getSupplierType: () => supplierTypeMap[SimType.PWM]!, + getSupplierValue: () => SimPWM.GetSpeed(device) ?? 0, + } } } export class SimCAN { private constructor() {} - public static GetDeviceWithID(id: number, type: SimType): any { - const id_exp = /.*\[(\d+)\]/g - const entries = [...simMap.entries()].filter(([simType, _data]) => simType == type || simType == "SimDevice") + public static GetDeviceWithID(id: number, type: SimType): DeviceData | undefined { + const id_exp = /SYN.*\[(\d+)\]/g + const map = getSimMap() + if (!map) return undefined + const entries = [...map.entries()].filter(([simType, _data]) => simType == type) for (const [_simType, data] of entries) { for (const key of data.keys()) { const result = [...key.matchAll(id_exp)] if (result?.length <= 0 || result[0].length <= 1) continue const parsed_id = parseInt(result[0][1]) if (parsed_id != id) continue - return data.get(key) } } @@ -138,111 +289,366 @@ export class SimCAN { export class SimCANMotor { private constructor() {} - public static GetDutyCycle(device: string): number | undefined { - return SimGeneric.Get("CANMotor", device, CANMOTOR_DUTY_CYCLE, 0.0) + public static GetPercentOutput(device: string): number | undefined { + return SimDriverStation.IsEnabled() + ? SimGeneric.Get(SimType.CANMotor, device, CANMOTOR_PERCENT_OUTPUT, 0.0) + : 0.0 } - public static SetSupplyVoltage(device: string, voltage: number): boolean { - return SimGeneric.Set("CANMotor", device, CANMOTOR_SUPPLY_VOLTAGE, voltage) + public static GetBrakeMode(device: string): number | undefined { + return SimGeneric.Get(SimType.CANMotor, device, CANMOTOR_BRAKE_MODE, 0.0) + } + + public static GetNeutralDeadband(device: string): number | undefined { + return SimGeneric.Get(SimType.CANMotor, device, CANMOTOR_NEUTRAL_DEADBAND, 0.0) + } + + public static SetSupplyCurrent(device: string, current: number): boolean { + return SimGeneric.Set(SimType.CANMotor, device, CANMOTOR_SUPPLY_CURRENT, current) + } + + public static SetMotorCurrent(device: string, current: number): boolean { + return SimGeneric.Set(SimType.CANMotor, device, CANMOTOR_MOTOR_CURRENT, current) + } + + public static SetBusVoltage(device: string, voltage: number): boolean { + return SimGeneric.Set(SimType.CANMotor, device, CANMOTOR_BUS_VOLTAGE, voltage) } -} + public static GenSupplier(device: string): SimSupplier { + return { + getSupplierType: () => supplierTypeMap[SimType.CANMotor]!, + getSupplierValue: () => SimCANMotor.GetPercentOutput(device) ?? 0, + } + } +} export class SimCANEncoder { private constructor() {} - public static SetRawInputPosition(device: string, rawInputPosition: number): boolean { - return SimGeneric.Set("CANEncoder", device, CANENCODER_RAW_INPUT_POSITION, rawInputPosition) + public static SetVelocity(device: string, velocity: number): boolean { + return SimGeneric.Set(SimType.CANEncoder, device, CANENCODER_VELOCITY, velocity) + } + + public static SetPosition(device: string, position: number): boolean { + return SimGeneric.Set(SimType.CANEncoder, device, CANENCODER_POSITION, position) + } + + public static GenReceiver(device: string): SimReceiver { + return { + getReceiverType: () => receiverTypeMap[SimType.CANEncoder]!, + setReceiverValue: ([count, rate]: NoraNumber2) => { + SimCANEncoder.SetPosition(device, count) + SimCANEncoder.SetVelocity(device, rate) + }, + } } } -worker.addEventListener("message", (eventData: MessageEvent) => { - let data: any | undefined - try { - if (typeof eventData.data == "object") { - data = eventData.data - } else { - data = JSON.parse(eventData.data) +export class SimGyro { + private constructor() {} + + public static SetAngleX(device: string, angle: number): boolean { + return SimGeneric.Set(SimType.Gyro, device, ">angle_x", angle) + } + + public static SetAngleY(device: string, angle: number): boolean { + return SimGeneric.Set(SimType.Gyro, device, ">angle_y", angle) + } + + public static SetAngleZ(device: string, angle: number): boolean { + return SimGeneric.Set(SimType.Gyro, device, ">angle_z", angle) + } + + public static SetRateX(device: string, rate: number): boolean { + return SimGeneric.Set(SimType.Gyro, device, ">rate_x", rate) + } + + public static SetRateY(device: string, rate: number): boolean { + return SimGeneric.Set(SimType.Gyro, device, ">rate_y", rate) + } + + public static SetRateZ(device: string, rate: number): boolean { + return SimGeneric.Set(SimType.Gyro, device, ">rate_z", rate) + } +} + +export class SimAccel { + private constructor() {} + + public static SetX(device: string, accel: number): boolean { + return SimGeneric.Set(SimType.Accel, device, ">x", accel) + } + + public static SetY(device: string, accel: number): boolean { + return SimGeneric.Set(SimType.Accel, device, ">y", accel) + } + + public static SetZ(device: string, accel: number): boolean { + return SimGeneric.Set(SimType.Accel, device, ">z", accel) + } + + public static GenReceiver(device: string): SimReceiver { + return { + getReceiverType: () => receiverTypeMap[SimType.Accel]!, + setReceiverValue: ([x, y, z]: NoraNumber3) => { + SimAccel.SetX(device, x) + SimAccel.SetY(device, y) + SimAccel.SetZ(device, z) + }, + } + } +} + +export class SimDIO { + private constructor() {} + + public static SetValue(device: string, value: boolean): boolean { + return SimGeneric.Set(SimType.DIO, device, "<>value", value) + } + + public static GetValue(device: string): boolean { + return SimGeneric.Get(SimType.DIO, device, "<>value", false) + } + + public static GenReceiver(device: string): SimReceiver { + return { + getReceiverType: () => receiverTypeMap[SimType.DIO]!, + setReceiverValue: (a: NoraNumber) => { + SimDIO.SetValue(device, a > 0.5) + }, + } + } + + public static GenSupplier(device: string): SimSupplier { + return { + getSupplierType: () => receiverTypeMap[SimType.DIO]!, + getSupplierValue: () => (SimDIO.GetValue(device) ? 1 : 0), } - } catch (e) { - console.warn(`Failed to parse data:\n${JSON.stringify(eventData.data)}`) } +} - if (!data || !data.type) { - console.log("No data, bailing out") +export class SimAI { + constructor() {} + + public static SetValue(device: string, value: number): boolean { + return SimGeneric.Set(SimType.AI, device, ">voltage", value) + } + + /** + * The number of averaging bits + */ + public static GetAvgBits(device: string) { + return SimGeneric.Get(SimType.AI, device, "voltage", voltage) + } + /** + * If the accumulator is initialized in the robot program + */ + public static GetAccumInit(device: string) { + return SimGeneric.Get(SimType.AI, device, "accum_value", accum_value) + } + /** + * The number of accumulated values + */ + public static SetAccumCount(device: string, accum_count: number) { + return SimGeneric.Set(SimType.AI, device, ">accum_count", accum_count) + } + /** + * The center value of the accumulator + */ + public static GetAccumCenter(device: string) { + return SimGeneric.Get(SimType.AI, device, "voltage", 0.0) + } +} + +type WSMessage = { + type: string // might be a SimType + device: string // device name + data: Map +} + +worker.getValue().addEventListener("message", (eventData: MessageEvent) => { + let data: WSMessage | undefined + + if (eventData.data.status) { + switch (eventData.data.status) { + case "open": + isConnected = true + break + case "close": + case "error": + isConnected = false + break + default: + return + } return } - const device = data.device - const updateData = data.data - - switch (data.type) { - case "PWM": - UpdateSimMap("PWM", device, updateData) - break - case "Solenoid": - UpdateSimMap("Solenoid", device, updateData) - break - case "SimDevice": - UpdateSimMap("SimDevice", device, updateData) - break - case "CANMotor": - UpdateSimMap("CANMotor", device, updateData) - break - case "CANEncoder": - UpdateSimMap("CANEncoder", device, updateData) - break - default: - break + if (typeof eventData.data == "object") { + data = eventData.data + } else { + try { + data = JSON.parse(eventData.data) + } catch (e) { + console.error(`Failed to parse data:\n${JSON.stringify(eventData.data)}`) + return + } } + + if (!data?.type || !(Object.values(SimType) as string[]).includes(data.type)) return + + UpdateSimMap(data.type as SimType, data.device, data.data) }) -function UpdateSimMap(type: SimType, device: string, updateData: any) { +function UpdateSimMap(type: SimType, device: string, updateData: DeviceData) { + const simMap = getSimMap() + if (!simMap) return let typeMap = simMap.get(type) if (!typeMap) { - typeMap = new Map() + typeMap = new Map() simMap.set(type, typeMap) } let currentData = typeMap.get(device) if (!currentData) { - currentData = {} + currentData = new Map() typeMap.set(device, currentData) } - Object.entries(updateData).forEach(([key, value]) => (currentData[key] = value)) + + Object.entries(updateData).forEach(([key, value]) => currentData.set(key, value)) window.dispatchEvent(new SimMapUpdateEvent(false)) } class WPILibBrain extends Brain { private _simLayer: SimulationLayer + private _assembly: MirabufSceneObject + + private _simOutputs: SimOutput[] = [] + private _simInputs: SimInput[] = [] + private _simFlows: SimFlow[] = [] + + public get assemblyName() { + return this._assembly.assemblyName + } - private _simDevices: SimOutputGroup[] = [] + constructor(assembly: MirabufSceneObject) { + super(assembly.mechanism, "wpilib") - constructor(mechanism: Mechanism) { - super(mechanism) + this._assembly = assembly - this._simLayer = World.SimulationSystem.GetSimulationLayer(mechanism)! + this._simLayer = World.SimulationSystem.GetSimulationLayer(this._mechanism)! if (!this._simLayer) { console.warn("SimulationLayer is undefined") return } + + this.addSimInput(new SimGyroInput("Test Gyro[1]", this._mechanism)) + this.addSimInput(new SimAccelInput("ADXL362[4]", this._mechanism)) + this.addSimInput(new SimDigitalInput("SYN DI[0]", () => Random() > 0.5)) + this.addSimOutput(new SimDigitalOutput("SYN DO[1]")) + this.addSimInput(new SimAnalogInput("SYN AI[0]", () => Random() * 12)) + this.addSimOutput(new SimAnalogOutput("SYN AO[1]")) + + this.loadSimConfig() + + World.SceneRenderer.sceneObjects.forEach(v => { + if (v instanceof MirabufSceneObject && v.brain?.brainType == "wpilib") { + v.brain = new SynthesisBrain(v, v.assemblyName) + } + }) + } + + public addSimOutput(device: SimOutput) { + this._simOutputs.push(device) } - public addSimOutputGroup(device: SimOutputGroup) { - this._simDevices.push(device) + public addSimInput(input: SimInput) { + this._simInputs.push(input) + } + + public addSimFlow(flow: SimFlow): boolean { + if (validate(flow.supplier, flow.receiver)) { + this._simFlows.push(flow) + return true + } + return false + } + + public loadSimConfig(): boolean { + this._simFlows = [] + const configData = this._assembly.simConfigData + if (!configData) return false + + const flows = SimConfig.Compile(configData, this._assembly) + if (!flows) { + console.error(`Failed to compile saved simulation configuration data for '${this.assemblyName}'`) + return false + } + + let counter = 0 + flows.forEach(x => { + if (!this.addSimFlow(x)) { + console.debug("Failed to validate flow, skipping...") + } else { + counter++ + } + }) + console.debug(`${counter} Flows added!`) + return true } public Update(deltaT: number): void { - this._simDevices.forEach(d => d.Update(deltaT)) + this._simOutputs.forEach(d => d.Update(deltaT)) + this._simInputs.forEach(i => i.Update(deltaT)) + this._simFlows.forEach(({ supplier, receiver }) => { + receiver.setReceiverValue(supplier.getSupplierValue()) + }) } public Enable(): void { - worker.postMessage({ command: "connect" }) + setSimBrain(this) + // worker.getValue().postMessage({ command: "enable", reconnect: RECONNECT }) } public Disable(): void { - worker.postMessage({ command: "disconnect" }) + if (simBrain == this) { + setSimBrain(undefined) + } + // worker.getValue().postMessage({ command: "disable" }) } } @@ -263,53 +669,3 @@ export class SimMapUpdateEvent extends Event { } export default WPILibBrain - -abstract class SimOutputGroup { - public name: string - public ports: number[] - public drivers: Driver[] - public type: SimType - - public constructor(name: string, ports: number[], drivers: Driver[], type: SimType) { - this.name = name - this.ports = ports - this.drivers = drivers - this.type = type - } - - public abstract Update(deltaT: number): void -} - -export class PWMGroup extends SimOutputGroup { - public constructor(name: string, ports: number[], drivers: Driver[]) { - super(name, ports, drivers, "PWM") - } - - public Update(_deltaT: number) { - // let average = 0; - for (const port of this.ports) { - const speed = SimPWM.GetSpeed(`${port}`) ?? 0 - // average += speed; - console.log(port, speed) - } - // average /= this.ports.length - - // this.drivers.forEach(d => { - // (d as WheelDriver).targetWheelSpeed = average * 40 - // d.Update(_deltaT) - // }) - } -} - -export class CANGroup extends SimOutputGroup { - public constructor(name: string, ports: number[], drivers: Driver[]) { - super(name, ports, drivers, "CANMotor") - } - - public Update(_deltaT: number) { - for (const port of this.ports) { - const device = SimCAN.GetDeviceWithID(port, this.type) - console.log(port, device) - } - } -} diff --git a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts index 836dbd0d4f..49dca0465b 100644 --- a/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts +++ b/fission/src/systems/simulation/wpilib_brain/WPILibWSWorker.ts @@ -4,7 +4,19 @@ let socket: WebSocket | undefined = undefined const connectMutex = new Mutex() -async function tryConnect(port: number | undefined): Promise { +let intervalHandle: NodeJS.Timeout | undefined = undefined +let reconnect = false +const RECONNECT_INTERVAL = 1000 + +function socketOpen(): boolean { + return (socket && socket.readyState == WebSocket.OPEN) ?? false +} + +function socketConnecting(): boolean { + return (socket && socket.readyState == WebSocket.CONNECTING) ?? false +} + +async function tryConnect(port?: number): Promise { await connectMutex .runExclusive(() => { if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) { @@ -21,6 +33,9 @@ async function tryConnect(port: number | undefined): Promise { console.log("WS Could not open") self.postMessage({ status: "error" }) }) + socket.addEventListener("close", () => { + self.postMessage({ status: "close" }) + }) socket.addEventListener("message", onMessage) }) @@ -29,37 +44,54 @@ async function tryConnect(port: number | undefined): Promise { async function tryDisconnect(): Promise { await connectMutex.runExclusive(() => { - if (!socket) { - return - } + if (!socket) return - socket?.close() + socket.close() socket = undefined }) } +// Posts incoming messages function onMessage(event: MessageEvent) { - // console.log(`${JSON.stringify(JSON.parse(event.data), null, '\t')}`) self.postMessage(event.data) } +// Sends outgoing messages self.addEventListener("message", e => { switch (e.data.command) { - case "connect": - tryConnect(e.data.port) + case "enable": { + reconnect = e.data.reconnect ?? false + const intervalFunc = () => { + if (intervalHandle != undefined && !socketOpen() && !socketConnecting()) { + tryConnect() + } + + if (!reconnect) { + clearInterval(intervalHandle) + intervalHandle = undefined + } + } + if (intervalHandle != undefined) { + clearInterval(intervalHandle) + } + intervalHandle = setInterval(intervalFunc, RECONNECT_INTERVAL) break - case "disconnect": + } + case "disable": { + clearInterval(intervalHandle) + intervalHandle = undefined tryDisconnect() break - case "update": - if (socket) { - socket.send(JSON.stringify(e.data.data)) + } + case "update": { + if (socketOpen()) { + socket!.send(JSON.stringify(e.data.data)) } break - default: + } + default: { console.warn(`Unrecognized command '${e.data.command}'`) break + } } }) - -console.log("Worker started") diff --git a/fission/src/test/InputSystem.test.ts b/fission/src/test/InputSystem.test.ts new file mode 100644 index 0000000000..fc63f5e829 --- /dev/null +++ b/fission/src/test/InputSystem.test.ts @@ -0,0 +1,79 @@ +import { test, describe, assert, expect } from "vitest" +import InputSystem, { EmptyModifierState, ModifierState } from "@/systems/input/InputSystem" +import InputSchemeManager from "@/systems/input/InputSchemeManager" +import DefaultInputs from "@/systems/input/DefaultInputs" + +describe("Input Scheme Manager Checks", () => { + test("Available Schemes", () => { + assert(InputSchemeManager.availableInputSchemes[0].schemeName == DefaultInputs.ernie().schemeName) + assert(InputSchemeManager.defaultInputSchemes.length >= 1) + + const startingLength = InputSchemeManager.availableInputSchemes.length + InputSchemeManager.addCustomScheme(DefaultInputs.newBlankScheme) + + expect(InputSchemeManager.availableInputSchemes.length).toBe(startingLength + 1) + }) + test("Add a Custom Scheme", () => { + const startingLength = InputSchemeManager.availableInputSchemes.length + InputSchemeManager.addCustomScheme(DefaultInputs.newBlankScheme) + + assert((InputSchemeManager.availableInputSchemes.length = startingLength + 1)) + }) + test("Get Random Names", () => { + const names: string[] = [] + for (let i = 0; i < 20; i++) { + const name = InputSchemeManager.randomAvailableName + expect(names.includes(name)).toBe(false) + assert(name != undefined) + expect(name.length).toBeGreaterThan(0) + + const scheme = DefaultInputs.newBlankScheme + scheme.schemeName = name + + InputSchemeManager.addCustomScheme(scheme) + + names.push(name) + } + }) +}) + +describe("Input System Checks", () => { + const inputSystem = new InputSystem() + + test("Brain Map Exists?", () => { + assert(InputSystem.brainIndexSchemeMap != undefined) + }) + + test("Inputs are Zero", () => { + expect(InputSystem.getInput("arcadeDrive", 0)).toBe(0) + expect(InputSystem.getGamepadAxis(0)).toBe(0) + expect(InputSystem.getInput("randomInputThatDoesNotExist", 1273)).toBe(0) + expect(InputSystem.isKeyPressed("keyA")).toBe(false) + expect(InputSystem.isKeyPressed("ajhsekff")).toBe(false) + expect(InputSystem.isGamepadButtonPressed(1)).toBe(false) + }) + + test("Modifier State Comparison", () => { + const allFalse: ModifierState = { + alt: false, + ctrl: false, + shift: false, + meta: false, + } + + const differentState: ModifierState = { + alt: false, + ctrl: true, + shift: false, + meta: true, + } + + inputSystem.Update(-1) + + expect(InputSystem.compareModifiers(allFalse, EmptyModifierState)).toBe(true) + expect(InputSystem.compareModifiers(allFalse, InputSystem.currentModifierState)).toBe(true) + expect(InputSystem.compareModifiers(differentState, InputSystem.currentModifierState)).toBe(false) + expect(InputSystem.compareModifiers(differentState, differentState)).toBe(true) + expect(InputSystem.compareModifiers(differentState, allFalse)).toBe(false) + }) +}) diff --git a/fission/src/test/PreferencesSystem.test.ts b/fission/src/test/PreferencesSystem.test.ts new file mode 100644 index 0000000000..22828937d7 --- /dev/null +++ b/fission/src/test/PreferencesSystem.test.ts @@ -0,0 +1,34 @@ +import PreferencesSystem from "@/systems/preferences/PreferencesSystem" +import { test, describe, expect } from "vitest" + +describe("Preferences System", () => { + test("Setting without saving", () => { + PreferencesSystem.setGlobalPreference("ZoomSensitivity", 15) + PreferencesSystem.setGlobalPreference("RenderSceneTags", true) + PreferencesSystem.setGlobalPreference("RenderScoreboard", false) + + expect(PreferencesSystem.getGlobalPreference("ZoomSensitivity")).toBe(15) + expect(PreferencesSystem.getGlobalPreference("RenderSceneTags")).toBe(true) + expect(PreferencesSystem.getGlobalPreference("RenderScoreboard")).toBe(false) + }) + test("Reset to default if undefined", () => { + PreferencesSystem.setGlobalPreference("ZoomSensitivity", undefined) + PreferencesSystem.setGlobalPreference("RenderSceneTags", undefined) + PreferencesSystem.setGlobalPreference("RenderScoreboard", undefined) + + expect(PreferencesSystem.getGlobalPreference("ZoomSensitivity")).toBe(15) + expect(PreferencesSystem.getGlobalPreference("RenderSceneTags")).toBe(true) + expect(PreferencesSystem.getGlobalPreference("RenderScoreboard")).toBe(true) + }) + test("Setting then saving", () => { + PreferencesSystem.setGlobalPreference("ZoomSensitivity", 13) + PreferencesSystem.setGlobalPreference("RenderSceneTags", true) + PreferencesSystem.setGlobalPreference("RenderScoreboard", false) + + PreferencesSystem.savePreferences() + + expect(PreferencesSystem.getGlobalPreference("ZoomSensitivity")).toBe(13) + expect(PreferencesSystem.getGlobalPreference("RenderSceneTags")).toBe(true) + expect(PreferencesSystem.getGlobalPreference("RenderScoreboard")).toBe(false) + }) +}) diff --git a/fission/src/test/ui/Button.test.tsx b/fission/src/test/ui/Button.test.tsx new file mode 100644 index 0000000000..f911494d09 --- /dev/null +++ b/fission/src/test/ui/Button.test.tsx @@ -0,0 +1,29 @@ +import { render, fireEvent, getByText } from "@testing-library/react" +import { assert, describe, expect, test } from "vitest" +import Button from "@/ui/components/Button" + +describe("Button", () => { + test("Click Enabled Button", () => { + let buttonClicked = false + const container = render( ) } -export let MainHUD_AddToast: (type: ToastType, title: string, description: string) => void = (_a, _b, _c) => {} - const variants = { open: { opacity: 1, y: "-50%", x: 0 }, closed: { opacity: 0, y: "-50%", x: "-100%" }, @@ -55,7 +56,7 @@ const MainHUD: React.FC = () => { const { addToast } = useToastContext() const [isOpen, setIsOpen] = useState(false) - MainHUD_AddToast = addToast + setAddToast(addToast) const [userInfo, setUserInfo] = useState(APS.userInfo) @@ -68,12 +69,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" >
- - + />
} + icon={SynthesisIcons.Add} larger={true} onClick={() => openPanel("import-mirabuf")} /> -
+ } - onClick={() => openModal("manage-assemblies")} + value={"Configure Assets"} + icon={SynthesisIcons.Wrench} + onClick={() => openPanel("configure")} /> - } onClick={() => openModal("settings")} /> - {/* } onClick={() => openModal("view")} /> */} - } onClick={() => openModal("change-inputs")} /> } - onClick={() => openModal("import-local-mirabuf")} + value={"General Settings"} + icon={SynthesisIcons.Gear} + onClick={() => openModal("settings")} /> -
-
- } - onClick={() => { - openPanel("scoring-zones") - }} - /> - } onClick={() => openModal("config-robot")} /> + {/** Will be coming soonish...tm */} + {/* openModal("view")} + /> */} } + icon={SynthesisIcons.Bug} onClick={() => { openPanel("debug") }} /> -
+ {userInfo ? ( } larger={true} - onClick={() => APS.logout()} + onClick={() => openModal("aps-management")} /> ) : ( } + icon={SynthesisIcons.People} larger={true} onClick={() => APS.requestAuthCode()} /> diff --git a/fission/src/ui/components/Modal.tsx b/fission/src/ui/components/Modal.tsx index a0570db82e..ad7357bb12 100644 --- a/fission/src/ui/components/Modal.tsx +++ b/fission/src/ui/components/Modal.tsx @@ -56,64 +56,83 @@ const Modal: React.FC = ({
{name && ( )}
{children}
- + {(cancelEnabled || middleEnabled || acceptEnabled) && ( + + )}
) diff --git a/fission/src/ui/components/Panel.tsx b/fission/src/ui/components/Panel.tsx index b8faee4977..06399c4ed7 100644 --- a/fission/src/ui/components/Panel.tsx +++ b/fission/src/ui/components/Panel.tsx @@ -93,6 +93,7 @@ type PanelProps = { children?: ReactNode className?: string contentClassName?: string + full?: boolean } const Panel: React.FC = ({ @@ -116,30 +117,45 @@ const Panel: React.FC = ({ acceptBlocked = false, className, contentClassName, + full = false, }) => { const { closePanel } = usePanelControlContext() const iconEl: ReactNode = typeof icon === "string" ? Icon : icon openLocation ||= "center" sidePadding ||= 16 const locationClasses = getLocationClasses(openLocation, sidePadding) + + const mainSizing = full ? "left-5 right-5 top-5 bottom-5" : "max-h-[95vh] max-w-[50vw]" + const contentSizing = full ? "grow" : "max-h-[75vh]" + return (
{name && ( )}
{children}
@@ -158,7 +174,9 @@ const Panel: React.FC = ({ }} 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 +188,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 +203,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/Scene.tsx b/fission/src/ui/components/Scene.tsx index 6ba35ddb6f..034d865503 100644 --- a/fission/src/ui/components/Scene.tsx +++ b/fission/src/ui/components/Scene.tsx @@ -17,8 +17,6 @@ function Scene({ useStats }: SceneProps) { World.InitWorld() if (refContainer.current) { - console.debug("Adding ThreeJs to DOM") - const sr = World.SceneRenderer sr.renderer.domElement.style.width = "100%" sr.renderer.domElement.style.height = "100%" @@ -30,7 +28,6 @@ function Scene({ useStats }: SceneProps) { }) if (useStats && !stats) { - console.log("Adding stat") stats = new Stats() stats.dom.style.position = "absolute" stats.dom.style.top = "0px" diff --git a/fission/src/ui/components/SceneOverlay.tsx b/fission/src/ui/components/SceneOverlay.tsx index 679b507bc4..dc4cb9f758 100644 --- a/fission/src/ui/components/SceneOverlay.tsx +++ b/fission/src/ui/components/SceneOverlay.tsx @@ -33,7 +33,9 @@ function SceneOverlay() { transform: "translate(-50%, -100%)", }} > - +
)) }, []) diff --git a/fission/src/ui/components/SelectButton.tsx b/fission/src/ui/components/SelectButton.tsx index d7c04f9943..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." + )}
-
{ - setChosenKey(selectedInput ? e.code : "") - setModifierState({ - ctrl: e.ctrlKey, - alt: e.altKey, - shift: e.shiftKey, - meta: e.metaKey, - }) - }} - > - {selectedScheme ? ( - <> - - {selectedScheme.inputs.map(c => { - if (!useGamepad) { - // Keyboard button - if (c instanceof ButtonInput) { - return KeyboardButtonSelection(c) - } - // Keyboard Axis - else if (c instanceof AxisInput) { - return KeyboardAxisSelection(c) - } - } else { - // Joystick Button - if (c instanceof ButtonInput) { - return JoystickButtonSelection(c) - } - - // Gamepad axis - else if (c instanceof AxisInput) { - return ( -
- {useButtons[c.inputName] - ? GamepadButtonAxisSelection(c) - : // Gamepad joystick axis - JoystickAxisSelection(c)} - - {/* // Button to switch between two buttons and a joystick axis */} - { - setUseButtons(prevState => ({ - ...prevState, - [c.inputName]: val, - })) - c.useGamepadButtons = val - }} - /> - {/* // Button to invert the joystick axis */} - { - c.joystickInverted = val - }} - /> -
- ) - } - } - })} -
- - ) : ( - - )} -
- - - - ) -} - -export default ChangeInputsModal diff --git a/fission/src/ui/modals/configuring/ChooseMultiplayerModeModal.tsx b/fission/src/ui/modals/configuring/ChooseMultiplayerModeModal.tsx index 64cf36c77c..58b3fbda05 100644 --- a/fission/src/ui/modals/configuring/ChooseMultiplayerModeModal.tsx +++ b/fission/src/ui/modals/configuring/ChooseMultiplayerModeModal.tsx @@ -1,11 +1,17 @@ import React from "react" import Modal, { ModalPropsImpl } from "@/components/Modal" -import { FaGear } from "react-icons/fa6" import Button from "@/components/Button" +import { SynthesisIcons } from "@/ui/components/StyledComponents" const ChooseMultiplayerModeModal: React.FC = ({ modalId }) => { return ( - } modalId={modalId} cancelEnabled={false} acceptEnabled={false}> + - ) -} - -const AssemblyCard: React.FC = ({ mira, update }) => { - const { openPanel } = usePanelControlContext() - const { closeModal } = useModalControlContext() - - const brain = useMemo(() => (mira.brain as SynthesisBrain)?.brainIndex, [mira]) - - return ( -
- -
-
-
- ) -} - -const ManageAssembliesModal: React.FC = ({ modalId }) => { - const [_, update] = useReducer(x => !x, false) - - const assemblies = [...World.SceneRenderer.sceneObjects.entries()] - .filter(x => { - const y = x[1] instanceof MirabufSceneObject - return y - }) - .map(x => x[1] as MirabufSceneObject) - - return ( - } modalId={modalId} acceptEnabled={false} cancelName="Back"> -
- - {assemblies.map(x => ( - - ))} -
-
- ) -} - -export default ManageAssembliesModal diff --git a/fission/src/ui/modals/spawning/MatchModeModal.tsx b/fission/src/ui/modals/spawning/MatchModeModal.tsx index 7ab0466e2c..5b0a87e155 100644 --- a/fission/src/ui/modals/spawning/MatchModeModal.tsx +++ b/fission/src/ui/modals/spawning/MatchModeModal.tsx @@ -1,7 +1,7 @@ -import { FaGear } from "react-icons/fa6" import Modal, { ModalPropsImpl } from "@/components/Modal" import Dropdown from "@/components/Dropdown" import Label, { LabelSize } from "@/components/Label" +import { SynthesisIcons } from "@/ui/components/StyledComponents" const MatchModeModal: React.FC = ({ modalId }) => { const robotsPerAlliance = 3 @@ -17,7 +17,7 @@ const MatchModeModal: React.FC = ({ modalId }) => { return ( } + icon={SynthesisIcons.Gear} modalId={modalId} acceptName="Load" cancelEnabled={false} diff --git a/fission/src/ui/modals/spawning/SpawningModals.tsx b/fission/src/ui/modals/spawning/SpawningModals.tsx deleted file mode 100644 index a115808f68..0000000000 --- a/fission/src/ui/modals/spawning/SpawningModals.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import React, { useEffect, useState } from "react" -import Modal, { ModalPropsImpl } from "../../components/Modal" -import { FaPlus } from "react-icons/fa6" -import Stack, { StackDirection } from "../../components/Stack" -import Button, { ButtonSize } from "../../components/Button" -import { useModalControlContext } from "@/ui/ModalContext" -import Label, { LabelSize } from "@/components/Label" -import World from "@/systems/World" -import { useTooltipControlContext } from "@/ui/TooltipContext" -import MirabufCachingService, { MirabufCacheInfo, MiraType } from "@/mirabuf/MirabufLoader" -import { CreateMirabuf } from "@/mirabuf/MirabufSceneObject" - -interface MirabufRemoteInfo { - displayName: string - src: string -} - -interface MirabufRemoteCardProps { - info: MirabufRemoteInfo - select: (info: MirabufRemoteInfo) => void -} - -const MirabufRemoteCard: React.FC = ({ info, select }) => { - return ( -
- -
- ) -} - -interface MirabufCacheCardProps { - info: MirabufCacheInfo - select: (info: MirabufCacheInfo) => void -} - -const MirabufCacheCard: React.FC = ({ info, select }) => { - return ( -
- -
- ) -} - -export const AddRobotsModal: React.FC = ({ modalId }) => { - const { showTooltip } = useTooltipControlContext() - const { closeModal } = useModalControlContext() - - const [cachedRobots, setCachedRobots] = useState(undefined) - - // prettier-ignore - useEffect(() => { - (async () => { - const map = MirabufCachingService.GetCacheMap(MiraType.ROBOT) - setCachedRobots(Object.values(map)) - })() - }, []) - - const [remoteRobots, setRemoteRobots] = useState(undefined) - - // prettier-ignore - useEffect(() => { - (async () => { - fetch("/api/mira/manifest.json") - .then(x => x.json()) - .then(x => { - const map = MirabufCachingService.GetCacheMap(MiraType.ROBOT) - const robots: MirabufRemoteInfo[] = [] - for (const src of x["robots"]) { - if (typeof src == "string") { - const str = `/api/mira/Robots/${src}` - if (!map[str]) robots.push({ displayName: src, src: str }) - } else { - if (!map[src["src"]]) robots.push({ displayName: src["displayName"], src: src["src"] }) - } - } - setRemoteRobots(robots) - }) - })() - }, []) - - const selectCache = async (info: MirabufCacheInfo) => { - const assembly = await MirabufCachingService.Get(info.id, MiraType.ROBOT) - - if (assembly) { - showTooltip("controls", [ - { control: "WASD", description: "Drive" }, - { control: "E", description: "Intake" }, - { control: "Q", description: "Dispense" }, - ]) - - CreateMirabuf(assembly).then(x => { - if (x) { - World.SceneRenderer.RegisterSceneObject(x) - } - }) - - if (!info.name) - MirabufCachingService.CacheInfo(info.cacheKey, MiraType.ROBOT, assembly.info?.name ?? undefined) - } else { - console.error("Failed to spawn robot") - } - - closeModal() - } - - const selectRemote = async (info: MirabufRemoteInfo) => { - const cacheInfo = await MirabufCachingService.CacheRemote(info.src, MiraType.ROBOT) - - if (!cacheInfo) { - console.error("Failed to cache robot") - closeModal() - } else { - selectCache(cacheInfo) - } - } - - return ( - } modalId={modalId} acceptEnabled={false}> -
- - {cachedRobots ? cachedRobots!.map(x => MirabufCacheCard({ info: x, select: selectCache })) : <>} - - {remoteRobots ? remoteRobots!.map(x => MirabufRemoteCard({ info: x, select: selectRemote })) : <>} -
-
- ) -} - -export const AddFieldsModal: React.FC = ({ modalId }) => { - const { closeModal } = useModalControlContext() - - const [cachedFields, setCachedFields] = useState(undefined) - - // prettier-ignore - useEffect(() => { - (async () => { - const map = MirabufCachingService.GetCacheMap(MiraType.FIELD) - setCachedFields(Object.values(map)) - })() - }, []) - - const [remoteFields, setRemoteFields] = useState(undefined) - - // prettier-ignore - useEffect(() => { - (async () => { - fetch("/api/mira/manifest.json") - .then(x => x.json()) - .then(x => { - // TODO: Skip already cached fields - const map = MirabufCachingService.GetCacheMap(MiraType.FIELD) - const fields: MirabufRemoteInfo[] = [] - for (const src of x["fields"]) { - if (typeof src == "string") { - const newSrc = `/api/mira/Fields/${src}` - if (!map[newSrc]) fields.push({ displayName: src, src: newSrc }) - } else { - if (!map[src["src"]]) - fields.push({ displayName: src["displayName"], src: src["src"] }) - } - } - setRemoteFields(fields) - }) - })() - }, []) - - const selectCache = async (info: MirabufCacheInfo) => { - const assembly = await MirabufCachingService.Get(info.id, MiraType.FIELD) - - if (assembly) { - CreateMirabuf(assembly).then(x => { - if (x) { - World.SceneRenderer.RegisterSceneObject(x) - } - }) - - if (!info.name) - MirabufCachingService.CacheInfo(info.cacheKey, MiraType.FIELD, assembly.info?.name ?? undefined) - } else { - console.error("Failed to spawn field") - } - - closeModal() - } - - const selectRemote = async (info: MirabufRemoteInfo) => { - const cacheInfo = await MirabufCachingService.CacheRemote(info.src, MiraType.FIELD) - - if (!cacheInfo) { - console.error("Failed to cache field") - closeModal() - } else { - selectCache(cacheInfo) - } - } - - return ( - } modalId={modalId} acceptEnabled={false}> -
- - {cachedFields ? cachedFields!.map(x => MirabufCacheCard({ info: x, select: selectCache })) : <>} - - {remoteFields ? remoteFields!.map(x => MirabufRemoteCard({ info: x, select: selectRemote })) : <>} -
-
- ) -} - -export const SpawningModal: React.FC = ({ modalId }) => { - const { openModal } = useModalControlContext() - - return ( - } modalId={modalId}> - - - ) - })} - - {/* TODO: remove the accept button on this version */} - - ) : ( - <> - {/* Button for user to select the parent node */} - trySetSelectedNode(body.GetID())} - /> - - {/* Slider for user to set velocity of ejector configuration */} - { - setZoneSize(vel as number) - }} - step={0.01} - /> - { - if (transformGizmo) { - const robotTransformation = JoltMat44_ThreeMatrix4( - World.PhysicsSystem.GetBody(selectedRobot.GetRootNodeId()!).GetWorldTransform() - ) - transformGizmo.mesh.position.setFromMatrixPosition(robotTransformation) - transformGizmo.mesh.rotation.setFromRotationMatrix(robotTransformation) - } - setZoneSize((MIN_ZONE_SIZE + MAX_ZONE_SIZE) / 2.0) - setSelectedNode(selectedRobot?.rootNodeId) - }} - /> - - )} - - ) -} - -export default ConfigureGamepiecePickupPanel diff --git a/fission/src/ui/panels/configuring/ConfigureShotTrajectoryPanel.tsx b/fission/src/ui/panels/configuring/ConfigureShotTrajectoryPanel.tsx deleted file mode 100644 index 6f8d2cd046..0000000000 --- a/fission/src/ui/panels/configuring/ConfigureShotTrajectoryPanel.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import * as THREE from "three" -import { useCallback, useEffect, useMemo, useState } from "react" -import { FaGear } from "react-icons/fa6" -import Panel, { PanelPropsImpl } from "@/components/Panel" -import SelectButton from "@/components/SelectButton" -import TransformGizmos from "@/ui/components/TransformGizmos" -import Slider from "@/ui/components/Slider" -import Jolt from "@barclah/jolt-physics" -import Label from "@/ui/components/Label" -import PreferencesSystem from "@/systems/preferences/PreferencesSystem" -import Button from "@/ui/components/Button" -import MirabufSceneObject, { RigidNodeAssociate } from "@/mirabuf/MirabufSceneObject" -import World from "@/systems/World" -import { MiraType } from "@/mirabuf/MirabufLoader" -import { - Array_ThreeMatrix4, - JoltMat44_ThreeMatrix4, - ReactRgbaColor_ThreeColor, - ThreeMatrix4_Array, -} from "@/util/TypeConversions" -import { useTheme } from "@/ui/ThemeContext" -import LabeledButton, { LabelPlacement } from "@/ui/components/LabeledButton" -import { RigidNodeId } from "@/mirabuf/MirabufParser" - -// slider constants -const MIN_VELOCITY = 0.0 -const MAX_VELOCITY = 20.0 - -/** - * Saves ejector configuration to selected robot. - * - * Math Explanation: - * Let W be the world transformation matrix of the gizmo. - * Let R be the world transformation matrix of the selected robot node. - * Let L be the local transformation matrix of the gizmo, relative to the selected robot node. - * - * We are given W and R, and want to save L with the robot. This way when we create - * the ejection point afterwards, it will be relative to the selected robot node. - * - * W = L R - * L = W R^(-1) - * - * ThreeJS sets the standard multiplication operation for matrices to be premultiply. I really - * don't like this terminology as it's thrown me off multiple times, but I suppose it does go - * against most other multiplication operations. - * - * @param ejectorVelocity Velocity to eject gamepiece at. - * @param gizmo Reference to the transform gizmo object. - * @param selectedRobot Selected robot to save data to. - * @param selectedNode Selected node that configuration is relative to. - */ -function save( - ejectorVelocity: number, - gizmo: TransformGizmos, - selectedRobot: MirabufSceneObject, - selectedNode?: RigidNodeId -) { - if (!selectedRobot?.ejectorPreferences || !gizmo) { - return - } - - selectedNode ??= selectedRobot.rootNodeId - - const nodeBodyId = selectedRobot.mechanism.nodeToBody.get(selectedNode) - if (!nodeBodyId) { - return - } - - const gizmoTransformation = gizmo.mesh.matrixWorld - const robotTransformation = JoltMat44_ThreeMatrix4(World.PhysicsSystem.GetBody(nodeBodyId).GetWorldTransform()) - const deltaTransformation = gizmoTransformation.premultiply(robotTransformation.invert()) - - selectedRobot.ejectorPreferences.deltaTransformation = ThreeMatrix4_Array(deltaTransformation) - selectedRobot.ejectorPreferences.parentNode = selectedNode - selectedRobot.ejectorPreferences.ejectorVelocity = ejectorVelocity - - PreferencesSystem.savePreferences() -} - -const ConfigureShotTrajectoryPanel: React.FC = ({ panelId, openLocation, sidePadding }) => { - const { currentTheme, themes } = useTheme() - const theme = useMemo(() => { - return themes[currentTheme] - }, [currentTheme, themes]) - - const [selectedRobot, setSelectedRobot] = useState(undefined) - const [selectedNode, setSelectedNode] = useState(undefined) - const [ejectorVelocity, setEjectorVelocity] = useState((MIN_VELOCITY + MAX_VELOCITY) / 2.0) - const [transformGizmo, setTransformGizmo] = useState(undefined) - const robots = useMemo(() => { - const assemblies = [...World.SceneRenderer.sceneObjects.values()].filter(x => { - if (x instanceof MirabufSceneObject) { - return x.miraType === MiraType.ROBOT - } - return false - }) as MirabufSceneObject[] - return assemblies - }, []) - - // Not sure I like this, but made it a state and effect rather than a memo to add the cleanup to the end - useEffect(() => { - if (!selectedRobot?.ejectorPreferences) { - setTransformGizmo(undefined) - return - } - - const gizmo = new TransformGizmos( - new THREE.Mesh( - new THREE.ConeGeometry(0.1, 0.4, 4).rotateX(Math.PI / 2.0).translate(0, 0, 0.2), - World.SceneRenderer.CreateToonMaterial(ReactRgbaColor_ThreeColor(theme.HighlightSelect.color)) - ) - ) - - ;(gizmo.mesh.material as THREE.Material).depthTest = false - gizmo.AddMeshToScene() - gizmo.CreateGizmo("translate", 1.5) - gizmo.CreateGizmo("rotate", 2.0) - - const deltaTransformation = Array_ThreeMatrix4(selectedRobot.ejectorPreferences.deltaTransformation) - - let nodeBodyId = selectedRobot.mechanism.nodeToBody.get( - selectedRobot.ejectorPreferences.parentNode ?? selectedRobot.rootNodeId - ) - if (!nodeBodyId) { - // In the event that something about the id generation for the rigid nodes changes and parent node id is no longer in use - nodeBodyId = selectedRobot.mechanism.nodeToBody.get(selectedRobot.rootNodeId)! - } - - /** W = L x R. See save() for math details */ - const robotTransformation = JoltMat44_ThreeMatrix4(World.PhysicsSystem.GetBody(nodeBodyId).GetWorldTransform()) - const gizmoTransformation = deltaTransformation.premultiply(robotTransformation) - - gizmo.mesh.position.setFromMatrixPosition(gizmoTransformation) - gizmo.mesh.rotation.setFromRotationMatrix(gizmoTransformation) - - setTransformGizmo(gizmo) - - return () => { - gizmo.RemoveGizmos() - setTransformGizmo(undefined) - } - }, [selectedRobot, theme]) - - useEffect(() => { - if (selectedRobot?.ejectorPreferences) { - setEjectorVelocity(selectedRobot.ejectorPreferences.ejectorVelocity) - setSelectedNode(selectedRobot.ejectorPreferences.parentNode) - } else { - setSelectedNode(undefined) - } - }, [selectedRobot]) - - useEffect(() => { - World.PhysicsSystem.HoldPause() - - return () => { - World.PhysicsSystem.ReleasePause() - } - }, []) - - const trySetSelectedNode = useCallback( - (body: Jolt.BodyID) => { - if (!selectedRobot) { - return false - } - - const assoc = World.PhysicsSystem.GetBodyAssociation(body) as RigidNodeAssociate - if (!assoc || !assoc.sceneObject || assoc.sceneObject != selectedRobot) { - return false - } - - setSelectedNode(assoc.rigidNodeId) - return true - }, - [selectedRobot] - ) - - return ( - } - panelId={panelId} - openLocation={openLocation} - sidePadding={sidePadding} - onAccept={() => { - if (transformGizmo && selectedRobot) { - save(ejectorVelocity, transformGizmo, selectedRobot, selectedNode) - const currentGp = selectedRobot.activeEjectable - selectedRobot.SetEjectable(undefined, true) - selectedRobot.SetEjectable(currentGp) - } - }} - onCancel={() => {}} - acceptEnabled={selectedRobot?.ejectorPreferences != undefined} - > - {selectedRobot?.ejectorPreferences == undefined ? ( - <> - - {/** Scroll view for selecting a robot to configure */} -
- {robots.map(mirabufSceneObject => { - return ( - - ) - })} -
- {/* TODO: remove the accept button on this version */} - - ) : ( - <> - {/* Button for user to select the parent node */} - trySetSelectedNode(body.GetID())} - /> - - {/* Slider for user to set velocity of ejector configuration */} - { - setEjectorVelocity(vel as number) - }} - step={0.01} - /> - { - if (transformGizmo) { - const robotTransformation = JoltMat44_ThreeMatrix4( - World.PhysicsSystem.GetBody(selectedRobot.GetRootNodeId()!).GetWorldTransform() - ) - transformGizmo.mesh.position.setFromMatrixPosition(robotTransformation) - transformGizmo.mesh.rotation.setFromRotationMatrix(robotTransformation) - } - setEjectorVelocity((MIN_VELOCITY + MAX_VELOCITY) / 2.0) - setSelectedNode(selectedRobot?.rootNodeId) - }} - /> - - )} -
- ) -} - -export default ConfigureShotTrajectoryPanel diff --git a/fission/src/ui/panels/configuring/TransformAssemblyPanel.tsx b/fission/src/ui/panels/configuring/TransformAssemblyPanel.tsx new file mode 100644 index 0000000000..ce9b3bd729 --- /dev/null +++ b/fission/src/ui/panels/configuring/TransformAssemblyPanel.tsx @@ -0,0 +1,51 @@ +import Panel, { PanelPropsImpl } from "@/components/Panel" +import { SynthesisIcons } from "@/ui/components/StyledComponents" +import { useEffect, useMemo } from "react" +import { getSpotlightAssembly } from "@/mirabuf/MirabufSceneObject" +import TransformGizmoControl from "@/ui/components/TransformGizmoControl" +import World from "@/systems/World" +import { PAUSE_REF_ASSEMBLY_MOVE } from "@/systems/physics/PhysicsSystem" + +const TransformAssemblyPanel: React.FC = ({ panelId }) => { + const targetAssembly = useMemo(() => { + return getSpotlightAssembly() + }, []) + + useEffect(() => { + World.PhysicsSystem.HoldPause(PAUSE_REF_ASSEMBLY_MOVE) + + return () => { + World.PhysicsSystem.ReleasePause(PAUSE_REF_ASSEMBLY_MOVE) + } + }, []) + + return ( + + {/** A scroll view with buttons to select default and custom input schemes */} +
+ {targetAssembly ? ( + + ) : ( + <> + )} +
+
+ ) +} + +export default TransformAssemblyPanel diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurationSavedEvent.ts b/fission/src/ui/panels/configuring/assembly-config/ConfigurationSavedEvent.ts new file mode 100644 index 0000000000..9f240286f3 --- /dev/null +++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurationSavedEvent.ts @@ -0,0 +1,16 @@ +/** An event to save whatever configuration interface is open when it is closed */ +export class ConfigurationSavedEvent extends Event { + public constructor() { + super("ConfigurationSaved") + + window.dispatchEvent(this) + } + + public static Listen(func: (e: Event) => void) { + window.addEventListener("ConfigurationSaved", func) + } + + public static RemoveListener(func: (e: Event) => void) { + window.removeEventListener("ConfigurationSaved", func) + } +} diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurationType.ts b/fission/src/ui/panels/configuring/assembly-config/ConfigurationType.ts new file mode 100644 index 0000000000..927beac567 --- /dev/null +++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurationType.ts @@ -0,0 +1,15 @@ +export enum ConfigurationType { + ROBOT, + FIELD, + INPUTS, +} + +let selectedConfigurationType: ConfigurationType = ConfigurationType.ROBOT + +export function setSelectedConfigurationType(type: ConfigurationType) { + selectedConfigurationType = type +} + +export function getConfigurationType() { + return selectedConfigurationType +} diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx new file mode 100644 index 0000000000..5d82ebb031 --- /dev/null +++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx @@ -0,0 +1,380 @@ +import { MiraType } from "@/mirabuf/MirabufLoader" +import MirabufSceneObject, { setSpotlightAssembly } from "@/mirabuf/MirabufSceneObject" +import World from "@/systems/World" +import Label from "@/ui/components/Label" +import Panel, { PanelPropsImpl } from "@/ui/components/Panel" +import SelectMenu, { SelectMenuOption } from "@/ui/components/SelectMenu" +import { ToggleButton, ToggleButtonGroup } from "@/ui/components/ToggleButtonGroup" +import { useEffect, useMemo, useReducer, useState, MouseEvent } from "react" +import ConfigureScoringZonesInterface from "./interfaces/scoring/ConfigureScoringZonesInterface" +import ChangeInputsInterface from "./interfaces/inputs/ConfigureInputsInterface" +import InputSystem from "@/systems/input/InputSystem" +import SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain" +import { usePanelControlContext } from "@/ui/PanelContext" +import Button from "@/ui/components/Button" +import ConfigureSchemeInterface from "./interfaces/inputs/ConfigureSchemeInterface" +import { SynthesisIcons } from "@/ui/components/StyledComponents" +import ConfigureSubsystemsInterface from "./interfaces/ConfigureSubsystemsInterface" +import SequentialBehaviorsInterface from "./interfaces/SequentialBehaviorsInterface" +import ConfigureShotTrajectoryInterface from "./interfaces/ConfigureShotTrajectoryInterface" +import ConfigureGamepiecePickupInterface from "./interfaces/ConfigureGamepiecePickupInterface" +import { ConfigurationSavedEvent } from "./ConfigurationSavedEvent" +import { ConfigurationType, getConfigurationType, setSelectedConfigurationType } from "./ConfigurationType" +import TransformGizmoControl from "@/ui/components/TransformGizmoControl" +import { ConfigMode, popConfigurePanelSettings } from "./ConfigurePanelControls" +import BrainSelectionInterface from "./interfaces/BrainSelectionInterface" +import SimulationInterface from "./interfaces/SimulationInterface" + +/** Option for selecting a robot of field */ +class AssemblySelectionOption extends SelectMenuOption { + assemblyObject: MirabufSceneObject + + constructor(name: string, assemblyObject: MirabufSceneObject) { + super(assemblyObject.id.toString(), name) + this.assemblyObject = assemblyObject + } +} + +interface ConfigurationSelectionProps { + configurationType: ConfigurationType + onAssemblySelected: (assembly: MirabufSceneObject | undefined) => void + selectedAssembly?: MirabufSceneObject +} + +function makeSelectionOption(configurationType: ConfigurationType, assembly: MirabufSceneObject) { + return new AssemblySelectionOption( + `${configurationType == ConfigurationType.ROBOT ? `[${InputSystem.brainIndexSchemeMap.get((assembly.brain as SynthesisBrain).brainIndex)?.schemeName ?? "-"}]` : ""} ${assembly.assemblyName}`, + assembly + ) +} + +const AssemblySelection: React.FC = ({ + configurationType, + onAssemblySelected, + selectedAssembly, +}) => { + // Update is used when a robot or field is deleted to update the select menu + const [u, update] = useReducer(x => !x, false) + const { openPanel } = usePanelControlContext() + + const robots = useMemo(() => { + const assemblies = [...World.SceneRenderer.sceneObjects.values()].filter(x => { + if (x instanceof MirabufSceneObject) { + return x.miraType === MiraType.ROBOT + } + return false + }) as MirabufSceneObject[] + + return assemblies + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [u]) + + const fields = useMemo(() => { + const assemblies = [...World.SceneRenderer.sceneObjects.values()].filter(x => { + if (x instanceof MirabufSceneObject) { + return x.miraType === MiraType.FIELD + } + return false + }) as MirabufSceneObject[] + + return assemblies + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [u]) + + const options = useMemo(() => { + return (configurationType == ConfigurationType.ROBOT ? robots : fields).map(assembly => + makeSelectionOption(configurationType, assembly) + ) + }, [configurationType, fields, robots]) + + /** Robot or field select menu */ + return ( + { + onAssemblySelected((val as AssemblySelectionOption)?.assemblyObject) + }} + defaultHeaderText={`Select a ${configurationType == ConfigurationType.ROBOT ? "Robot" : "Field"}`} + onDelete={val => { + World.SceneRenderer.RemoveSceneObject((val as AssemblySelectionOption).assemblyObject.id) + // onAssemblySelected(undefined) + update() + }} + onAddClicked={() => { + openPanel("import-mirabuf") + }} + noOptionsText={`No ${configurationType == ConfigurationType.ROBOT ? "robots" : "fields"} spawned!`} + defaultSelectedOption={ + selectedAssembly ? makeSelectionOption(configurationType, selectedAssembly) : undefined + } + /> + ) +} + +class ConfigModeSelectionOption extends SelectMenuOption { + configMode: ConfigMode + + constructor(name: string, configMode: ConfigMode, tooltip?: string) { + super(name, name, tooltip) + this.configMode = configMode + } +} + +function getRobotModes(assembly: MirabufSceneObject): Map { + const modes = new Map([ + [ + ConfigMode.BRAIN, + new ConfigModeSelectionOption( + "Brain", + ConfigMode.BRAIN, + "Select and modify what is controlling of the robot." + ), + ], + [ConfigMode.MOVE, new ConfigModeSelectionOption("Move", ConfigMode.MOVE)], + [ConfigMode.INTAKE, new ConfigModeSelectionOption("Intake", ConfigMode.INTAKE)], + [ConfigMode.EJECTOR, new ConfigModeSelectionOption("Ejector", ConfigMode.EJECTOR)], + [ + ConfigMode.SUBSYSTEMS, + new ConfigModeSelectionOption( + "Configure Joints", + ConfigMode.SUBSYSTEMS, + "Set the velocities, torques, and accelerations of your robot's motors." + ), + ], + [ + ConfigMode.SEQUENTIAL, + new ConfigModeSelectionOption( + "Sequence Joints", + ConfigMode.SEQUENTIAL, + "Set which joints follow each other. For example, the second stage of an elevator could follow the first, moving in unison with it." + ), + ], + ]) + + switch (assembly.brain?.brainType) { + case "wpilib": + modes.set( + ConfigMode.SIM, + new ConfigModeSelectionOption( + "Simulation", + ConfigMode.SIM, + "Configure the WPILib simulation settings for this robot." + ) + ) + break + case "synthesis": + modes.set(ConfigMode.CONTROLS, new ConfigModeSelectionOption("Controls", ConfigMode.CONTROLS)) + break + default: + break + } + + return modes +} + +const fieldModes: Map = new Map([ + [ConfigMode.MOVE, new ConfigModeSelectionOption("Move", ConfigMode.MOVE)], + [ConfigMode.SCORING_ZONES, new ConfigModeSelectionOption("Scoring Zones", ConfigMode.SCORING_ZONES)], +]) + +interface ConfigModeSelectionProps { + configurationType: ConfigurationType + onModeSelected: (mode: ConfigMode | undefined) => void + selectedMode?: ConfigMode + assembly: MirabufSceneObject +} + +const ConfigModeSelection: React.FC = ({ + configurationType, + onModeSelected, + selectedMode, + assembly, +}) => { + // Not sure about leaving this outside of a hook + const robotModes = getRobotModes(assembly) + + return ( + { + onModeSelected((val as ConfigModeSelectionOption)?.configMode) + }} + defaultHeaderText="Select a Configuration Mode" + indentation={1} + defaultSelectedOption={ + selectedMode + ? configurationType == ConfigurationType.ROBOT + ? robotModes.get(selectedMode)! + : fieldModes.get(selectedMode)! + : undefined + } + /> + ) +} + +interface ConfigInterfaceProps { + configMode: ConfigMode + assembly: MirabufSceneObject + openPanel: (panelId: string) => void + closePanel: (panelId: string) => void +} + +/** The interface for the actual configuration */ +const ConfigInterface: React.FC = ({ configMode, assembly, openPanel, closePanel }) => { + switch (configMode) { + case ConfigMode.INTAKE: + return + case ConfigMode.EJECTOR: + return + case ConfigMode.SUBSYSTEMS: + return + case ConfigMode.CONTROLS: { + const brainIndex = (assembly.brain as SynthesisBrain).brainIndex + const scheme = InputSystem.brainIndexSchemeMap.get(brainIndex) + + return ( + <> + - ) - })} - - - ) : ( - <> - {zones?.length > 0 ? ( - - {zones.map((zonePrefs: ScoringZonePreferences, i: number) => ( - { - return zonePrefs - })()} - field={selectedField} - openPanel={openPanel} - save={() => saveZones(zones, selectedField)} - deleteZone={() => { - setZones(zones.filter((_, idx) => idx !== i)) - saveZones( - zones.filter((_, idx) => idx !== i), - selectedField - ) - }} - /> - ))} - - ) : ( - - )} - - ) -} - -const ButtonSecondary: React.FC = ({ value, onClick }) => { - return ( - - ) -} - -const ButtonIcon: React.FC = ({ value, onClick }) => { - return ( - - ) -} +import Panel, { PanelPropsImpl } from "@/ui/components/Panel" +import Button from "@/ui/components/Button" +import { Global_OpenPanel } from "@/ui/components/GlobalUIControls" +import { PAUSE_REF_ASSEMBLY_SPAWNING } from "@/systems/physics/PhysicsSystem" interface ItemCardProps { id: string name: string primaryButtonNode: ReactNode - secondaryButtonNode?: ReactNode primaryOnClick: () => void secondaryOnClick?: () => void } -const ItemCard: React.FC = ({ - id, - name, - primaryButtonNode, - secondaryButtonNode, - primaryOnClick, - secondaryOnClick, -}) => { +const ItemCard: React.FC = ({ id, name, primaryButtonNode, primaryOnClick, secondaryOnClick }) => { return ( = ({ alignItems={"center"} gap={"1rem"} > - {name.replace(/.mira$/, "")} + {name.replace(/.mira$/, "")} = ({ justifyContent={"center"} alignItems={"center"} > - - {secondaryButtonNode && secondaryOnClick && ( - - )} + + {secondaryOnClick && DeleteButton(secondaryOnClick)} ) @@ -122,7 +79,9 @@ export type MiraManifest = { } function GetCacheInfo(miraType: MiraType): MirabufCacheInfo[] { - return Object.values(MirabufCachingService.GetCacheMap(miraType)) + return Object.values( + canOPFS ? MirabufCachingService.GetCacheMap(miraType) : miraType == MiraType.ROBOT ? backUpRobots : backUpFields + ) } function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle?: ProgressHandle) { @@ -130,6 +89,7 @@ function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle? progressHandle = new ProgressHandle(info.name ?? info.cacheKey) } + World.PhysicsSystem.HoldPause(PAUSE_REF_ASSEMBLY_SPAWNING) MirabufCachingService.Get(info.id, type) .then(assembly => { if (assembly) { @@ -137,6 +97,8 @@ function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle? if (x) { World.SceneRenderer.RegisterSceneObject(x) progressHandle.Done() + + Global_OpenPanel?.("initial-config") } else { progressHandle.Fail() } @@ -149,11 +111,15 @@ function SpawnCachedMira(info: MirabufCacheInfo, type: MiraType, progressHandle? } }) .catch(() => progressHandle.Fail()) + .finally(() => { + setTimeout(() => World.PhysicsSystem.ReleasePause(PAUSE_REF_ASSEMBLY_SPAWNING), 500) + }) } const ImportMirabufPanel: React.FC = ({ panelId }) => { const { showTooltip } = useTooltipControlContext() - const { closePanel, openPanel } = usePanelControlContext() + const { closePanel } = usePanelControlContext() + const { openModal } = useModalControlContext() const [cachedRobots, setCachedRobots] = useState(GetCacheInfo(MiraType.ROBOT)) const [cachedFields, setCachedFields] = useState(GetCacheInfo(MiraType.FIELD)) @@ -190,6 +156,11 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { } }, []) + useEffect(() => { + closePanel("configure") + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + // Get Default Mirabuf Data, Load into manifest. useEffect(() => { // To remove the prettier warning @@ -237,10 +208,8 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { ]) closePanel(panelId) - - if (type == MiraType.ROBOT) openPanel("choose-scheme") }, - [showTooltip, closePanel, panelId, openPanel] + [showTooltip, closePanel, panelId] ) // Cache a selected remote mirabuf assembly, load from cache. @@ -260,10 +229,8 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { .catch(() => status.Fail()) closePanel(panelId) - - if (type == MiraType.ROBOT) openPanel("choose-scheme") }, - [closePanel, panelId, openPanel] + [closePanel, panelId] ) const selectAPS = useCallback( @@ -282,57 +249,57 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { .catch(() => status.Fail()) closePanel(panelId) - - if (type == MiraType.ROBOT) openPanel("choose-scheme") }, - [closePanel, panelId, openPanel] + [closePanel, panelId] ) // Generate Item cards for cached robots. const cachedRobotElements = useMemo( () => - cachedRobots.map(info => - ItemCard({ - name: info.name || info.cacheKey || "Unnamed Robot", - id: info.id, - primaryButtonNode: AddIcon, - primaryOnClick: () => { - console.log(`Selecting cached robot: ${info.cacheKey}`) - selectCache(info, MiraType.ROBOT) - }, - secondaryButtonNode: DeleteIcon, - secondaryOnClick: () => { - console.log(`Deleting cache of: ${info.cacheKey}`) - MirabufCachingService.Remove(info.cacheKey, info.id, MiraType.ROBOT) - - setCachedRobots(GetCacheInfo(MiraType.ROBOT)) - }, - }) - ), + cachedRobots + .sort((a, b) => a.name?.localeCompare(b.name ?? "") ?? -1) + .map(info => + ItemCard({ + name: info.name || info.cacheKey || "Unnamed Robot", + id: info.id, + primaryButtonNode: SynthesisIcons.AddLarge, + primaryOnClick: () => { + console.log(`Selecting cached robot: ${info.cacheKey}`) + selectCache(info, MiraType.ROBOT) + }, + secondaryOnClick: () => { + console.log(`Deleting cache of: ${info.cacheKey}`) + MirabufCachingService.Remove(info.cacheKey, info.id, MiraType.ROBOT) + + setCachedRobots(GetCacheInfo(MiraType.ROBOT)) + }, + }) + ), [cachedRobots, selectCache, setCachedRobots] ) // Generate Item cards for cached fields. const cachedFieldElements = useMemo( () => - cachedFields.map(info => - ItemCard({ - name: info.name || info.cacheKey || "Unnamed Field", - id: info.id, - primaryButtonNode: AddIcon, - primaryOnClick: () => { - console.log(`Selecting cached field: ${info.cacheKey}`) - selectCache(info, MiraType.FIELD) - }, - secondaryButtonNode: DeleteIcon, - secondaryOnClick: () => { - console.log(`Deleting cache of: ${info.cacheKey}`) - MirabufCachingService.Remove(info.cacheKey, info.id, MiraType.FIELD) - - setCachedFields(GetCacheInfo(MiraType.FIELD)) - }, - }) - ), + cachedFields + .sort((a, b) => a.name?.localeCompare(b.name ?? "") ?? -1) + .map(info => + ItemCard({ + name: info.name || info.cacheKey || "Unnamed Field", + id: info.id, + primaryButtonNode: SynthesisIcons.AddLarge, + primaryOnClick: () => { + console.log(`Selecting cached field: ${info.cacheKey}`) + selectCache(info, MiraType.FIELD) + }, + secondaryOnClick: () => { + console.log(`Deleting cache of: ${info.cacheKey}`) + MirabufCachingService.Remove(info.cacheKey, info.id, MiraType.FIELD) + + setCachedFields(GetCacheInfo(MiraType.FIELD)) + }, + }) + ), [cachedFields, selectCache, setCachedFields] ) @@ -341,17 +308,19 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { const remoteRobots = manifest?.robots.filter( path => !cachedRobots.some(info => info.cacheKey.includes(path.src)) ) - return remoteRobots?.map(path => - ItemCard({ - name: path.displayName, - id: path.src, - primaryButtonNode: DownloadIcon, - primaryOnClick: () => { - console.log(`Selecting remote: ${path}`) - selectRemote(path, MiraType.ROBOT) - }, - }) - ) + return remoteRobots + ?.sort((a, b) => a.displayName.localeCompare(b.displayName)) + .map(path => + ItemCard({ + name: path.displayName, + id: path.src, + primaryButtonNode: SynthesisIcons.DownloadLarge, + primaryOnClick: () => { + console.log(`Selecting remote: ${path}`) + selectRemote(path, MiraType.ROBOT) + }, + }) + ) }, [manifest?.robots, cachedRobots, selectRemote]) // Generate Item cards for remote fields. @@ -359,40 +328,44 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { const remoteFields = manifest?.fields.filter( path => !cachedFields.some(info => info.cacheKey.includes(path.src)) ) - return remoteFields?.map(path => - ItemCard({ - name: path.displayName, - id: path.src, - primaryButtonNode: DownloadIcon, - primaryOnClick: () => { - console.log(`Selecting remote: ${path}`) - selectRemote(path, MiraType.FIELD) - }, - }) - ) + return remoteFields + ?.sort((a, b) => a.displayName.localeCompare(b.displayName)) + .map(path => + ItemCard({ + name: path.displayName, + id: path.src, + primaryButtonNode: SynthesisIcons.DownloadLarge, + primaryOnClick: () => { + console.log(`Selecting remote: ${path}`) + selectRemote(path, MiraType.FIELD) + }, + }) + ) }, [manifest?.fields, cachedFields, selectRemote]) // Generate Item cards for APS robots and fields. const hubElements = useMemo( () => - files?.map(file => - ItemCard({ - name: file.attributes.displayName!, - id: file.id, - primaryButtonNode: DownloadIcon, - primaryOnClick: () => { - console.debug(file.raw) - selectAPS(file, viewType) - }, - }) - ), + files + ?.sort((a, b) => a.attributes.displayName!.localeCompare(b.attributes.displayName!)) + .map(file => + ItemCard({ + name: `${file.attributes.displayName!.replace(".mira", "")}${file.attributes.versionNumber != undefined ? ` (v${file.attributes.versionNumber})` : ""}`, + id: file.id, + primaryButtonNode: SynthesisIcons.DownloadLarge, + primaryOnClick: () => { + console.debug(file.raw) + selectAPS(file, viewType) + }, + }) + ), [files, selectAPS, viewType] ) return ( = ({ panelId }) => { {viewType == MiraType.ROBOT ? ( <> - + {cachedRobotElements ? `${cachedRobotElements.length} Saved Robot${cachedRobotElements.length == 1 ? "" : "s"}` : "Loading Saved Robots"} - - + + {cachedRobotElements} ) : ( <> - + {cachedFieldElements ? `${cachedFieldElements.length} Saved Field${cachedFieldElements.length == 1 ? "" : "s"}` : "Loading Saved Fields"} - - + + {cachedFieldElements} )} @@ -440,40 +413,39 @@ const ImportMirabufPanel: React.FC = ({ panelId }) => { justifyContent={"center"} alignItems={"center"} > - + {hubElements ? `${hubElements.length} Remote Asset${hubElements.length == 1 ? "" : "s"}` : filesStatus.message} - - {hubElements && filesStatus.isDone ? ( - RequestMirabufFiles()} /> - ) : ( - <> - )} + + {hubElements && filesStatus.isDone ? RefreshButton(() => RequestMirabufFiles()) : <>} - + {hubElements} {viewType == MiraType.ROBOT ? ( <> - + {remoteRobotElements ? `${remoteRobotElements.length} Default Robot${remoteRobotElements.length == 1 ? "" : "s"}` : "Loading Default Robots"} - - + + {remoteRobotElements} ) : ( <> - + {remoteFieldElements ? `${remoteFieldElements.length} Default Field${remoteFieldElements.length == 1 ? "" : "s"}` : "Loading Default Fields"} - - + + {remoteFieldElements} )} + + + ) +} + +function FlowControls({ onCreateJunction }: FlowControlsProps) { + const { zoomIn, zoomOut, fitView } = useReactFlow() + + return ( + + + + + + + + + + + + + + + ) +} + +export default FlowControls diff --git a/fission/src/ui/panels/simulation/FlowInfo.tsx b/fission/src/ui/panels/simulation/FlowInfo.tsx new file mode 100644 index 0000000000..2b680abfd2 --- /dev/null +++ b/fission/src/ui/panels/simulation/FlowInfo.tsx @@ -0,0 +1,15 @@ +import { Panel as FlowPanel } from "@xyflow/react" +import { CustomTooltip, RefreshButton } from "@/ui/components/StyledComponents" + +function FlowInfo({ reset }: { reset: () => void }) { + return ( + + {CustomTooltip( + "Click and drag to make connection to your robot's IO. Use the controls in the bottom left to zoom in/out, fit to the nodes in the graph, and add junction nodes for an easier experience connecting many motors to many joints. Holding ALT while dropping an edge over nothing will break out the edge into it's separate components, if it has multiple. Click the reset button to the left to completely reset all the wiring nodes" + )} + {RefreshButton(reset)} + + ) +} + +export default FlowInfo diff --git a/fission/src/ui/panels/simulation/SimConfigShared.ts b/fission/src/ui/panels/simulation/SimConfigShared.ts new file mode 100644 index 0000000000..4358f68c21 --- /dev/null +++ b/fission/src/ui/panels/simulation/SimConfigShared.ts @@ -0,0 +1,835 @@ +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import Driver, { DriverType } from "@/systems/simulation/driver/Driver" +import { + deconstructNoraType, + hasNoraAverageFunc, + noraAverageFunc, + NoraType, + NoraTypes, +} from "@/systems/simulation/Nora" +import Stimulus, { StimulusType } from "@/systems/simulation/stimulus/Stimulus" +import { + getSimMap, + receiverTypeMap, + SimAccel, + SimCANEncoder, + SimCANMotor, + SimPWM, + SimType, + supplierTypeMap, +} from "@/systems/simulation/wpilib_brain/WPILibBrain" +import World from "@/systems/World" +import { Random } from "@/util/Random" +import { XYPosition } from "@xyflow/react" +import WiringNode from "./WiringNode" +import { SimFlow, SimReceiver, SimSupplier } from "@/systems/simulation/wpilib_brain/SimDataFlow" +import { SimulationLayer } from "@/systems/simulation/SimulationSystem" + +export const NORA_TYPES_COLORS: { [k in NoraTypes]: string } = { + [NoraTypes.Number]: "#5f60ff", + [NoraTypes.Number2]: "#2bc275", + [NoraTypes.Number3]: "#ffc21a", + [NoraTypes.Unknown]: "#bebebe", +} + +let id = 0 +export function genId(): number { + return ++id +} + +export function genRandomId(): string { + return Math.floor(Random() * Number.MAX_SAFE_INTEGER).toString() +} + +const savedToGenMap = new Map() +const genToSavedMap = new Map() +function makeMapping(savedId: string): string { + const genId = (++id).toString() + savedToGenMap.set(savedId, genId) + genToSavedMap.set(genId, savedId) + return genId +} +export function savedIdToGenId(savedId: string): string { + const genId = savedToGenMap.get(savedId) + if (genId != undefined) { + return genId + } else { + return makeMapping(savedId) + } +} +export function genIdToSavedId(genId: string): string | undefined { + return genToSavedMap.get(genId) +} + +export const handleInfoDisplayCompare: (a: HandleInfo, b: HandleInfo) => number = (a, b) => + a.displayName.localeCompare(b.displayName) + +export const NODE_ID_ROBOT_IO = "robot-io-node" +export const NODE_ID_SIM_OUT = "sim-output-node" +export const NODE_ID_SIM_IN = "sim-input-node" + +export type ConfigState = "wiring" | "simIO" | "robotIO" +export type OriginType = SimType | StimulusType | DriverType +export enum FuncType { + Junction = "junct", + Constructor = "construct", + Deconstructor = "deconstruct", +} + +export type HandleInfo = { + id: string + nodeId: string + noraType: NoraTypes + originType?: OriginType + originId: string + + displayName: string + enabled: boolean + + many: boolean + isSource: boolean +} + +export type FlowControlsProps = { + onCreateJunction?: () => void +} + +export function getDriverSignals(assembly: MirabufSceneObject): Driver[] { + const simLayer = World.SimulationSystem.GetSimulationLayer(assembly.mechanism) + return simLayer?.drivers ?? [] +} + +export function getStimulusSignals(assembly: MirabufSceneObject): Stimulus[] { + const simLayer = World.SimulationSystem.GetSimulationLayer(assembly.mechanism) + return simLayer?.stimuli ?? [] +} + +export function getCANMotors(): [string, Map][] { + const cans = getSimMap()?.get(SimType.CANMotor) ?? new Map>() + return [...cans.entries()].filter(([_, data]) => data.get("][] { + return [...(getSimMap()?.get(SimType.CANEncoder)?.entries() ?? [])] +} + +export function getPWMDevices(): [string, Map][] { + const pwms = getSimMap()?.get(SimType.PWM) + if (pwms) { + return [...pwms.entries()].filter(([_, data]) => data.get("][] { + return [...(getSimMap()?.get(SimType.Accel)?.entries() ?? [])] +} + +export function getDIODevices(): [string, Map][] { + return [...(getSimMap()?.get(SimType.DIO)?.entries() ?? [])] +} + +function displayNameCAN(id: string) { + const a = id.indexOf("[") + const b = id.indexOf("]") + if (a === -1 || b === -1 || b - a < 2) return id + return `CAN [${id.substring(a + 1, b)}]` +} + +function displayNamePWM(id: string) { + return `PWM [${id}]` +} + +function displayNameAccel(id: string) { + if (id.startsWith("BuiltIn")) { + return "Accel [Built In]" + } else { + const a = id.indexOf("[") + const b = id.indexOf("]") + if (a === -1 || b === -1 || b - a < 2) return id + return `Accel [${id.substring(0, a)} - ${id.substring(a + 1, b)}]` + } +} + +// TODO +// function displayNameDI(id: string) { +// return `DI [${id}]` +// } + +// function displayNameDO(id: string) { +// return `DO [${id}]` +// } + +export type NodeInfo = { + id: string + type: string + funcType?: FuncType + position: XYPosition + tooltip?: string + sources: HandleId_Alias[] + targets: HandleId_Alias[] +} + +type NodeId_Alias = string +type HandleId_Alias = string +type EdgeId_Alias = string +type Edge_Alias = { sourceId: HandleId_Alias; targetId: HandleId_Alias } + +export type SimConfigData = { + handles: { [k: HandleId_Alias]: HandleInfo } + edges: { [k: EdgeId_Alias]: Edge_Alias } + adjacency: { [k: HandleId_Alias]: { [j: EdgeId_Alias]: boolean } } + nodes: { [k: NodeId_Alias]: NodeInfo } +} + +export class SimConfig { + private constructor() {} + + public static Default(assembly: MirabufSceneObject) { + const config: SimConfigData = { + handles: {}, + edges: {}, + adjacency: {}, + nodes: {}, + } + + SimConfig.AddRobotIONode(config) + const simInNode: NodeInfo = { + id: NODE_ID_SIM_IN, + type: WiringNode.name, + position: { x: 800, y: 0 }, + tooltip: + "These handles represent the input of the simulation. These are drivers for wheels, hinges, and sliders. Use the edit button to hide/reveal handles.", + sources: [], + targets: [], + } + const simOutNode: NodeInfo = { + id: NODE_ID_SIM_OUT, + type: WiringNode.name, + position: { x: -800, y: 0 }, + tooltip: + "These handles represent the output of the simulation. These are stimuli for wheels, hinges, and sliders that represent encoder positions and speeds. Use the edit button to hide/reveal handles.", + sources: [], + targets: [], + } + config.nodes[NODE_ID_SIM_IN] = simInNode + config.nodes[NODE_ID_SIM_OUT] = simOutNode + getDriverSignals(assembly).forEach(x => { + if (x.info?.GUID) { + const handle: HandleInfo = { + id: "", + nodeId: NODE_ID_SIM_IN, + noraType: x.getReceiverType(), + originType: x.id.type, + originId: x.idStr, + + displayName: x.DisplayName(), + enabled: true, + + many: hasNoraAverageFunc(x.getReceiverType()), + isSource: false, + } + this.AddHandle(config, handle) + simInNode.targets.push(handle.id) + } + }) + getStimulusSignals(assembly).forEach(x => { + if (x.info?.GUID) { + const handle: HandleInfo = { + id: "", + nodeId: NODE_ID_SIM_OUT, + noraType: x.getSupplierType(), + originType: x.id.type, + originId: x.idStr, + + displayName: x.DisplayName(), + enabled: true, + + many: hasNoraAverageFunc(x.getSupplierType()), + isSource: true, + } + this.AddHandle(config, handle) + simOutNode.sources.push(handle.id) + } else { + console.debug("Skipping stimulus", x) + } + }) + + return config + } + + public static RefreshRobotIO(config: SimConfigData) { + SimConfig.AddRobotIONode(config) + // TODO: Try to restore connections that remain valid after refresh + } + + private static AddRobotIONode(config: SimConfigData) { + if (config.nodes[NODE_ID_ROBOT_IO] != undefined) { + SimConfig.RemoveNode(config, NODE_ID_ROBOT_IO) + } + + const robotIONode: NodeInfo = { + id: NODE_ID_ROBOT_IO, + type: WiringNode.name, + position: { x: 0, y: 0 }, + tooltip: + "These handles represent the different devices we've discovered from your connected robot code. The left handles represent input devices such as sensors. The right handles represent output devices such as motor controllers. Use the edit button to hide/reveal handles.", + sources: [], + targets: [], + } + config.nodes[NODE_ID_ROBOT_IO] = robotIONode + getCANMotors().forEach(([id, _]) => { + const handle: HandleInfo = { + id: "", + nodeId: NODE_ID_ROBOT_IO, + noraType: supplierTypeMap[SimType.CANMotor]!, + originType: SimType.CANMotor, + originId: id, + + displayName: displayNameCAN(id), + enabled: true, + + many: true, + isSource: true, + } + this.AddHandle(config, handle) + robotIONode.sources.push(handle.id) + }) + getCANEncoder().forEach(([id, _]) => { + const handle: HandleInfo = { + id: "", + nodeId: NODE_ID_ROBOT_IO, + noraType: receiverTypeMap[SimType.CANEncoder]!, + originType: SimType.CANEncoder, + originId: id, + + displayName: displayNameCAN(id), + enabled: true, + + many: hasNoraAverageFunc(receiverTypeMap[SimType.CANEncoder]!), + isSource: false, + } + this.AddHandle(config, handle) + robotIONode.targets.push(handle.id) + }) + getPWMDevices().forEach(([id, _]) => { + const handle: HandleInfo = { + id: "", + nodeId: NODE_ID_ROBOT_IO, + noraType: supplierTypeMap[SimType.PWM]!, + originType: SimType.PWM, + originId: id, + + displayName: displayNamePWM(id), + enabled: true, + + many: true, + isSource: true, + } + this.AddHandle(config, handle) + robotIONode.sources.push(handle.id) + }) + getAccelDevices().forEach(([id, data]) => { + const handle: HandleInfo = { + id: "", + nodeId: NODE_ID_ROBOT_IO, + noraType: receiverTypeMap[SimType.Accel]!, + originType: SimType.Accel, + originId: id, + + displayName: displayNameAccel(id), + enabled: data.get(" { + // const handleIn: HandleInfo = { + // id: "", + // nodeId: NODE_ID_ROBOT_IO, + // noraType: receiverTypeMap[SimType.DIO]!, + // originType: SimType.DIO, + // originId: id, + + // displayName: displayNameDI(id), + // enabled: data.get(" this.DeleteEdge(config, x)) + delete config.adjacency[id] + delete config.handles[id] + return true + } + + public static RemoveNode(config: SimConfigData, id: NodeId_Alias): boolean { + if (config.nodes[id] == undefined) return false + ;[...Object.values(config.handles)].filter(x => x.nodeId == id).forEach(x => this.RemoveHandle(config, x.id)) + delete config.nodes[id] + return true + } + + public static AddJunctionNode(config: SimConfigData): NodeId_Alias { + let nodeId = "" + do { + nodeId = genRandomId() + } while (config.handles[nodeId] != undefined) + const node: NodeInfo = { + id: nodeId, + type: WiringNode.name, + position: { x: 300 + Random() * 100, y: -100 + Random() * 50 }, + funcType: FuncType.Junction, + targets: [], + sources: [], + } + config.nodes[nodeId] = node + + const targetHandle: HandleInfo = { + id: "", + nodeId: nodeId, + noraType: NoraTypes.Number, + originType: SimType.SimDevice, + originId: nodeId, + + displayName: "In", + enabled: true, + + many: true, + isSource: false, + } + SimConfig.AddHandle(config, targetHandle) + node.targets.push(targetHandle.id) + + const sourceHandle: HandleInfo = { + id: "", + nodeId: nodeId, + noraType: NoraTypes.Number, + originType: SimType.SimDevice, + originId: nodeId, + + displayName: "Out", + enabled: true, + + many: true, + isSource: true, + } + SimConfig.AddHandle(config, sourceHandle) + node.sources.push(sourceHandle.id) + return nodeId + } + + public static AddDeconstructorNode( + config: SimConfigData, + targetNoraType: NoraTypes, + positionHint?: XYPosition + ): HandleId_Alias | undefined { + const types = deconstructNoraType(targetNoraType) + if (types == undefined || types.length == 0) return undefined + + let nodeId = "" + do { + nodeId = genRandomId() + } while (config.handles[nodeId] != undefined) + const node: NodeInfo = { + id: nodeId, + type: WiringNode.name, + position: positionHint ?? { x: 0, y: 0 }, + funcType: FuncType.Deconstructor, + targets: [], + sources: [], + } + config.nodes[nodeId] = node + + const targetHandle: HandleInfo = { + id: "", + nodeId: nodeId, + noraType: targetNoraType, + originType: SimType.SimDevice, + originId: `target_${nodeId}`, + + displayName: "In", + enabled: true, + + many: hasNoraAverageFunc(targetNoraType), + isSource: false, + } + SimConfig.AddHandle(config, targetHandle) + node.targets.push(targetHandle.id) + + types.forEach((sourceType, i) => { + const sourceHandle: HandleInfo = { + id: "", + nodeId: nodeId, + noraType: sourceType, + originType: SimType.SimDevice, + originId: `source_${i}_${nodeId}`, + + displayName: `Out ${i + 1}`, + enabled: true, + + many: true, + isSource: true, + } + SimConfig.AddHandle(config, sourceHandle) + node.sources.push(sourceHandle.id) + }) + + return targetHandle.id + } + + public static AddConstructorNode( + config: SimConfigData, + sourceNoraType: NoraTypes, + positionHint?: XYPosition + ): HandleId_Alias | undefined { + const types = deconstructNoraType(sourceNoraType) + if (types == undefined || types.length == 0) return undefined + + let nodeId = "" + do { + nodeId = genRandomId() + } while (config.handles[nodeId] != undefined) + const node: NodeInfo = { + id: nodeId, + type: WiringNode.name, + position: positionHint ?? { x: 0, y: 0 }, + funcType: FuncType.Constructor, + targets: [], + sources: [], + } + config.nodes[nodeId] = node + + const sourceHandle: HandleInfo = { + id: "", + nodeId: nodeId, + noraType: sourceNoraType, + originType: SimType.SimDevice, + originId: `source_${nodeId}`, + + displayName: "Out", + enabled: true, + + many: true, + isSource: true, + } + SimConfig.AddHandle(config, sourceHandle) + node.sources.push(sourceHandle.id) + + types.forEach((targetType, i) => { + const targetHandle: HandleInfo = { + id: "", + nodeId: nodeId, + noraType: targetType, + originType: SimType.SimDevice, + originId: `target_${i}_${nodeId}`, + + displayName: `In ${i + 1}`, + enabled: true, + + many: hasNoraAverageFunc(targetType), + isSource: false, + } + SimConfig.AddHandle(config, targetHandle) + node.targets.push(targetHandle.id) + }) + + return sourceHandle.id + } + + public static GetEdge( + config: SimConfigData, + sourceId: HandleId_Alias, + targetId: HandleId_Alias + ): EdgeId_Alias | undefined { + const targetEdges = config.adjacency[targetId]! + return [...Object.keys(config.adjacency[sourceId]!)].filter(x => targetEdges[x] != undefined)[0] + } + + public static ValidateConnection( + config: SimConfigData, + sourceId: HandleId_Alias, + targetId: HandleId_Alias + ): boolean { + const sourceInfo = config.handles[sourceId] + const targetInfo = config.handles[targetId] + if (sourceInfo == undefined || targetInfo == undefined) return false + + if (!targetInfo.many && Object.entries(config.adjacency[targetId])!.length >= 1) return false + + return sourceInfo.noraType == targetInfo.noraType + } + + public static MakeConnection(config: SimConfigData, sourceId: HandleId_Alias, targetId: HandleId_Alias): boolean { + if (!SimConfig.ValidateConnection(config, sourceId, targetId)) { + console.error("Failed to make edge") + return false + } + + if (SimConfig.GetEdge(config, sourceId, targetId) != undefined) { + console.error("Connection already exists") + return false + } + + let edgeId = genRandomId() + while (config.edges[edgeId] != undefined) { + edgeId = genRandomId() + } + config.edges[edgeId] = { sourceId: sourceId, targetId: targetId } + config.adjacency[sourceId][edgeId] = true + config.adjacency[targetId][edgeId] = true + return true + } + + public static DeleteConnection(config: SimConfigData, sourceId: HandleId_Alias, targetId: HandleId_Alias): boolean { + const edgeId = SimConfig.GetEdge(config, sourceId, targetId) + if (edgeId == undefined) return false + delete config.adjacency[sourceId][edgeId] + delete config.adjacency[targetId][edgeId] + delete config.edges[edgeId] + return true + } + + public static DeleteEdge(config: SimConfigData, edgeId: EdgeId_Alias): boolean { + const edge = config.edges[edgeId] + if (edge == undefined) return false + delete config.adjacency[edge.sourceId][edgeId] + delete config.adjacency[edge.targetId][edgeId] + delete config.edges[edgeId] + return true + } + + public static Compile(config: SimConfigData, assembly: MirabufSceneObject): SimFlow[] | undefined { + const simLayer = World.SimulationSystem.GetSimulationLayer(assembly.mechanism) + if (!simLayer) { + console.error("No sim layer found") + return undefined + } + try { + const flows: SimFlow[] = [] + Object.entries(config.handles).forEach(([id, info]) => { + if ( + !info.isSource && + (info.nodeId == NODE_ID_ROBOT_IO || info.nodeId == NODE_ID_SIM_IN) && + (Object.entries(config.adjacency[info.id])?.length ?? 0) > 0 + ) { + const flow = SimConfig.CompileTargetHandle(config, simLayer, id, new Set()) + if (flow) flows.push(flow) + else throw new Error("Failed to compile flows") + } + }) + return flows + } catch (error) { + console.error("Error thrown during compilation", error) + return undefined + } + } + + public static CompileTargetHandle( + config: SimConfigData, + simLayer: SimulationLayer, + targetHandleId: HandleId_Alias, + encountered: Set + ): SimFlow | undefined { + const edges = Object.keys(config.adjacency[targetHandleId]) + if (!edges || edges.length < 1) { + console.warn("No edges found for target handle") + return undefined + } + + // Generate receiver + const targetHandle = config.handles[targetHandleId] + if (!targetHandle) return undefined + + const targetNoraType = targetHandle.noraType + let receiver: SimReceiver | undefined = undefined + if (targetHandle.nodeId == NODE_ID_ROBOT_IO) { + switch (targetHandle.originType) { + case SimType.CANEncoder: { + receiver = SimCANEncoder.GenReceiver(targetHandle.originId) + break + } + case SimType.Accel: { + receiver = SimAccel.GenReceiver(targetHandle.originId) + break + } + } + } else if (targetHandle.nodeId == NODE_ID_SIM_IN) { + receiver = simLayer.GetDriver(targetHandle.originId) + } else { + receiver = { + getReceiverType: () => targetHandle.noraType, + setReceiverValue: _ => { + console.debug("If you're seeing this, that means bad") + }, + } + } + if (!receiver) return undefined + + if (!hasNoraAverageFunc(targetNoraType) && edges.length > 1) return + + const suppliers: SimSupplier[] = [] + edges.forEach(edgeId => { + const edge = config.edges[edgeId] + if (!edge) return + const sourceHandle = config.handles[edge.sourceId] + if (!sourceHandle || sourceHandle.noraType != targetNoraType) return + if (encountered.has(sourceHandle.id)) return + encountered.add(sourceHandle.id) + switch (sourceHandle.nodeId) { + case NODE_ID_ROBOT_IO: { + // Get supplier from robot output + switch (sourceHandle.originType) { + case SimType.CANMotor: { + suppliers.push(SimCANMotor.GenSupplier(sourceHandle.originId)) + break + } + case SimType.PWM: { + suppliers.push(SimPWM.GenSupplier(sourceHandle.originId)) + break + } + } + break + } + case NODE_ID_SIM_OUT: { + // Get supplier from simulation output + const stim: SimSupplier | undefined = simLayer.GetStimuli(sourceHandle.originId) + if (stim) suppliers.push(stim) + break + } + default: { + // Figure out function type + const node = config.nodes[sourceHandle.nodeId] + if (!node?.funcType) break + const index = node.sources.indexOf(sourceHandle.id) + if (index == -1) break + const funcSuppliers = SimConfig.CompileFunctionNode(config, simLayer, node, encountered) + if (!funcSuppliers || funcSuppliers?.length != node.sources.length) break + suppliers.push(funcSuppliers[index]) + } + } + encountered.delete(sourceHandle.id) + }) + + if (suppliers.length == 0) return undefined + + if (suppliers.length == 1) { + return { + supplier: suppliers[0], + receiver: receiver, + } + } else { + const func = noraAverageFunc(targetNoraType) + if (!func) return undefined + return { + supplier: { + getSupplierType: () => targetNoraType, + getSupplierValue: func, + }, + receiver: receiver, + } + } + } + + public static CompileFunctionNode( + config: SimConfigData, + simLayer: SimulationLayer, + node: NodeInfo, + encountered: Set + ): SimSupplier[] | undefined { + switch (node.funcType) { + case FuncType.Constructor: { + if (node.sources.length != 1 || node.targets.length < 1) { + return undefined + } + const outputType = config.handles[node.sources[0]].noraType + const inputs = node.targets.map(x => { + const flow = SimConfig.CompileTargetHandle(config, simLayer, x, encountered) + if (!flow) { + console.error(`Failed to compile flow. TargetHandleId: ${x}`) + throw new Error("Failed to compile SimConfig") + } + return flow.supplier + }) + return [ + { + getSupplierType: () => outputType, + getSupplierValue: () => inputs.map(x => x.getSupplierValue()), + }, + ] + } + case FuncType.Deconstructor: { + if (node.sources.length < 1 || node.targets.length != 1) { + return undefined + } + const inputType = config.handles[node.targets[0]].noraType + const input = SimConfig.CompileTargetHandle(config, simLayer, node.targets[0], encountered) + if (!input) { + console.error(`Failed to compile flow. TargetHandleId: ${node.targets[0]}`) + throw new Error("Failed to compile SimConfig") + } + const deconstructedType = deconstructNoraType(inputType) + if (!deconstructedType) return undefined + const suppliers: SimSupplier[] = [] + for (let i = 0; i < deconstructedType.length; ++i) { + suppliers.push({ + getSupplierType: () => deconstructedType[i], + getSupplierValue: () => (input.supplier.getSupplierValue() as NoraType[])[i], + }) + } + return suppliers + } + case FuncType.Junction: { + if (node.sources.length != 1 || node.targets.length != 1) { + return undefined + } + const input = SimConfig.CompileTargetHandle(config, simLayer, node.targets[0], encountered) + if (!input) { + console.error(`Failed to compile flow. TargetHandleId: ${node.targets[0]}`) + throw new Error("Failed to compile SimConfig") + } + return [input.supplier] + break + } + } + return undefined + } +} diff --git a/fission/src/ui/panels/simulation/TextUpdaterNode.tsx b/fission/src/ui/panels/simulation/TextUpdaterNode.tsx new file mode 100644 index 0000000000..da8ad59ddc --- /dev/null +++ b/fission/src/ui/panels/simulation/TextUpdaterNode.tsx @@ -0,0 +1,30 @@ +import { useCallback, ChangeEvent } from "react" +import { Handle, NodeProps, Position } from "@xyflow/react" + +const handleStyle = { left: 10 } + +function TextUpdaterNode({ data, isConnectable }: NodeProps) { + const onChange = useCallback((evt: ChangeEvent) => { + console.log(evt.target.value) + }, []) + + return ( +
+ +
+ + +
+ + +
+ ) +} + +export default TextUpdaterNode diff --git a/fission/src/ui/panels/simulation/WiringNode.tsx b/fission/src/ui/panels/simulation/WiringNode.tsx new file mode 100644 index 0000000000..2718a0756b --- /dev/null +++ b/fission/src/ui/panels/simulation/WiringNode.tsx @@ -0,0 +1,126 @@ +import { Connection, Edge, Handle, NodeProps, Position } from "@xyflow/react" +import { handleInfoDisplayCompare, SimConfig, SimConfigData, HandleInfo, NORA_TYPES_COLORS } from "./SimConfigShared" +import { useCallback, useMemo } from "react" +import { CustomTooltip, DeleteButton, EditButton, RefreshButton } from "@/ui/components/StyledComponents" + +function WiringNode({ data, isConnectable }: NodeProps) { + const robotInput = data["input"] as HandleInfo[] | undefined + const robotOutput = data["output"] as HandleInfo[] | undefined + const onEdit = data["onEdit"] as (() => void) | undefined + const onRefresh = data["onRefresh"] as (() => void) | undefined + const onDelete = data["onDelete"] as (() => void) | undefined + const simConfig = data["simConfig"] as SimConfigData + const title = data["title"] as string + const tooltip = data["tooltip"] as string | undefined + + const validateConnection = useCallback( + (edge: Edge | Connection) => { + return SimConfig.ValidateConnection(simConfig, edge.sourceHandle!, edge.targetHandle!) + }, + [simConfig] + ) + + const inputHandles = useMemo( + () => + robotInput ? ( +
+ {robotInput.sort(handleInfoDisplayCompare).map((x, i) => { + return ( +
+
{x.displayName}
+ +
+ ) + })} +
+ ) : ( + <> + ), + [isConnectable, robotInput] + ) + + const outputHandles = useMemo( + () => + robotOutput ? ( +
+ {robotOutput.sort(handleInfoDisplayCompare).map((x, i) => { + return ( +
+
{x.displayName}
+ +
+ ) + })} +
+ ) : ( + <> + ), + [isConnectable, robotOutput, validateConnection] + ) + + return ( +
+
+ {tooltip ? CustomTooltip(tooltip) : <>} + {title} +
+
+ {inputHandles} + {outputHandles} +
+ {onEdit || onDelete ? ( +
+ {onEdit ? EditButton(onEdit) : <>} + {onRefresh ? RefreshButton(onRefresh) : <>} + {onDelete ? DeleteButton(onDelete) : <>} +
+ ) : ( + <> + )} +
+ ) +} + +export default WiringNode diff --git a/fission/src/ui/panels/simulation/WiringPanel.tsx b/fission/src/ui/panels/simulation/WiringPanel.tsx new file mode 100644 index 0000000000..37fa01fc6b --- /dev/null +++ b/fission/src/ui/panels/simulation/WiringPanel.tsx @@ -0,0 +1,488 @@ +import "@xyflow/react/dist/style.css" +import Panel, { PanelPropsImpl } from "@/components/Panel" +import { SectionDivider, SectionLabel, SynthesisIcons } from "@/ui/components/StyledComponents" +import React, { ComponentType, useCallback, useEffect, useMemo, useReducer, useState } from "react" +import { + ReactFlow, + Node as FlowNode, + Edge as FlowEdge, + useNodesState, + useEdgesState, + NodeProps, + Connection, + FinalConnectionState, + useReactFlow, + ReactFlowProvider, +} from "@xyflow/react" +import { + ConfigState, + HandleInfo, + handleInfoDisplayCompare, + NODE_ID_ROBOT_IO, + NODE_ID_SIM_IN, + NODE_ID_SIM_OUT, + SimConfig, +} from "./SimConfigShared" +import Label, { LabelSize } from "@/ui/components/Label" +import ScrollView from "@/ui/components/ScrollView" +import Checkbox from "@/ui/components/Checkbox" +import MirabufSceneObject from "@/mirabuf/MirabufSceneObject" +import World from "@/systems/World" +import Button from "@/ui/components/Button" +import { usePanelControlContext } from "@/ui/PanelContext" +import { Global_AddToast } from "@/ui/components/GlobalUIControls" +import { SimConfigData } from "./SimConfigShared" +import FlowControls from "./FlowControls" +import WiringNode from "./WiringNode" +import { SimType } from "@/systems/simulation/wpilib_brain/WPILibBrain" +import { isNoraDeconstructable } from "@/systems/simulation/Nora" +import InputSystem from "@/systems/input/InputSystem" +import FlowInfo from "./FlowInfo" + +type ConfigComponentProps = { + setConfigState: (state: ConfigState) => void + selectedAssembly: MirabufSceneObject + simConfig: SimConfigData + reset?: () => void +} + +type NodeType = ComponentType< + NodeProps & { + data: Record + type: string + } +> + +// This took way too long +const nodeTypes: Record = [WiringNode].reduce<{ [k: string]: NodeType }>((prev, next) => { + prev[next.name] = next + return prev +}, {}) + +function generateGraph( + simConfig: SimConfigData, + refreshGraph: () => void, + setConfigState: (state: ConfigState) => void +): [FlowNode[], FlowEdge[]] { + const nodes: Map = new Map() + const edges: FlowEdge[] = [] + + Object.entries(simConfig.nodes).forEach(([_k, v]) => { + let onEdit: (() => void) | undefined = undefined + let onRefresh: (() => void) | undefined = undefined + let onDelete: (() => void) | undefined = undefined + let title: string = "" + + switch (v.id) { + case NODE_ID_ROBOT_IO: + title = "Robot IO" + onEdit = () => setConfigState("robotIO") + onRefresh = () => { + SimConfig.RefreshRobotIO(simConfig) + refreshGraph() + } + break + case NODE_ID_SIM_IN: + title = "Simulation Input" + onEdit = () => setConfigState("simIO") + break + case NODE_ID_SIM_OUT: + title = "Simulation Output" + onEdit = () => setConfigState("simIO") + break + default: + onDelete = () => { + if (SimConfig.RemoveNode(simConfig, v.id)) refreshGraph() + } + break + } + + nodes.set(v.id, { + id: v.id, + type: v.type, + position: v.position, + data: { + title: title, + onEdit: onEdit, + onRefresh: onRefresh, + onDelete: onDelete, + simConfig: simConfig, + input: [], + output: [], + tooltip: v.tooltip, + }, + }) + }) + + Object.entries(simConfig.handles).forEach(([_k, v]) => { + if (!v.enabled) return + const node = nodes.get(v.nodeId) + if (!node) { + console.warn("Orphaned handle found") + return + } + const list = (v.isSource ? node.data.output : node.data.input) as unknown[] + list.push(v) + }) + + Object.entries(simConfig.edges).forEach(([k, v]) => { + const sourceHandle = simConfig.handles[v.sourceId] + const targetHandle = simConfig.handles[v.targetId] + + if (sourceHandle?.enabled && targetHandle?.enabled) { + edges.push({ + id: k, + source: sourceHandle.nodeId, + target: targetHandle.nodeId, + sourceHandle: sourceHandle.id, + targetHandle: targetHandle.id, + }) + } + }) + + return [[...nodes.values()], edges] +} + +function SimIOComponent({ setConfigState, simConfig }: ConfigComponentProps) { + const simOut: HandleInfo[] = [] + const simIn: HandleInfo[] = [] + Object.entries(simConfig.handles).forEach(([_k, v]) => { + if (v.nodeId == NODE_ID_SIM_OUT || v.nodeId == NODE_ID_SIM_IN) { + const list = v.isSource ? simOut : simIn + list.push(v) + } + }) + + return ( +
+ +
+
+ + + {simOut.sort(handleInfoDisplayCompare).map(handle => ( + { + handle.enabled = checked + }} + /> + ))} + +
+
+ + + {simIn.sort(handleInfoDisplayCompare).map(handle => ( + { + handle.enabled = checked + }} + /> + ))} + +
+
+
+ ) +} + +function RobotIOComponent({ setConfigState, simConfig }: ConfigComponentProps) { + const [canEncoders, canMotors, pwmDevices, accelerometers] = useMemo(() => { + const canEncoders: JSX.Element[] = [] + const canMotors: JSX.Element[] = [] + const pwmDevices: JSX.Element[] = [] + const accelerometers: JSX.Element[] = [] + + Object.entries(simConfig.handles).forEach(([_k, v]) => { + if (v.nodeId != NODE_ID_ROBOT_IO) return + + const checkbox = ( + { + v.enabled = checked + }} + /> + ) + + switch (v.originType) { + case SimType.CANMotor: + canMotors.push(checkbox) + break + case SimType.PWM: + pwmDevices.push(checkbox) + break + case SimType.CANEncoder: + canEncoders.push(checkbox) + break + case SimType.Accel: + accelerometers.push(checkbox) + break + } + }) + + return [canEncoders, canMotors, pwmDevices, accelerometers] + }, [simConfig]) + + return ( +
+ +
+
+ + + + CAN Encoders + + + {canEncoders} + + Accelerometers + + + {accelerometers} + +
+
+ + + + CAN Motors + + + {canMotors} + + PWM Devices + + + {pwmDevices} + +
+
+
+ ) +} + +function WiringComponent({ setConfigState, simConfig, reset }: ConfigComponentProps) { + const { screenToFlowPosition } = useReactFlow() + const [nodes, setNodes, onNodesChange] = useNodesState([] as FlowNode[]) + const [edges, setEdges, onEdgesChange] = useEdgesState([] as FlowEdge[]) + const [refreshHook, refreshGraph] = useReducer(x => !x, false) // Whenever I use reducers, it's always sketch. -Hunter + + // Essentially a callback, but it can use itself. + useEffect(() => { + const [nodes, edges] = generateGraph(simConfig, refreshGraph, setConfigState) + setNodes(nodes) + setEdges(edges) + }, [setConfigState, setEdges, setNodes, simConfig, refreshHook]) + + const onEdgeDoubleClick = useCallback( + (_: React.MouseEvent, edge: FlowEdge) => { + if (SimConfig.DeleteConnection(simConfig, edge.sourceHandle!, edge.targetHandle!)) { + refreshGraph() + } + }, + [simConfig] + ) + + const onNodeDragStop = useCallback( + (_event: React.MouseEvent, node: FlowNode, _nodes: FlowNode[]) => { + const nodeInfo = simConfig.nodes[node.id] + if (!nodeInfo) { + console.warn(`Unregistered Node detected: ${node.id}`) + return + } + nodeInfo.position = node.position + }, + [simConfig] + ) + + const onConnect = useCallback( + (connection: Connection) => { + const sourceId = connection.sourceHandle + const targetId = connection.targetHandle + if (SimConfig.MakeConnection(simConfig, sourceId!, targetId!)) { + refreshGraph() + } + }, + [simConfig] + ) + + const onConnectEnd = useCallback( + (event: MouseEvent | TouchEvent, state: FinalConnectionState) => { + if (state.isValid || state.fromHandle == null) return + + if (!(InputSystem.isKeyPressed("AltRight") || InputSystem.isKeyPressed("AltLeft"))) return + + const { clientX, clientY } = "changedTouches" in event ? event.changedTouches[0] : event + + const handleInfo = simConfig.handles[state.fromHandle.id!] + if (!handleInfo || !isNoraDeconstructable(handleInfo.noraType)) { + return + } + + const newHandleId = (handleInfo.isSource ? SimConfig.AddDeconstructorNode : SimConfig.AddConstructorNode)( + simConfig, + handleInfo.noraType, + screenToFlowPosition({ x: clientX, y: clientY }) + ) + if (!newHandleId) return + + if ( + handleInfo.isSource + ? SimConfig.MakeConnection(simConfig, handleInfo.id, newHandleId) + : SimConfig.MakeConnection(simConfig, newHandleId, handleInfo.id) + ) + refreshGraph() + }, + [screenToFlowPosition, simConfig] + ) + + const onCreateJunction = useCallback(() => { + SimConfig.AddJunctionNode(simConfig) + refreshGraph() + }, [refreshGraph, simConfig]) + + return ( + + {/* */} + + {})} /> + + ) +} + +function WiringPanel({ panelId }: PanelPropsImpl) { + const [configState, setConfigState] = useState("wiring") + const { closePanel } = usePanelControlContext() + const [simConfig, setSimConfig] = useState(undefined) + + const selectedAssembly = useMemo(() => { + const miraObjs = [...World.SceneRenderer.sceneObjects.entries()].filter(x => x[1] instanceof MirabufSceneObject) + if (miraObjs.length > 0) { + return miraObjs[0][1] as MirabufSceneObject + } else { + // TEMPORARY: Will be moved to config panel to ensure selected assembly + Global_AddToast?.("warning", "Missing Robot", "Must have at least one robot spawned for selection.") + closePanel(panelId) + } + }, [closePanel, panelId]) + + useEffect(() => { + if (!selectedAssembly) return + + const existingConfig = selectedAssembly.simConfigData + if (existingConfig) { + setSimConfig(JSON.parse(JSON.stringify(existingConfig))) // Create copy to not force a save + } else { + setSimConfig(SimConfig.Default(selectedAssembly)) + } + }, [selectedAssembly]) + + const save = useCallback(() => { + if (simConfig && selectedAssembly) { + const flows = SimConfig.Compile(simConfig, selectedAssembly) + if (!flows) { + console.error("Compilation Failed") + return + } + console.debug(`${flows.length} Flows Successfully Compiled!`) + + selectedAssembly.UpdateSimConfig(simConfig) + } + }, [selectedAssembly, simConfig]) + + const reset = useCallback(() => { + if (selectedAssembly) { + setSimConfig(SimConfig.Default(selectedAssembly)) + } + }, [selectedAssembly]) + + return ( + + {selectedAssembly && simConfig ? ( +
+ {configState === "wiring" ? ( + + + + ) : ( + <> + )} + {configState === "robotIO" ? ( + + ) : ( + <> + )} + {configState === "simIO" ? ( + + ) : ( + <> + )} +
+ ) : ( + <>ERRR + )} +
+ ) +} + +export default WiringPanel diff --git a/fission/src/util/Units.ts b/fission/src/util/Units.ts new file mode 100644 index 0000000000..425ac58f73 --- /dev/null +++ b/fission/src/util/Units.ts @@ -0,0 +1,73 @@ +import { getFontSize } from "./Utility" + +export class DOMUnitExpression { + public exprA: DOMUnitExpression | DOMUnit + public exprB?: DOMUnitExpression | DOMUnit + public op?: (a: number, b: number) => number + + private constructor( + exprA: DOMUnitExpression | DOMUnit, + exprB?: DOMUnitExpression | DOMUnit, + op?: (a: number, b: number) => number + ) { + this.exprA = exprA + this.exprB = exprB + this.op = op + } + + public static fromUnit(value: number, type?: DOMUnitTypes): DOMUnitExpression { + return new DOMUnitExpression(new DOMUnit(value, type ?? "px")) + } + + public evaluate(element: Element): number { + if (this.op && this.exprB) { + return this.op(this.exprA.evaluate(element), this.exprB.evaluate(element)) + } else { + return this.exprA.evaluate(element) + } + } + + public add(b: DOMUnit | DOMUnitExpression): DOMUnitExpression { + return new DOMUnitExpression(this, b, (x, y) => x + y) + } + + public sub(b: DOMUnit | DOMUnitExpression): DOMUnitExpression { + return new DOMUnitExpression(this, b, (x, y) => x - y) + } + + public mul(b: DOMUnit | DOMUnitExpression): DOMUnitExpression { + return new DOMUnitExpression(this, b, (x, y) => x * y) + } + + public div(b: DOMUnit | DOMUnitExpression): DOMUnitExpression { + return new DOMUnitExpression(this, b, (x, y) => x / y) + } +} + +type DOMUnitTypes = "px" | "rem" | "w" | "h" + +export class DOMUnit { + public type: DOMUnitTypes + public value: number + + public constructor(value: number, type: DOMUnitTypes) { + this.value = value + this.type = type + } + + public evaluate(element: Element, verbose: boolean = false): number { + if (verbose) console.debug(`${this.value} ${this.type} END UNIT`) + switch (this.type) { + case "px": + return this.value + case "rem": + return this.value * getFontSize(element) + case "w": + return this.value * element.clientWidth + case "h": + return this.value * element.clientHeight + default: + return 0 + } + } +} diff --git a/fission/src/util/Utility.ts b/fission/src/util/Utility.ts index f0e62c88d4..bf3c226863 100644 --- a/fission/src/util/Utility.ts +++ b/fission/src/util/Utility.ts @@ -1,3 +1,8 @@ export function ternaryOnce(obj: A | undefined, ifTrue: (x: A) => B, ifFalse: () => B): B { return obj ? ifTrue(obj) : ifFalse() } + +export function getFontSize(element: Element): number { + const str = window.getComputedStyle(element).fontSize + return Number(str.substring(0, str.length - 2)) +} diff --git a/fission/tailwind.config.js b/fission/tailwind.config.mjs similarity index 100% rename from fission/tailwind.config.js rename to fission/tailwind.config.mjs diff --git a/fission/vite.config.ts b/fission/vite.config.ts index d1120aebe9..428d7d0616 100644 --- a/fission/vite.config.ts +++ b/fission/vite.config.ts @@ -1,47 +1,57 @@ import { defineConfig } from 'vitest/config' import * as path from 'path' import react from '@vitejs/plugin-react-swc' +import basicSsl from '@vitejs/plugin-basic-ssl' import glsl from 'vite-plugin-glsl'; -const basePath = '/fission/' +const basePath = "/fission/" const serverPort = 3000 const dockerServerPort = 80 const useLocal = false +const useSsl = false + +const plugins = [ + react(), glsl({ + include: [ // Glob pattern, or array of glob patterns to import + '**/*.glsl', '**/*.wgsl', + '**/*.vert', '**/*.frag', + '**/*.vs', '**/*.fs' + ], + exclude: undefined, // Glob pattern, or array of glob patterns to ignore + warnDuplicatedImports: true, // Warn if the same chunk was imported multiple times + defaultExtension: 'glsl', // Shader suffix when no extension is specified + compress: false, // Compress output shader code + watch: true, // Recompile shader on change + root: '/' // Directory for root imports + }) +] + +if (useSsl) { + plugins.push(basicSsl()) +} // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react(), /* viteSingleFile() */ glsl({ - include: [ // Glob pattern, or array of glob patterns to import - '**/*.glsl', '**/*.wgsl', - '**/*.vert', '**/*.frag', - '**/*.vs', '**/*.fs' - ], - exclude: undefined, // Glob pattern, or array of glob patterns to ignore - warnDuplicatedImports: true, // Warn if the same chunk was imported multiple times - defaultExtension: 'glsl', // Shader suffix when no extension is specified - compress: false, // Compress output shader code - watch: true, // Recompile shader on change - root: '/' // Directory for root imports - }) ], + plugins: plugins, resolve: { alias: [ - { find: '@/components', replacement: path.resolve(__dirname, 'src', 'ui', 'components') }, - { find: '@/modals', replacement: path.resolve(__dirname, 'src', 'ui', 'modals') }, - { find: '@/panels', replacement: path.resolve(__dirname, 'src', 'ui', 'panels') }, - { find: '@', replacement: path.resolve(__dirname, 'src') } - ] + { find: "@/components", replacement: path.resolve(__dirname, "src", "ui", "components") }, + { find: "@/modals", replacement: path.resolve(__dirname, "src", "ui", "modals") }, + { find: "@/panels", replacement: path.resolve(__dirname, "src", "ui", "panels") }, + { find: "@", replacement: path.resolve(__dirname, "src") }, + ], }, test: { testTimeout: 5000, globals: true, - environment: 'jsdom', + environment: "jsdom", browser: { enabled: true, - name: 'chromium', + name: "chromium", headless: true, - provider: 'playwright' - } + provider: "playwright", + }, }, server: { // this ensures that the browser opens upon server start @@ -49,28 +59,30 @@ export default defineConfig({ // this sets a default port to 3000 port: serverPort, cors: false, - proxy: useLocal ? { - '/api/mira': { - target: `http://localhost:${serverPort}${basePath}`, - changeOrigin: true, - secure: false, - rewrite: (path) => path.replace(/^\/api\/mira/, '/Downloadables/Mira') - }, - '/api/aps': { - target: `http://localhost:${dockerServerPort}/`, - changeOrigin: true, - secure: false, - }, - } : { - '/api': { - target: `https://synthesis.autodesk.com/`, - changeOrigin: true, - secure: true, - }, - }, + proxy: useLocal + ? { + "/api/mira": { + target: `http://localhost:${serverPort}${basePath}`, + changeOrigin: true, + secure: false, + rewrite: path => path.replace(/^\/api\/mira/, "/Downloadables/Mira"), + }, + "/api/aps": { + target: `http://localhost:${dockerServerPort}/`, + changeOrigin: true, + secure: false, + }, + } + : { + "/api": { + target: `https://synthesis.autodesk.com/`, + changeOrigin: true, + secure: true, + }, + }, }, build: { - target: 'esnext', + target: "esnext", }, - base: basePath + base: basePath, }) diff --git a/installer/Linux/.gitignore b/installer/Linux/.gitignore deleted file mode 100644 index 2bdd6e8d6c..0000000000 --- a/installer/Linux/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -Synthesis.AppDir/usr/bin/* -Synthesis.AppDir/fields/* -Synthesis.AppDir/robots/* - -*.md5 -*.AppImage diff --git a/installer/Linux/README.md b/installer/Linux/README.md deleted file mode 100644 index 46e539d52a..0000000000 --- a/installer/Linux/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# `>_` Synthesis App Image - -For running Synthesis we have decided to package our application as an AppImage. It allows Synthesis to be packaged as a single .AppImage file. This also allows for users to run Synthesis without needing a specific distribution. - -### Initial Setup ### -It is recommended that you update the packages on your system. For arch users, run `pacman -Syu` as root. Debian users can run `apt update && apt upgrade` as root. - -### Dependencies ### -Certain dependencies are necessary in order to package Synthesis as an AppImage. - -##### FUSE ##### -For Arch, you may need to run: `pacman -S fuse` as root. For Debian, run: `apt install fuse` as root. -If you are still encountering issues, refer to this page: https://docs.appimage.org/user-guide/troubleshooting/fuse.html#ref-install-fuse - -##### appimagetool ##### -You will also end up needing appimagetool to actually create the AppImage file. However the `package.sh` script will prompt you and install it automatically. If you wish to install in manually, download the latest release from here: https://github.com/AppImage/AppImageKit/releases and make it executable. It should be called appimagetool-$ARCH.AppImage ($ARCH = whatever architecture you are using to package synthesis; most likely x86_64). - -### Initial Setup ### -In order to acheive the proper package structure for proper extraction, you must first compile a Unity build as: `Synthesis.x86_64` and store it along with all other files and directories that came with it somewhere on your machine. - -Note: It is important that you do not modify or remove any of the files and folders that come built with the `Synthesis.x86_64` file. - -It is also strongly recommended that you have some fields and robots exported in the Mirabuf format. - -### Packaging ### -The recommended way of creating the AppImage is by using the `package.sh` script. You may also opt to package Synthesis manually. There is some documentation for that process but it is recommended that you have a good understanding of what you are doing if you choose this option. - -To run the script, you will likely need to make it executable by running: `chmod +x package.sh` in your preferred terminal. You may also right click on `package.sh` in a file browser and select the option to make it executable. - -Now run the script and specify input directories for the version of synthesis you compiled as well as fields and robots: `./init.sh -f /path/to/fields/ -r /path/to/robots/ -b /path/to/synthesis/` - -Note: While it is not strictly necessary to include fields and robots, it is strongly recommended to include at least one of each - -If it is not already installed, the script will ask to install appimagetool. We recommended answering yes as it will install appimagetool.AppImage to the `~/Applications/` directory and is necessary for creating AppImage files. - -### Installing appimagetool ### -appimagetool is the name of the program that is used to create AppImages. You can download and install appimagetool through the official website https://appimage.github.io/appimagetool/ or get it through your distribution's package manager. - -Note: appimagetool is usually packaged under AppImageKit rather than as a standalone application. - -### Manual Packaging ### - -##### File locations ##### -Certain files must be moved to the correct locations. First you should move any robot files to `Synthesis.AppDir/robots` (create it if it doesn't exist). Do the same for field files but put them in `Synthesis.AppDir/fields` (create it if it doesn't exist). Finally, move all files and directories in your Synthesis build directory into `Synthesis.AppDir/usr/bin/` (once again, create it if it doesn't exist). - -##### Creating The AppImage ##### -Finally you can create your AppImage! Make sure you have all dependencies installed and run: `ARCH=x86_64 appimagetool Synthesis.AppDir` which will create the Synthesis AppImage. - -Note: Run this instead if you installed appimagetool locally: `ARCH=x86_64 /path/to/appimagetool Synthesis.AppDir` - -### Final Note ### -When the end user is downloading the AppImage file, it is strongly recommended to have them put it in the `~/Applications/` directory. This allows it to be found by appimaged as well as itself when running uninstall. It is also recommended to allow them to download the checksum.md5 file so that the file integrity can be verified using `md5sum -c checksum.md5` in the same directory as the AppImage and md5 files. - -### Troubleshooting ### -Refer to the AppImage troubleshooting page first if you are having issues: https://docs.appimage.org/user-guide/troubleshooting/index.html -The general documentation may be of use as well: https://docs.appimage.org/index.html -If the issues persist, open a github issue with details about the problem. - diff --git a/installer/Linux/Synthesis.AppDir/.DirIcon b/installer/Linux/Synthesis.AppDir/.DirIcon deleted file mode 120000 index 8c7bb9a13f..0000000000 --- a/installer/Linux/Synthesis.AppDir/.DirIcon +++ /dev/null @@ -1 +0,0 @@ -synthesis.png \ No newline at end of file diff --git a/installer/Linux/Synthesis.AppDir/AppRun b/installer/Linux/Synthesis.AppDir/AppRun deleted file mode 100755 index da2da947f5..0000000000 --- a/installer/Linux/Synthesis.AppDir/AppRun +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/sh - -show_help() { - echo "-h display help message" - echo "-u run uninstall script" -} - -install_appimaged() { - mkdir -p ~/Applications - wget -c https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases -O - | grep "appimaged-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2) -P ~/Applications/ - chmod +x ~/Applications/appimaged-*.AppImage - - # Launch - ~/Applications/appimaged-*.AppImage & -} - -uninstall_synthesis() { - rm -R ~/.config/Autodesk/Synthesis/ - if [ -e ~/Applications/Synthesis*.AppImage ] - then - rm ~/Applications/Synthesis*.AppImage - fi - if [ -e ~/Applications/appimaged-*.AppImage ] - then - while true; do - read -p "Do You wish to try and uninstall appimaged? (recommended) (y/n): " yn - case $yn in - [Yy]* ) - rm ~/Applications/appimaged-*.AppImage - break - ;; - [Nn]* ) - break - ;; - * ) - echo "Please answer yes or no." - ;; - esac - done - fi -} - -run_synthesis() { - mkdir -p ~/.config/Autodesk/Synthesis/Mira/Fields - cp "$HERE/fields/"*.mira ~/.config/Autodesk/Synthesis/Mira/Fields - cp "$HERE/robots/"*.mira ~/.config/Autodesk/Synthesis/Mira/ - - if [ ! -e ~/Applications/appimaged-*.AppImage ] - then - while true; do - read -p "Do You wish to install and start appimaged? (recommended) (y/n): " yn - case $yn in - [Yy]* ) - install_appimaged; - break - ;; - [Nn]* ) - break - ;; - * ) - echo "Please answer yes or no." - ;; - esac - done - fi - - exec "$EXEC" -} - -HERE="$(dirname "$(readlink -f "${0}")")" -EXEC="$HERE/usr/bin/Synthesis.x86_64" - -OPTIND=1 -while getopts ":hu" opt; do - case "$opt" in - h|\?) - show_help - exit 0 - ;; - u) - uninstall_synthesis - exit 0 - ;; - esac -done - -shift $((OPTIND-1)) - -run_synthesis diff --git a/installer/Linux/Synthesis.AppDir/synthesis.desktop b/installer/Linux/Synthesis.AppDir/synthesis.desktop deleted file mode 100755 index 424ddebffb..0000000000 --- a/installer/Linux/Synthesis.AppDir/synthesis.desktop +++ /dev/null @@ -1,6 +0,0 @@ -[Desktop Entry] -Name=Synthesis -Exec=Synthesis.x86_64 -Icon=synthesis -Type=Application -Categories=Game diff --git a/installer/Linux/Synthesis.AppDir/synthesis.png b/installer/Linux/Synthesis.AppDir/synthesis.png deleted file mode 100644 index 35f6df0736..0000000000 Binary files a/installer/Linux/Synthesis.AppDir/synthesis.png and /dev/null differ diff --git a/installer/Linux/package.sh b/installer/Linux/package.sh deleted file mode 100755 index 8a584f90a1..0000000000 --- a/installer/Linux/package.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/sh - -# run before with path to a compiled build as well as robots and fields directory -# also allow user to specify version - -show_help() { - echo "-h display help message" - echo "-f specify the input directory for fields" - echo "-r specify the input directory for robots" - echo "-b specify the build directory of synthesis" -} - -install_appimagetool() { - mkdir -p ~/Applications/ - wget -c https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -P ~/Applications/ - chmod +x ~/Applications/appimagetool-x86_64.AppImage -} - -create_appimage() { - ~/Applications/appimagetool-x86_64.AppImage $APP_DIR - md5sum $INIT_DIR/Synthesis-x86_64.AppImage > $INIT_DIR/checksum.md5 - md5sum -c $INIT_DIR/checksum.md5 -} - -OPTIND=1 - -ARCH="x86_64" -APP_NAME="Synthesis" -INIT_DIR="$(dirname "$(readlink -f "${0}")")" -APP_DIR="$INIT_DIR/$APP_NAME.AppDir" - -mkdir -p "$APP_DIR/usr/bin/" -mkdir -p "$APP_DIR/fields" -mkdir -p "$APP_DIR/robots" - -while getopts "h?f:r:b:" opt; do - case "$opt" in - h|\?) - show_help - exit 0 - ;; - f) - fields="$OPTARG" - ;; - r) - robots="$OPTARG" - ;; - b) - build="$OPTARG" - ;; - esac -done - -shift $((OPTIND-1)) - -if [ ! -n "$build" ] ; then - echo "Specify synthesis build directory using \"-b\"" - exit 1 -fi - -if [ -n "$fields" ] ; then - cp "$fields/"*.mira "$APP_DIR/fields/" -fi - -if [ -n "$robots" ] ; then - cp "$robots/"*.mira "$APP_DIR/robots/" -fi - -cp -R "$build/"* "$APP_DIR/usr/bin" -chmod +x "$APP_DIR/AppRun" -chmod +x "$APP_DIR/synthesis.desktop" -chmod +x "$APP_DIR/usr/bin/Synthesis.x86_64" - -if [ ! -e ~/Applications/appimagetool-*.AppImage ] ; then - while true; do - read -p "Do you wish to install appimagetool (Needed for creating the AppImage file) (y/n): " yn - case $yn in - [Yy]* ) - install_appimagetool - break - ;; - [Nn]* ) - break - ;; - * ) - echo "Please answer yes or no." - ;; - esac - done -fi - -if [ -e ~/Applications/appimagetool-x86_64.AppImage ] ; then - create_appimage -else - echo "Install appimagetool before creating AppImage" -fi diff --git a/installer/OSX-DMG/.gitignore b/installer/OSX-DMG/.gitignore deleted file mode 100644 index 5ddb15821b..0000000000 --- a/installer/OSX-DMG/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -source_folder/Synthesis.app/ -exporter_source_folder/ -Synthesis-Installer.dmg -create-dmg/ -addins-folder-link - -exporter-install-instructions.pdf - -*.dmg diff --git a/installer/OSX-DMG/README.md b/installer/OSX-DMG/README.md deleted file mode 100644 index 18cd11dd1c..0000000000 --- a/installer/OSX-DMG/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Synthesis OSX Installer (DMG Version) - -## Setup -1. Install `create-dmg`: -``` -$ git clone git@github.com:create-dmg/create-dmg.git -``` - -2. Copy the signed Synthesis app into source_folder: -``` -$ cp -r [Location of app] ./source_folder/Synthesis.app -``` - -2. Compile the `exporter-install-instructions.md` into a PDF. I recommend using the Yzane extension in VSCode. - -## Create Disk Image -Run the `make-installer.sh` shell script: -``` -$ ./make-installer.sh -``` - -Disk Image will be created at `/installer/OSX-DMG/Synthesis-Installer.dmg` - -## Notes -Update `source_folder/license.html` as needed as well as settings for the `create-dmg` command inside of `make-installer.sh`. See [create-dmg repository](https://github.com/create-dmg/create-dmg) for configuration information. \ No newline at end of file diff --git a/installer/OSX-DMG/SynthesisMacInstallerBackground.png b/installer/OSX-DMG/SynthesisMacInstallerBackground.png deleted file mode 100644 index 0d4d27e9a1..0000000000 Binary files a/installer/OSX-DMG/SynthesisMacInstallerBackground.png and /dev/null differ diff --git a/installer/OSX-DMG/exporter-install-instructions.md b/installer/OSX-DMG/exporter-install-instructions.md deleted file mode 100644 index 2b99e16696..0000000000 --- a/installer/OSX-DMG/exporter-install-instructions.md +++ /dev/null @@ -1,12 +0,0 @@ -# MacOS Exporter Installation Instructions -## Requirements -- Fusion 360 installed on your system. - -## Copy the Files -Move the `Exporter` folder, located in the installer pointing to this file, into the following directory: -``` -~/Library/Application Support/Autodesk/Autodesk Fusion 360/API/AddIns/ -``` - -## Usage -Upon opening Fusion 360, the exporter should automatically run and be made available in the `UTILITIES` tab. If not, please open the add-ins panel and manually run it. \ No newline at end of file diff --git a/installer/OSX-DMG/license.txt b/installer/OSX-DMG/license.txt deleted file mode 100755 index 4704a010d5..0000000000 --- a/installer/OSX-DMG/license.txt +++ /dev/null @@ -1,188 +0,0 @@ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and - -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -Copyright 2021 Autodesk inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/installer/OSX-DMG/make-installer.sh b/installer/OSX-DMG/make-installer.sh deleted file mode 100755 index e153bd8614..0000000000 --- a/installer/OSX-DMG/make-installer.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh -test -f Synthesis-Installer.dmg && rm Synthesis-Installer.dmg -./create-dmg/create-dmg \ - --volname "Synthesis Installer" \ - --background "SynthesisMacInstallerBackground.png" \ - --window-pos 200 120 \ - --window-size 375 320 \ - --text-size 12 \ - --icon-size 60 \ - --icon "Synthesis.app" 85 80 \ - --add-file Instructions.pdf exporter-install-instructions.pdf 288 190 \ - --add-file Exporter ../../exporter/SynthesisFusionAddin 85 190 \ - --hide-extension "Synthesis.app" \ - --app-drop-link 288 80 \ - --eula "license.txt" \ - --text-size 10 \ - "Synthesis-Installer.dmg" \ - "source_folder/" - -# --volicon "synthesis-icon.icns" \ -# --background "installer_background.png" \ -# --background "Synthesis-Background.png" \ diff --git a/installer/OSX/.gitignore b/installer/OSX/.gitignore deleted file mode 100644 index 57068f9f27..0000000000 --- a/installer/OSX/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -target/ -application/* -Autdoesk -!application/.gitkeep -!application/uninstall.sh -!application/background.png -!application/spec.json -*.pkg -dmg - -build -build.zip \ No newline at end of file diff --git a/installer/OSX/App/payload/Contents/Info.plist b/installer/OSX/App/payload/Contents/Info.plist deleted file mode 100755 index 734c2b7a8a..0000000000 --- a/installer/OSX/App/payload/Contents/Info.plist +++ /dev/null @@ -1,39 +0,0 @@ - - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - Synthesis - CFBundleGetInfoString - Unity Player version 2019.4.0f1 (0af376155913). (c) 2020 Unity Technologies ApS. All rights reserved. - CFBundleIconFile - PlayerIcon.icns - CFBundleIdentifier - com.Autodesk.Synthesis - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Synthesis - CFBundlePackageType - APPL - CFBundleShortVersionString - 0.1 - CFBundleSupportedPlatforms - - MacOSX - - CFBundleVersion - 0 - LSApplicationCategoryType - public.app-category.games - LSMinimumSystemVersion - 10.9.0 - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - - diff --git a/installer/OSX/App/payload/Contents/README.md b/installer/OSX/App/payload/Contents/README.md deleted file mode 100644 index 3b82ea5db1..0000000000 --- a/installer/OSX/App/payload/Contents/README.md +++ /dev/null @@ -1 +0,0 @@ -## Synthesis.zip diff --git a/installer/OSX/App/scripts/postinstall b/installer/OSX/App/scripts/postinstall deleted file mode 100755 index 2c990d0d53..0000000000 --- a/installer/OSX/App/scripts/postinstall +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -mv $2/Contents/Synthesis.zip /Applications/ -rm -rf $2 -cd /Applications/ -unzip /Applications/Synthesis.zip -rm -rf Synthesis.zip -exit 0 diff --git a/installer/OSX/App/scripts/preinstall b/installer/OSX/App/scripts/preinstall deleted file mode 100755 index 06bd986563..0000000000 --- a/installer/OSX/App/scripts/preinstall +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exit 0 diff --git a/installer/OSX/Assets/payload/Contents/Info.plist b/installer/OSX/Assets/payload/Contents/Info.plist deleted file mode 100755 index 734c2b7a8a..0000000000 --- a/installer/OSX/Assets/payload/Contents/Info.plist +++ /dev/null @@ -1,39 +0,0 @@ - - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - Synthesis - CFBundleGetInfoString - Unity Player version 2019.4.0f1 (0af376155913). (c) 2020 Unity Technologies ApS. All rights reserved. - CFBundleIconFile - PlayerIcon.icns - CFBundleIdentifier - com.Autodesk.Synthesis - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Synthesis - CFBundlePackageType - APPL - CFBundleShortVersionString - 0.1 - CFBundleSupportedPlatforms - - MacOSX - - CFBundleVersion - 0 - LSApplicationCategoryType - public.app-category.games - LSMinimumSystemVersion - 10.9.0 - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - - diff --git a/installer/OSX/Assets/payload/Contents/Synthesis/README.md b/installer/OSX/Assets/payload/Contents/Synthesis/README.md deleted file mode 100644 index 970708317f..0000000000 --- a/installer/OSX/Assets/payload/Contents/Synthesis/README.md +++ /dev/null @@ -1 +0,0 @@ -## Asset/Data Files diff --git a/installer/OSX/Assets/scripts/postinstall b/installer/OSX/Assets/scripts/postinstall deleted file mode 100755 index 421ba3de1c..0000000000 --- a/installer/OSX/Assets/scripts/postinstall +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -rm -rf ~/.config/Autodesk/Synthesis -mv $2/Contents/Synthesis ~/.config/Autodesk/ -rm -rf $2 -exit 0 diff --git a/installer/OSX/Assets/scripts/preinstall b/installer/OSX/Assets/scripts/preinstall deleted file mode 100755 index 06bd986563..0000000000 --- a/installer/OSX/Assets/scripts/preinstall +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exit 0 diff --git a/installer/OSX/Exporter/payload/Contents/Info.plist b/installer/OSX/Exporter/payload/Contents/Info.plist deleted file mode 100755 index 734c2b7a8a..0000000000 --- a/installer/OSX/Exporter/payload/Contents/Info.plist +++ /dev/null @@ -1,39 +0,0 @@ - - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - Synthesis - CFBundleGetInfoString - Unity Player version 2019.4.0f1 (0af376155913). (c) 2020 Unity Technologies ApS. All rights reserved. - CFBundleIconFile - PlayerIcon.icns - CFBundleIdentifier - com.Autodesk.Synthesis - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Synthesis - CFBundlePackageType - APPL - CFBundleShortVersionString - 0.1 - CFBundleSupportedPlatforms - - MacOSX - - CFBundleVersion - 0 - LSApplicationCategoryType - public.app-category.games - LSMinimumSystemVersion - 10.9.0 - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - - diff --git a/installer/OSX/Exporter/payload/Contents/SynthesisInventorGltfExporter/README.md b/installer/OSX/Exporter/payload/Contents/SynthesisInventorGltfExporter/README.md deleted file mode 100644 index 0ec8650013..0000000000 --- a/installer/OSX/Exporter/payload/Contents/SynthesisInventorGltfExporter/README.md +++ /dev/null @@ -1 +0,0 @@ -## Exporter Files diff --git a/installer/OSX/Exporter/scripts/postinstall b/installer/OSX/Exporter/scripts/postinstall deleted file mode 100755 index 4000f1e866..0000000000 --- a/installer/OSX/Exporter/scripts/postinstall +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -mv $2/Contents/SynthesisFusionAddin ~/Library/Application\ Support/Autodesk/Autodesk\ Fusion\ 360/API/AddIns/ -rm -rf $2 -exit 0 diff --git a/installer/OSX/Exporter/scripts/preinstall b/installer/OSX/Exporter/scripts/preinstall deleted file mode 100755 index 495e3edbba..0000000000 --- a/installer/OSX/Exporter/scripts/preinstall +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -mkdir -p ~/Library/Application Support/Autodesk/Autodesk Fusion 360/API/AddIns/ -exit 0 diff --git a/installer/OSX/Installer/Distribution.xml b/installer/OSX/Installer/Distribution.xml deleted file mode 100755 index 4d54db2521..0000000000 --- a/installer/OSX/Installer/Distribution.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - App.pkg - - - - Assets.pkg - - - - Exporter.pkg - diff --git a/installer/OSX/Installer/Resources/conclusion.html b/installer/OSX/Installer/Resources/conclusion.html deleted file mode 100755 index 9151c5f509..0000000000 --- a/installer/OSX/Installer/Resources/conclusion.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - -
-

Synthesis: An Autodesk Technology | 5.0.0.0 ALPHA

-

Thank you for installing Synthesis: An Autodesk Technology

-
-
-

In order to improve this product and understand how it is used, we collect non-personal product usage information. This usage information may consist of custom events like Replay Mode, Driver Practice Mode, Tutorial Link Clicked, etc. This information is not used to identify or contact you. You can turn data collection off from the Control Panel within the simulator. By installing, you agree that you have read the terms of service agreement and data collection statement above.

-
-
-

Resources

-

Go through the following link for additional information.

- -
-
-
-

Copyright © 2021 Autodesk inc.

-
- - diff --git a/installer/OSX/Installer/Resources/license.html b/installer/OSX/Installer/Resources/license.html deleted file mode 100755 index 6be72e4428..0000000000 --- a/installer/OSX/Installer/Resources/license.html +++ /dev/null @@ -1,206 +0,0 @@ - - - - - - -
-

- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/

- -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

- -1. Definitions.

- -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. -


-"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. -


-"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. -


-"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. -


-"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. -


-"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. -


-"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). -


-"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. -


-"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." -


-"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. -


-2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. -


-3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. -


-4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: -


-(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and -


-(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and -


-(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and -


-(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. -


-You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. -


-5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. -


-6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. -


-7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. -


-8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. -


-9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. -


-END OF TERMS AND CONDITIONS -


-APPENDIX: How to apply the Apache License to your work. -


-Copyright 2021 Autodesk inc. -


-Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at -


-http://www.apache.org/licenses/LICENSE-2.0 -


-Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -


-

-

Click “Continue" to continue the setup

-
- - diff --git a/installer/OSX/Installer/Resources/welcome.html b/installer/OSX/Installer/Resources/welcome.html deleted file mode 100755 index dcea4723b3..0000000000 --- a/installer/OSX/Installer/Resources/welcome.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - -
-

This will install Synthesis: An Autodesk Application 5.0.0.0 ALPHA on your computer. You will be guided through the steps necessary to install this software.

-

Click “Continue" to continue the setup

-
- - diff --git a/installer/OSX/README.md b/installer/OSX/README.md deleted file mode 100644 index 846f17c8b2..0000000000 --- a/installer/OSX/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# OSX packager - -## Build Steps : - -1. Get signed Synthesis.app - -2. Zip Synthesis.app - -3. Copy Synthesis.zip to file system ` cp [Synthesis.zip] [synthesis/installer/OSX/App/payload/Contents] ` - - - Remove the Synthesis.zip placeholder ` rm [synthesis/installer/OSX/App/payload/Contents/README.md] ` - -3. Add data files to ` synthesis/installer/OSX/Assets/payload/Contents/Synthesis ` - - - Remove the data file placeholder ` rm [synthesis/installer/OSX/Assets/payload/Contents/Synthesis/README.md] ` - -4. Add unzipped exporter files to ` synthesis/installer/OSX/Exporter/payload/Contents/SynthesisFusionGltfExporter ` - - - Remove the exporter file placeholder ` rm [synthesis/installer/OSX/Exporter/payload/Contents/SynthesisFusionGltfExporter/README.md] ` - -5. Change directories to the OSX installer directory ` cd [synthesis/installer/OSX] ` - -6. Run the pkginstall script ` ./pkginstall ` - -### Optional Build Steps - -Update the license, welcome and conclusion installer menus located in ` [synthesis/installer/OSX/Installer/Resources] ` - -## Package - -Publish the newly created Synthesis.pkg - -## Important Note - -**Do not** rename or move files - diff --git a/installer/OSX/pkginstall b/installer/OSX/pkginstall deleted file mode 100644 index 359e862088..0000000000 --- a/installer/OSX/pkginstall +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -#Priviledges -chmod -R 777 ./Assets/payload/Contents/Synthesis -#Build app -pkgbuild --install-location ~/.config/Autodesk/SynthesisAppInstall --root ./App/payload/ --scripts ./App/scripts/ --identifier org.autodesk.synthesis.app ./Installer/App.pkg -#Build data files -pkgbuild --install-location ~/.config/Autodesk/SynthesisAssetsInstall --root ./Assets/payload/ --scripts ./Assets/scripts/ --identifier org.autodesk.synthesis.assets ./Installer/Assets.pkg -#Build exporter -pkgbuild --install-location ~/.config/Autodesk/SynthesisExporterInstall --root ./Exporter/payload/ --scripts ./Exporter/scripts/ --identifier org.autodesk.synthesis.exporter ./Installer/Exporter.pkg -#Build installer -cd Installer -productbuild --distribution Distribution.xml --resources Resources/ ../Synthesis.pkg diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 0000000000..598f6b8ae6 --- /dev/null +++ b/installer/README.md @@ -0,0 +1,41 @@ +# Installers + +We recently transitioned to a platform independent, web-based application. As such, we no longer maintain a installer for the core simulator. We do, however, still have one for our Fusion Exporter. + +## Installing the Synthesis Fusion Exporter + +### Using an Installer + +- Visit [synthesis.autodesk.com/download](https://synthesis.autodesk.com/download.html) and select the installer for your operating system. + - Note that there is no installer for Linux since Fusion is only supported on Windows and Mac. +- Once you have downloaded the installer for your operating system (`.exe` for Windows and `.pkg` for Mac) go ahead and run the executable. + - Since we do not code sign our installers (as interns of Autodesk we have very little control over this) you may get a warning from your operating system. + - For Mac to get around this see [this](https://support.apple.com/en-tm/guide/mac-help/mh40616/mac) guide for more information. +- If you are at all concerned that we are doing something nefarious please feel free to [install the exporter manually.](#manual-install) + - Alternatively, you can even inspect how we build our installers [here](./exporter/) and build them yourself. + +### Manual Install + +- Navigate to [`synthesis.autodesk.com/download`](https://synthesis.autodesk.com/download.html). +- Find the Exporter source code zip download. + - Note that the source code is platform agnostic, it will work for **both** `Windows` and `Mac`. +- Once the source code for the Exporter is downloaded, unzip the folder. +- Next, if you haven't already, install `Autodesk Fusion`. +- Once Fusion is open, navigate to the `Utilities Toolbar`. +![image_caption](../tutorials/img/fusion/fusion-empty.png) +- Click on `Scripts and Add-ins` in the toolbar. +![image_caption](../tutorials/img/fusion/fusion-addins-highlight.png) +- Navigate to `Add-ins` and select the green plus icon. +![image_caption](../tutorials/img/fusion/fusion-addins-panel.png) +- Now navigate to wherever you extracted the original `.zip` source code file you downloaded. + - Make sure to select the folder that contains the `Synthesis.py` file, this is the entry point to the Exporter. +![image_caption](../tutorials/img/fusion/fusion-add-addin.png) +- Once the extension is added you should be able to see it under `My Add-Ins`. +- Select `Synthesis` from the `My Add-Ins` drop down and click `Run` in the bottom right. +![image_caption](../tutorials/img/fusion/fusion-addin-synthesis.png) +- The first time you run the extension it may prompt you to restart Fusion, this is totally normal. +- Once you restart Fusion the extension will run on startup, you will be able to find it on the right side of the toolbar +under the `Utilities` tab. +![image_caption](../tutorials/img/fusion/fusion-utilities-with-synthesis.png) + +Thanks for installing the Synthesis Fusion Exporter! For any additional help visit our [Synthesis Community Discord Server](https://www.discord.gg/hHcF9AVgZA) where you can talk directly to our developers. diff --git a/installer/Windows/.gitignore b/installer/Windows/.gitignore deleted file mode 100644 index 65fac130c8..0000000000 --- a/installer/Windows/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -Robots/ -Fields/ -Exporter/ -Themes/ -MixAndMatch/ -Synthesis/ - -Robots.zip -Fields.zip -Themes.zip -MixAndMatch.zip diff --git a/installer/Windows/MainInstaller.nsi b/installer/Windows/MainInstaller.nsi deleted file mode 100644 index 7cf07b658e..0000000000 --- a/installer/Windows/MainInstaller.nsi +++ /dev/null @@ -1,332 +0,0 @@ -!include MUI2.nsh -!include x64.nsh -!define PRODUCT_VERSION "6.0.0" - -Name "Synthesis" - -;Icon "W16_SYN_launch.ico" -Icon "synthesis-logo-64x64.ico" - -Caption "Synthesis ${PRODUCT_VERSION} Setup" - -OutFile "SynthesisWin${PRODUCT_VERSION}.exe" - -InstallDir $PROGRAMFILES64\Autodesk\Synthesis - -InstallDirRegKey HKLM "Software\Synthesis" "Install_Dir" - -RequestExecutionLevel admin - - Section - ${If} ${RunningX64} - goto install_stuff - ${Else} - MessageBox MB_OK "ERROR: This install requires a 64 bit system." - Quit - ${EndIf} - install_stuff: - SectionEnd - -;-------------------------------- - -;Interface Settings - !define MUI_WELCOMEFINISHPAGE_BITMAP "W21_SYN_sidebar.bmp" - !define MUI_UNWELCOMEFINISHPAGE_BITMAP "W21_SYN_sidebar.bmp" - !define MUI_ICON "synthesis-logo-64x64.ico" - !define MUI_UNICON "synthesis-logo-64x64.ico" - !define MUI_HEADERIMAGE - !define MUI_HEADERIMAGE_BITMAP "orange-r.bmp" - !define MUI_HEADERIMAGE_RIGHT - !define MUI_ABORTWARNING - !define MUI_FINISHPAGE_TEXT 'Synthesis has been successfully installed on your system. $\r$\n $\r$\nIn order to improve this product and understand how it is used, we collect non-personal product usage information. This usage information may consist of custom events like Replay Mode, Driver Practice Mode, etc. $\r$\nThis information is not used to identify or contact you. $\r$\nYou can turn data collection off from the Control Panel within the simulator. $\r$\n $\r$\nBy clicking Finish, you agree that you have read the terms of service agreement and data collection statement above.' - !define MUI_FINISHPAGE_LINK "Synthesis Discord" - !define MUI_FINISHPAGE_LINK_LOCATION "https://www.discord.gg/hHcF9AVgZA" - -;-------------------------------- - - ; Installer GUI Pages - !insertmacro MUI_PAGE_WELCOME - !insertmacro MUI_PAGE_LICENSE "..\..\LICENSE.txt" - !insertmacro MUI_PAGE_COMPONENTS - !insertmacro MUI_PAGE_INSTFILES - !insertmacro MUI_PAGE_FINISH - - ; Uninstaller GUI Pages - !insertmacro MUI_UNPAGE_WELCOME - !insertmacro MUI_UNPAGE_CONFIRM - !insertmacro MUI_UNPAGE_INSTFILES - !insertmacro MUI_UNPAGE_FINISH - -;-------------------------------- - - ; Default Language - !insertmacro MUI_LANGUAGE "English" - -Section - -IfFileExists "$APPDATA\Autodesk\Synthesis" +1 +28 - MessageBox MB_YESNO "You appear to have Synthesis installed; would you like to reinstall it?" IDYES true IDNO false - true: - DeleteRegKey HKLM SOFTWARE\Synthesis - - ; Remove fusion plugins - RMDir /r "$APPDATA\Autodesk\Autodesk Fusion 360\API\AddIns\FusionRobotExporter" - RMDir /r "$APPDATA\Autodesk\Autodesk Fusion 360\API\AddIns\Synthesis" - RMDir /r "$APPDATA\Autodesk\Autodesk Fusion 360\API\AddIns\FusionExporter" - RMDir /r "$APPDATA\Autodesk\ApplicationPlugins\FusionRobotExporter.bundle" - RMDir /r "$APPDATA\Autodesk\ApplicationPlugins\FusionSynth.bundle" - - ; Remove inventor plugins - Delete "$APPDATA\Autodesk\Inventor 2021\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete "$APPDATA\Autodesk\Inventor 2020\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete "$APPDATA\Autodesk\Inventor 2019\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete "$APPDATA\Autodesk\Inventor 2018\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete "$APPDATA\Autodesk\Inventor 2017\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete "$APPDATA\Autodesk\ApplicationPlugins\Autodesk.InventorRobotExporter.Inventor.addin" - RMDir /r "$APPDATA\Autodesk\ApplicationPlugins\InventorRobotExporter" - - ; Remove deprecated bxd inventor plugins - Delete "$APPDATA\Autodesk\Inventor 2020\Addins\autodesk.BxDRobotExporter.inventor.addin" - Delete "$APPDATA\Autodesk\Inventor 2019\Addins\autodesk.BxDRobotExporter.inventor.addin" - Delete "$APPDATA\Autodesk\Inventor 2019\Addins\autodesk.BxDFieldExporter.inventor.addin" - Delete "$APPDATA\Autodesk\Inventor 2018\Addins\autodesk.BxDRobotExporter.inventor.addin" - Delete "$APPDATA\Autodesk\Inventor 2018\Addins\autodesk.BxDFieldExporter.inventor.addin" - Delete "$APPDATA\Autodesk\Inventor 2017\Addins\autodesk.BxDRobotExporter.inventor.addin" - Delete "$APPDATA\Autodesk\Inventor 2017\Addins\autodesk.BxDFieldExporter.inventor.addin" - Delete "$APPDATA\Autodesk\ApplicationPlugins\Autodesk.BxDRobotExporter.Inventor.addin" - RMDir /r "$APPDATA\Autodesk\ApplicationPlugins\BxDRobotExporter" - RMDIR /r $APPDATA\RobotViewer - - ; Remove excess shortcuts - Delete "$SMPROGRAMS\Synthesis.lnk" - Delete "$DESKTOP\Synthesis.lnk" - Delete "$SMPROGRAMS\BXD Synthesis.lnk" - Delete "$DESKTOP\BXD Synthesis.lnk" - Delete "$SMPROGRAMS\Autodesk Synthesis.lnk" - Delete "$DESKTOP\Autodesk Synthesis.lnk" - Delete "$DESKTOP\FieldExporter.lnk" - - ; Remove obsolete directories - RMDir /r $INSTDIR - RMDir /r $APPDATA\Synthesis - RMDir /r $APPDATA\BXD_Aardvark - RMDir /r $PROGRAMFILES\Autodesk\Synthesis - - DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Autodesk Synthesis" - DeleteRegKey HKCU "SOFTWARE\Autodesk\Synthesis" - ;DeleteRegKey HKCU "SOFTWARE\Autodesk\BXD Synthesis" - - Goto next - - false: - Quit - - next: - -# default section end -SectionEnd - -Section "Synthesis (required)" Synthesis - - SectionIn RO - - ; Set output path to the installation directory. - SetOutPath $INSTDIR - - File /r "Synthesis\*" - - CreateShortCut "$SMPROGRAMS\Synthesis.lnk" "$INSTDIR\Synthesis.exe" - CreateShortCut "$DESKTOP\Synthesis.lnk" "$INSTDIR\Synthesis.exe" - - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Autodesk Synthesis" \ - "DisplayName" "Autodesk Synthesis" - - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Autodesk Synthesis" \ - "DisplayIcon" "$INSTDIR\uninstall.exe" - - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Autodesk Synthesis" \ - "Publisher" "Autodesk" - - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Autodesk Synthesis" \ - "URLInfoAbout" "synthesis.autodesk.com/tutorials" - - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Autodesk Synthesis" \ - "DisplayVersion" "${PRODUCT_VERSION}" - - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Autodesk Synthesis" \ - "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" - - - WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Synthesis" "NoModify" 1 - WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Synthesis" "NoRepair" 1 - WriteUninstaller "uninstall.exe" - -SectionEnd - -/* -Section "Inventor Exporter Plugin" iExporter - - ; Set extraction path to Inventor plugin directory - SetOutPath $INSTDIR\Exporter - File /r "InventorExporter\*" - - SetOutPath $APPDATA\Autodesk\ApplicationPlugins - File /r "InventorExporter\Autodesk.InventorRobotExporter.Inventor.addin" - -SectionEnd -*/ - -Section "Fusion Exporter Plugin" fExporter - - ; Set extraction path to Fusion plugin directories - SetOutPath "$APPDATA\Autodesk\Autodesk Fusion 360\API\AddIns\Synthesis" - File /r "Exporter\*" - ; File /r "..\..\exporter\SynthesisFusionAddin\*" - - ; SetOutPath "$APPDATA\Autodesk\ApplicationPlugins\FusionRobotExporter.bundle\Contents\" - ; File /r "FusionExporter\FusionRobotExporter.dll" - -SectionEnd - -Section "Robots and Fields" RobotFiles - - ; Set extraction path for preloaded robot files - SetOutPath $APPDATA\Autodesk\Synthesis\Mira - File /r "Robots\*" - - SetOutPath $APPDATA\Autodesk\Synthesis\Mira\Fields - File /r "Fields\*" - -SectionEnd - -Section "PartBuilder Samples" PartBuilder - - ; Set extraction path for preloaded robot files - SetOutPath $APPDATA\Autodesk\Synthesis\MixAndMatch - File /r "MixAndMatch\*" - -SectionEnd - -Section "Themes" Themes - - ; Set extraction path for preloaded robot files - SetOutPath $APPDATA\Autodesk\Synthesis\Themes - File /r "Themes\*" - -SectionEnd - -/* -Section "Code Emulator" Emulator - - ; INetC.dll must be installed to proper NSIS Plugins x86 directories - inetc::get "https://qemu.weilnetz.de/w64/2019/qemu-w64-setup-20190724.exe" "$PLUGINSDIR\qemu-w64-setup-20190724.exe" - Pop $R0 ;Get the return value - - ${If} $R0 == "OK" ;Return value should be "OK" - SetOutPath $APPDATA\Autodesk\Synthesis\Emulator - File /r "Emulator\*" - HideWindow - ExecWait '"$PLUGINSDIR\qemu-w64-setup-20190724.exe" /SILENT' - ShowWindow hwnd show_state - ${Else} - MessageBox MB_ICONSTOP "Error: $R0" ;Show cancel/error message - ${EndIf} - -SectionEnd -*/ - -;-------------------------------- -;Component Descriptions - - LangString DESC_Synthesis ${LANG_ENGLISH} "The Simulator Engine is the real-time physics environment which simulates the robots and fields." - ; LangString DESC_iExporter ${LANG_ENGLISH} "The Inventor Exporter Plugin is an Inventor addin used to export Autodesk Inventor Assemblies directly into the simulator" - LangString DESC_fExporter ${LANG_ENGLISH} "The Fusion360 Exporter Plugin is a Fusion addin used to export Autodesk Fusion Assemblies directly into the simulator" - LangString DESC_RobotFiles ${LANG_ENGLISH} "A library of sample robots and fields pre-loaded into the simulator" - LangString DESC_PartBuilder ${LANG_ENGLISH} "A library of sample parts to use in Robot Builder" - LangString DESC_Themes ${LANG_ENGLISH} "Preinstalled themes" - ; LangString DESC_Emulator ${LANG_ENGLISH} "The Robot Code Emulator allows you to emulate your C++ & JAVA robot code in the simulator" - - !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN - !insertmacro MUI_DESCRIPTION_TEXT ${Synthesis} $(DESC_Synthesis) - ; !insertmacro MUI_DESCRIPTION_TEXT ${iExporter} $(DESC_iExporter) - !insertmacro MUI_DESCRIPTION_TEXT ${fExporter} $(DESC_fExporter) - !insertmacro MUI_DESCRIPTION_TEXT ${RobotFiles} $(DESC_RobotFiles) - !insertmacro MUI_DESCRIPTION_TEXT ${PartBuilder} $(DESC_PartBuilder) - !insertmacro MUI_DESCRIPTION_TEXT ${Themes} $(DESC_Themes) - ; !insertmacro MUI_DESCRIPTION_TEXT ${Emulator} $(DESC_Emulator) - !insertmacro MUI_FUNCTION_DESCRIPTION_END - -;-------------------------------- - -Section "Uninstall" - - MessageBox MB_YESNO "Would you like to remove your robot files?" IDNO NawFam - RMDir /r /REBOOTOK $APPDATA\Synthesis - RMDir /r /REBOOTOK $APPDATA\Autodesk\Synthesis - - NawFam: - ; Remove registry keys - DeleteRegKey HKLM SOFTWARE\Synthesis - DeleteRegKey HKCU SOFTWARE\Autodesk\Synthesis - DeleteRegKey HKCU "SOFTWARE\Autodesk\BXD Synthesis" - DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Autodesk Synthesis" - - ; Remove installation directories - RMDir /r /REBOOTOK $INSTDIR - RMDir /r /REBOOTOK $PROGRAMFILES\Autodesk\Synthesis - RMDir /r /REBOOTOK $APPDATA\BXD_Aardvark - RMDir /r /REBOOTOK $APPDATA\SynthesisTEMP - - ; Remove fusion plugins - RMDir /r "$APPDATA\Autodesk\Autodesk Fusion 360\API\AddIns\FusionRobotExporter" - RMDir /r "$APPDATA\Autodesk\Autodesk Fusion 360\API\AddIns\Synthesis" - RMDir /r "$APPDATA\Autodesk\Autodesk Fusion 360\API\AddIns\FusionExporter" - RMDir /r "$APPDATA\Autodesk\ApplicationPlugins\FusionRobotExporter.bundle" - RMDir /r "$APPDATA\Autodesk\ApplicationPlugins\FusionSynth.bundle" - - ; Remove inventor plugins - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2022\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2021\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2020\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2019\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2018\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2017\Addins\Autodesk.InventorRobotExporter.Inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\ApplicationPlugins\Autodesk.InventorRobotExporter.Inventor.addin" - - ; Remove deprecated bxd inventor plugins - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2020\Addins\autodesk.BxDRobotExporter.inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2019\Addins\autodesk.BxDRobotExporter.inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2019\Addins\autodesk.BxDFieldExporter.inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2018\Addins\autodesk.BxDRobotExporter.inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2018\Addins\autodesk.BxDFieldExporter.inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2017\Addins\autodesk.BxDRobotExporter.inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\Inventor 2017\Addins\autodesk.BxDFieldExporter.inventor.addin" - Delete /REBOOTOK "$APPDATA\Autodesk\ApplicationPlugins\Autodesk.BxDRobotExporter.Inventor.addin" - RMDir /REBOOTOK "$APPDATA\Autodesk\ApplicationPlugins\BxDRobotExporter" - - ; Remove excess shortcuts - Delete "$SMPROGRAMS\Synthesis.lnk" - Delete "$DESKTOP\Synthesis.lnk" - Delete "$SMPROGRAMS\BXD Synthesis.lnk" - Delete "$DESKTOP\BXD Synthesis.lnk" - Delete "$SMPROGRAMS\Autodesk Synthesis.lnk" - Delete "$DESKTOP\Autodesk Synthesis.lnk" - Delete "$DESKTOP\FieldExporter.lnk" - - /* - ; Execute QEMU uninstaller - IfFileExists "$PROGRAMFILES64\qemu" file_found uninstall_complete - - file_found: - MessageBox MB_YESNO "Would you like to uninstall QEMU as well?" IDNO uninstall_complete - Exec '"$PROGRAMFILES64\qemu\qemu-uninstall.exe"' - Quit - - uninstall_complete: - */ - -SectionEnd - -Function .OnInstSuccess - Exec "$INSTDIR\Synthesis.exe" -FunctionEnd diff --git a/installer/Windows/README.md b/installer/Windows/README.md deleted file mode 100644 index 21ea8340c9..0000000000 --- a/installer/Windows/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# logoNullsoft Scriptable Install System - -For installation on Windows Operating Systems, we use our own custom written NSIS installer in order to extract all the necessary files to their proper locations on the system. - -- [MainInstaller(x64)](https://github.com/Autodesk/synthesis/blob/prod/installer/Windows/MainInstaller.nsi) - (Used for the full installation of Synthesis, only compatible on 64 bit operating systems.) -- [EngInstaller(x86)](https://github.com/Autodesk/synthesis/blob/prod/installer/Windows/EngInstaller(x86).nsi) - (Used only for installation on 32 bit operating systems and extracts just the Unity Engine.) - -### Compiling NSIS: -In order to compile the NSIS configuration properly, you must compile all of the individual components of Synthesis pertaining to the particular script you are trying to compile. Then the compiled components must be stored in the same directory as the NSIS script, in order for them to be packaged during NSIS compilation. For details on this process, feel free to contact matthew.moradi@autodesk.com - -### NSIS FAQ: - -Q: Will I need admin privileges to run the installer? - -A: Yup. - -Q: If I download an updated Synthesis installer, will running it replace all of my custom robot export files? - -A: No, _reinstalling_ Synthesis will only replace all of the application components, but your custom robots will be saved. - -Q: Is it possible to accidently install multiple versions of Synthesis? - -A: It shouldn't be. The installer will always replace any existing Synthesis installations on your system. diff --git a/installer/Windows/W21_SYN_sidebar.bmp b/installer/Windows/W21_SYN_sidebar.bmp deleted file mode 100644 index 60c007ab76..0000000000 Binary files a/installer/Windows/W21_SYN_sidebar.bmp and /dev/null differ diff --git a/installer/Windows/orange-install-nsis.ico b/installer/Windows/orange-install-nsis.ico deleted file mode 100644 index ef3975f56c..0000000000 Binary files a/installer/Windows/orange-install-nsis.ico and /dev/null differ diff --git a/installer/Windows/orange-r.bmp b/installer/Windows/orange-r.bmp deleted file mode 100644 index c74fbdd511..0000000000 Binary files a/installer/Windows/orange-r.bmp and /dev/null differ diff --git a/installer/Windows/synthesis-logo-64x64.ico b/installer/Windows/synthesis-logo-64x64.ico deleted file mode 100644 index 8fe648c48c..0000000000 Binary files a/installer/Windows/synthesis-logo-64x64.ico and /dev/null differ diff --git a/installer/exporter/.gitignore b/installer/exporter/.gitignore deleted file mode 100644 index 290dfd77ff..0000000000 --- a/installer/exporter/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/Synthesis -/*.zip diff --git a/installer/exporter/OSX/Scripts/preinstall b/installer/exporter/OSX/Scripts/preinstall new file mode 100755 index 0000000000..014f372637 --- /dev/null +++ b/installer/exporter/OSX/Scripts/preinstall @@ -0,0 +1,7 @@ +#!/bin/bash + +FUSION_ADDIN_LOCATION=~/Library/Application\ Support/Autodesk/ApplicationPlugins/ + +if [ ! -d "$FUSION_ADDIN_LOCATION" ]; then + mkdir -p "$FUSION_ADDIN_LOCATION" +fi diff --git a/installer/exporter/OSX/build.sh b/installer/exporter/OSX/build.sh new file mode 100755 index 0000000000..9b13e2d29a --- /dev/null +++ b/installer/exporter/OSX/build.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +FUSION_ADDIN_LOCATION=~/Library/Application\ Support/Autodesk/ApplicationPlugins/ +EXPORTER_SOURCE_DIR=../../../exporter/SynthesisFusionAddin/ + +mkdir -p tmp/ +cp -r synthesis.bundle tmp/ +rsync -av ../synthesis.bundle tmp/ +cp -r "$EXPORTER_SOURCE_DIR"/* tmp/synthesis.bundle/Contents/ + +pkgbuild --root tmp/ --identifier com.Autodesk.Synthesis --scripts Scripts/ --version 2.0.0 --install-location "$FUSION_ADDIN_LOCATION" SynthesisExporter.pkg +productbuild --distribution distribution.xml --package-path . SynthesisExporterInstaller.pkg + +rm SynthesisExporter.pkg +rm -r tmp/ diff --git a/installer/exporter/OSX/distribution.xml b/installer/exporter/OSX/distribution.xml new file mode 100644 index 0000000000..e46f643e6f --- /dev/null +++ b/installer/exporter/OSX/distribution.xml @@ -0,0 +1,14 @@ + + + Synthesis Exporter Installer + + + + + + + + + + #SynthesisExporter.pkg + diff --git a/installer/exporter/OSX/synthesis.bundle/Contents/Info.plist b/installer/exporter/OSX/synthesis.bundle/Contents/Info.plist new file mode 100644 index 0000000000..5c6c161002 --- /dev/null +++ b/installer/exporter/OSX/synthesis.bundle/Contents/Info.plist @@ -0,0 +1,46 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleGetInfoString + Synthesis addin for Fusion. + CFBundleIdentifier + Synthesis Exporter for Autodesk Fusion 360 + CFBundleShortVersionString + 2.0.0 + IFMajorVersion + 1 + IFMinorVersion + 0 + IFPkgFlagAllowBackRev + + IFPkgFlagAuthorizationAction + AdminAuthorization + IFPkgFlagBackgroundAlignment + topleft + IFPkgFlagBackgroundScaling + none + IFPkgFlagDefaultLocation + / + IFPkgFlagFollowLinks + + IFPkgFlagInstallFat + + IFPkgFlagIsRequired + + IFPkgFlagOverwritePermissions + + IFPkgFlagRelocatable + + IFPkgFlagRestartAction + NoRestart + IFPkgFlagRootVolumeOnly + + IFPkgFlagUpdateInstalledLanguages + + IFPkgFormatVersion + 0.1000000014901161 + + diff --git a/installer/exporter/README.md b/installer/exporter/README.md index 631eb3527b..aa5871ce16 100644 --- a/installer/exporter/README.md +++ b/installer/exporter/README.md @@ -1,14 +1,29 @@ -# Synthesis Exporter Installer +# Synthesis Exporter Installers -## Creating the installer -### Windows -1. Run `setup.bat`. This will copy the Synthesis exporter into the current directory. -2. Zip together the `install.bat` script and the `Synthesis` directory. +This `readme` is for developers of Synthesis or those looking to build the installers themselves, if you are just looking for how to install our Fusion Exporter please navigate to [`/installer/`](../). -### MacOS -1. Run `create.sh`. This will copy the Synthesis exporter into the current directory and create a zip file with the necessary files. +## Windows -## Using the installer -1. Download the zip file. -2. Unzip it anywhere (likely your Downloads folder). -3. Run the `install.bat` (`install.sh` for MacOS) script. +The windows installer has the following prerequisites: +- Python `3.9` or newer and pip. +- And that's it! + +### To Build: + +Once you have verified that python and pip are installed on your computer: +- Open a powershell window and navigate to [`/installer/exporter/Windows/`] +- Run `./build.bat` in powershell. +- After some time you should see `installer.exe` in your current directory. +- And that's it! You have now built the Synthesis Exporter Installer for Windows! +- You can then run the `.exe` from file explorer or alternatively, for debugging purposes, run `./installer.exe` from the terminal. + +## MacOS + +The Mac installer has zero prerequisites. Hooray! + +### To Build: + +- Navigate to [`/installer/exporter/OSX/`](./OSX/). +- Run `./build.sh` in your terminal. +- You should then find `SynthesisExporterInstaller.pkg` in your current directory. +- And that's it! You now have built the Synthesis Exporter Installer for MacOS! diff --git a/installer/exporter/Windows/build.bat b/installer/exporter/Windows/build.bat new file mode 100644 index 0000000000..a44de1a285 --- /dev/null +++ b/installer/exporter/Windows/build.bat @@ -0,0 +1,37 @@ +@echo off +setlocal enabledelayedexpansion +setlocal + +python -m pip install -r requirements.txt --user + +set "EXPORTER_SOURCE_DIR=..\..\..\exporter\SynthesisFusionAddin\" + +mkdir tmp\synthesis.bundle\Contents\ +xcopy ..\synthesis.bundle .\tmp\synthesis.bundle +xcopy /e /i "%EXPORTER_SOURCE_DIR%" tmp\synthesis.bundle\Contents +tar -a -c -f SynthesisExporter.zip -C tmp synthesis.bundle\* + +@REM Find and run pyinstaller, this is a workaround that allows you to call pip packages as scripts without +@REM them being added to the system PATH. +for /f "delims=" %%i in ('pip show pyinstaller') do ( + echo %%i | findstr /b /c:"Location:" >nul + if not errorlevel 1 ( + set "location_line=%%i" + ) +) + +set "executable=!location_line:Location: =!" +for %%a in ("%executable%") do set "executable=%%~dpa" +set "executable=%executable%Scripts\pyinstaller.exe " +set executable=%executable:~0,-1% + +%executable% --onefile --add-data "SynthesisExporter.zip;." installer.py + +move .\dist\installer.exe . +rmdir /s /q tmp +rmdir /s /q build +rmdir /s /q dist +del SynthesisExporter.zip +del installer.spec + +endlocal diff --git a/installer/exporter/Windows/installer.py b/installer/exporter/Windows/installer.py new file mode 100644 index 0000000000..ab09ec1e77 --- /dev/null +++ b/installer/exporter/Windows/installer.py @@ -0,0 +1,55 @@ +import os +import shutil +import sys +import tempfile +import zipfile +import ctypes + + +def getResourcePath(relativePath: str | os.PathLike[str]) -> None: + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + basePath = sys._MEIPASS + else: + basePath = os.path.dirname(__file__) + + return os.path.join(basePath, relativePath) + + +def extractFile(resourceName: str | os.PathLike[str], destinationFolder: str | os.PathLike[str]) -> None: + fullResourcePath = getResourcePath(resourceName) + if not os.path.exists(fullResourcePath): + raise FileNotFoundError(f"Resource '{resourceName}' not found.") + + shutil.copy(fullResourcePath, os.path.join(destinationFolder, resourceName)) + + +def move_folder(sourceFolder: str | os.PathLike[str], destinationFolder: str | os.PathLike[str]) -> None: + if not os.path.exists(sourceFolder): + raise FileNotFoundError(f"Source folder '{sourceFolder}' not found.") + + dest_path = os.path.join(destinationFolder, os.path.basename(sourceFolder)) + if os.path.exists(dest_path): + print("Path exists, removing it...") + shutil.rmtree(dest_path) + + shutil.move(sourceFolder, destinationFolder) + print(f"Successfully moved '{sourceFolder}' to '{destinationFolder}'.") + + +def main() -> None: + if not ctypes.windll.shell32.IsUserAnAdmin(): + ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1) + + destinationFolder = os.path.expandvars(r"%appdata%\Autodesk\ApplicationPlugins") + os.makedirs(destinationFolder, exist_ok=True) + with tempfile.TemporaryDirectory() as tempDir: + extractFile("SynthesisExporter.zip", tempDir) + with zipfile.ZipFile(os.path.join(tempDir, "SynthesisExporter.zip"), "r") as zip: + zip.extractall(tempDir) + + sourceFolder = os.path.join(tempDir, "synthesis.bundle") + move_folder(sourceFolder, destinationFolder) + + +if __name__ == "__main__": + main() diff --git a/installer/exporter/Windows/requirements.txt b/installer/exporter/Windows/requirements.txt new file mode 100644 index 0000000000..ef376ca83a --- /dev/null +++ b/installer/exporter/Windows/requirements.txt @@ -0,0 +1 @@ +pyinstaller diff --git a/installer/exporter/create.sh b/installer/exporter/create.sh deleted file mode 100755 index 22efb4ac4b..0000000000 --- a/installer/exporter/create.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -if test -d ./Synthesis; then - rm -rf ./Synthesis -fi - -cp -r ../../exporter/SynthesisFusionAddin/ ./Synthesis -echo "Copied over Synthesis exporter!" - -zip -r SynthesisExporter Synthesis/ install.sh -echo "Created zip installer!" diff --git a/installer/exporter/install.bat b/installer/exporter/install.bat deleted file mode 100644 index 293bac431a..0000000000 --- a/installer/exporter/install.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off - -if exist "%AppData%\Autodesk\Autodesk Fusion 360\API\AddIns\Synthesis\" ( - echo "Removing existing Synthesis exporter..." - rmdir "%AppData%\Autodesk\Autodesk Fusion 360\API\AddIns\Synthesis\" /Q/S -) - -echo "Copying to %AppData%\Autodesk\Autodesk Fusion 360\API\AddIns\Synthesis..." -xcopy Synthesis "%AppData%\Autodesk\Autodesk Fusion 360\API\AddIns\Synthesis\" /E -echo "Synthesis Exporter Successfully Installed!" diff --git a/installer/exporter/install.sh b/installer/exporter/install.sh deleted file mode 100755 index 4ab0801918..0000000000 --- a/installer/exporter/install.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -if test -d ~/Library/Application\ Support/Autodesk/Autodesk\ Fusion\ 360/API/AddIns/SynthesisFusionAddin; then - echo "Removing existing Synthesis exporter..." - rm -rf ~/Library/Application\ Support/Autodesk/Autodesk\ Fusion\ 360/API/AddIns/SynthesisFusionAddin -fi - -cp -r Synthesis/ ~/Library/Application\ Support/Autodesk/Autodesk\ Fusion\ 360/API/AddIns/SynthesisFusionAddin - -echo "Synthesis successfully copied!" diff --git a/installer/exporter/setup.bat b/installer/exporter/setup.bat deleted file mode 100644 index a6ec90bb68..0000000000 --- a/installer/exporter/setup.bat +++ /dev/null @@ -1,9 +0,0 @@ -@echo off - -if exist Synthesis\ ( - rmdir Synthesis /Q/S - echo Removed .\Synthesis -) - -xcopy ..\..\exporter\SynthesisFusionAddin Synthesis\ /E -echo Copied exporter into .\Synthesis diff --git a/installer/exporter/synthesis.bundle/PackageContents.xml b/installer/exporter/synthesis.bundle/PackageContents.xml new file mode 100644 index 0000000000..462ba5ec04 --- /dev/null +++ b/installer/exporter/synthesis.bundle/PackageContents.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/jolt b/jolt new file mode 160000 index 0000000000..37d20efc60 --- /dev/null +++ b/jolt @@ -0,0 +1 @@ +Subproject commit 37d20efc6075d0bfc360a67345ca44a2d5caeb8b diff --git a/simulation/.gitignore b/simulation/.gitignore new file mode 100644 index 0000000000..581290531f --- /dev/null +++ b/simulation/.gitignore @@ -0,0 +1,3 @@ +.classpath +.settings +.project diff --git a/simulation/SyntheSimJava/.classpath b/simulation/SyntheSimJava/.classpath new file mode 100644 index 0000000000..ea7f567adf --- /dev/null +++ b/simulation/SyntheSimJava/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/simulation/SyntheSimJava/.gitignore b/simulation/SyntheSimJava/.gitignore index 3b2f6ee393..c73e0b2d0f 100644 --- a/simulation/SyntheSimJava/.gitignore +++ b/simulation/SyntheSimJava/.gitignore @@ -3,4 +3,9 @@ .vscode # Ignore Gradle build output directory -build +build/ +ctre-sil/ +bin/ + +.settings/ +.classpath \ No newline at end of file diff --git a/simulation/SyntheSimJava/.project b/simulation/SyntheSimJava/.project new file mode 100644 index 0000000000..5bb13d53d2 --- /dev/null +++ b/simulation/SyntheSimJava/.project @@ -0,0 +1,34 @@ + + + SyntheSimJava + Project SyntheSimJava created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + + + 1721749622820 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/simulation/SyntheSimJava/README.md b/simulation/SyntheSimJava/README.md index 3e10a613a3..fff3e72efd 100644 --- a/simulation/SyntheSimJava/README.md +++ b/simulation/SyntheSimJava/README.md @@ -4,21 +4,24 @@ This is the SyntheSim Java utility library. FRC users can add this to their proj ## Current 3rd-Party Support -This is a list of the following 3rd-Party libraries that SyntheSim - Java improves, as well as the level of capability currently offered. +A list of the 3rd-Party libraries SyntheSimJava supports, including the features currently offered. ### REVRobotics -- [ ] CANSparkMax + +- [x] CANSparkMax - [x] Basic motor control - [x] Basic internal encoder data - - [ ] Motor following - - [ ] Full encoder support + - [x] Motor following + - [x] Full encoder support + +### CTRE Phoenix v6 -### CTRE Phoenix -- [ ] TalonFX - - [ ] Basic motor control - - [ ] Basic internal encoder data - - [ ] Motor following - - [ ] Full encoder support +- [x] TalonFX + - [x] Basic motor control + - [x] Full configuration support (via a [TalonFXConfigurator](src/main/com/autodesk/synthesis/ctre/TalonFXConfigurator.java) wrapper) + - [x] Basic internal encoder data + - [x] Motor following + - [x] Full encoder support ## Building @@ -27,15 +30,18 @@ To build the project, run the `build` task:
Example - Windows: - ```sh - $ gradlew.bat build - ``` +Windows: + +```sh +gradlew.bat build +``` + +MacOS/Linux: + +```sh +./gradlew build +``` - MacOS/Linux: - ```sh - $ ./gradlew build - ```
## Usage @@ -49,20 +55,23 @@ To publish the project locally, run the `publishToMavenLocal` task:
Example - Windows: - ```sh - $ gradlew.bat publishToMavenLocal - ``` +Windows: + +```sh +gradlew.bat publishToMavenLocal +``` + +MacOS/Linux: + +```sh +./gradlew publishToMavenLocal +``` - MacOS/Linux: - ```sh - $ ./gradlew publishToMavenLocal - ```
### Adding to project locally -In order to add the project locally, you must include the the `mavenLocal()` repository to your projects: +In order to add the project locally, you must include the `mavenLocal()` repository to your projects: ```groovy repositories { @@ -83,4 +92,6 @@ dependencies { ### Swapping Imports -SyntheSimJava creates alternative classes that wrap the original ones. Everything that we intercept is passed on to the original class, making it so these classes can (although not recommended) be used when running your robot code on original hardware. Be sure to switch over any and all CAN devices that this project supports in order to effectively simulate your code inside of Synthesis, or with any HALSim, WebSocket supported simulation/device. \ No newline at end of file +SyntheSimJava creates alternative classes that wrap the original ones. Everything that we intercept is passed on to the original class, making it so these classes can (although not recommended) be used when running your robot code on original hardware. Be sure to switch over any and all CAN devices that this project supports in order to effectively simulate your code inside of Synthesis, or with any HALSim, WebSocket supported simulation/device. + +The one exception to this is the `CANSparkMax.getAbsoluteEncoder()` method, which you must substitute for the `CANSparkMax.getAbsoluteEncoderSim()` method, because we were unable to override the original, because we are unable to wrap the return type `SparkAbsoluteEncoder`, because it's constructor is private. We instead created a class that implements all the same interfaces and has all the same methods, and thus works exactly the same, except it isn't an explicit subclass of `SparkAbsoluteEncoder`. The only difference to the API is the method name, everything else operates identically. diff --git a/simulation/SyntheSimJava/build.gradle b/simulation/SyntheSimJava/build.gradle index 421980fb3d..e8631a287e 100644 --- a/simulation/SyntheSimJava/build.gradle +++ b/simulation/SyntheSimJava/build.gradle @@ -30,11 +30,17 @@ repositories { maven { url "https://maven.revrobotics.com/" } + + // KAUAI + maven { + url "https://dev.studica.com/maven/release/2024/" + } } def WPI_Version = '2024.3.2' def REV_Version = '2024.2.4' def CTRE_Version = '24.3.0' +def KAUAI_Version = '2024.1.0' dependencies { // This dependency is exported to consumers, that is to say found on their compile classpath. @@ -53,6 +59,9 @@ dependencies { // CTRE implementation "com.ctre.phoenix6:wpiapi-java:$CTRE_Version" + + // KAUAI + implementation "com.kauailabs.navx.frc:navx-frc-java:$KAUAI_Version" } java { diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java index fa74dbbe0f..23b2a29bd2 100644 --- a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANEncoder.java @@ -2,6 +2,7 @@ import edu.wpi.first.hal.SimDevice; import edu.wpi.first.hal.SimDouble; +import edu.wpi.first.hal.SimBoolean; import edu.wpi.first.hal.SimDevice.Direction; /** @@ -14,6 +15,7 @@ public class CANEncoder { private SimDevice m_device; + private SimBoolean m_init; private SimDouble m_position; private SimDouble m_velocity; @@ -24,16 +26,19 @@ public class CANEncoder { * @param deviceId CAN Device ID. */ public CANEncoder(String name, int deviceId) { - m_device = SimDevice.create(name, deviceId); + m_device = SimDevice.create(String.format("%s:%s", "CANEncoder", name), deviceId); + m_init = m_device.createBoolean("init", Direction.kOutput, true); m_position = m_device.createDouble("position", Direction.kInput, 0.0); m_velocity = m_device.createDouble("velocity", Direction.kInput, 0.0); + + m_init.set(true); } /** * Gets the current position of the encoder, simulated. * - * @return Current Position. + * @return Current position in revolutions. */ public double getPosition() { return m_position.get(); @@ -42,7 +47,7 @@ public double getPosition() { /** * Gets the current velocity of the encoder, simulated. * - * @return Current Velocity. + * @return Current velocity in revolutions per second. */ public double getVelocity() { return m_velocity.get(); diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java index eeca38ed7b..f1d2f5c5cc 100644 --- a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/CANMotor.java @@ -6,15 +6,19 @@ import edu.wpi.first.hal.SimDouble; /** - * CANMotor class for easy implementation of documentation-compliant simulation data. + * CANMotor class for easy implementation of documentation-compliant simulation + * data. * - * See https://github.com/wpilibsuite/allwpilib/blob/6478ba6e3fa317ee041b8a41e562d925602b6ea4/simulation/halsim_ws_core/doc/hardware_ws_api.md + * See + * https://github.com/wpilibsuite/allwpilib/blob/6478ba6e3fa317ee041b8a41e562d925602b6ea4/simulation/halsim_ws_core/doc/hardware_ws_api.md * for documentation on the WebSocket API Specification. */ public class CANMotor { private SimDevice m_device; + private SimBoolean m_init; + private SimDouble m_percentOutput; private SimBoolean m_brakeMode; private SimDouble m_neutralDeadband; @@ -24,24 +28,34 @@ public class CANMotor { private SimDouble m_busVoltage; /** - * Creates a CANMotor sim device in accordance with the WebSocket API Specification. + * Creates a CANMotor sim device in accordance with the WebSocket API + * Specification. * - * @param name Name of the CAN Motor. This is generally the class name of the originating motor, prefixed with something (ie. "SYN CANSparkMax"). - * @param deviceId CAN Device ID. - * @param defaultPercentOutput Default PercentOutput value. [-1.0, 1.0] - * @param defaultBrakeMode Default BrakeMode value. (true/false) - * @param defaultNeutralDeadband Default Neutral Deadband value. This is used to determine when braking should be enabled. [0.0, 1.0] + * @param name Name of the CAN Motor. This is generally the + * class name of the originating motor, prefixed + * with something (ie. "SYN CANSparkMax"). + * @param deviceId CAN Device ID. + * @param defaultPercentOutput Default PercentOutput value. [-1.0, 1.0] + * @param defaultBrakeMode Default BrakeMode value. (true/false) + * @param defaultNeutralDeadband Default Neutral Deadband value. This is used to + * determine when braking should be enabled. [0.0, + * 1.0] */ - public CANMotor(String name, int deviceId, double defaultPercentOutput, boolean defaultBrakeMode, double defaultNeutralDeadband) { - m_device = SimDevice.create(name, deviceId); + public CANMotor(String name, int deviceId, double defaultPercentOutput, boolean defaultBrakeMode, + double defaultNeutralDeadband) { + m_device = SimDevice.create(String.format("%s:%s", "CANMotor", name), deviceId); + + m_init = m_device.createBoolean("init", Direction.kOutput, true); m_percentOutput = m_device.createDouble("percentOutput", Direction.kOutput, 0.0); m_brakeMode = m_device.createBoolean("brakeMode", Direction.kOutput, false); - m_neutralDeadband = m_device.createDouble("neutralDeadband", Direction.kOutput, deviceId); + m_neutralDeadband = m_device.createDouble("neutralDeadband", Direction.kOutput, 0.5); m_supplyCurrent = m_device.createDouble("supplyCurrent", Direction.kInput, 120.0); m_motorCurrent = m_device.createDouble("motorCurrent", Direction.kInput, 120.0); m_busVoltage = m_device.createDouble("busVoltage", Direction.kInput, 12.0); + m_busVoltage.set(0.0); // disable CANMotor inputs + m_init.set(true); } /** @@ -79,10 +93,11 @@ public void setNeutralDeadband(double deadband) { m_neutralDeadband.set(Math.min(1.0, Math.max(0.0, deadband))); } + /** * Get the supply current, simulated. * - * @return Supply Current. + * @return Supply current in amps. */ public double getSupplyCurrent() { return m_supplyCurrent.get(); @@ -91,7 +106,7 @@ public double getSupplyCurrent() { /** * Get the motor current, simulated. * - * @return Motor Current. + * @return Motor current in amps. */ public double getMotorCurrent() { return m_motorCurrent.get(); @@ -100,7 +115,7 @@ public double getMotorCurrent() { /** * Get the Bus Voltage, simulated. * - * @return Bus Voltage + * @return Bus voltage */ public double getBusVoltage() { return m_busVoltage.get(); diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/Gyro.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/Gyro.java new file mode 100644 index 0000000000..c17f1af4ab --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/Gyro.java @@ -0,0 +1,121 @@ +package com.autodesk.synthesis; + +import edu.wpi.first.hal.SimBoolean; +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDevice.Direction; +import edu.wpi.first.hal.SimDouble; + +/** + * Gyro class for easy implementation of documentation-compliant simulation data. + * + * See https://github.com/wpilibsuite/allwpilib/blob/6478ba6e3fa317ee041b8a41e562d925602b6ea4/simulation/halsim_ws_core/doc/hardware_ws_api.md + * for documentation on the WebSocket API Specification. + */ +public class Gyro { + + private SimDevice m_device; + + private SimDouble m_range; + private SimBoolean m_connected; + private SimDouble m_angleX; + private SimDouble m_angleY; + private SimDouble m_angleZ; + private SimDouble m_rateX; + private SimDouble m_rateY; + private SimDouble m_rateZ; + + /** + * Creates a CANMotor sim device in accordance with the WebSocket API Specification. + * + * @param name Name of the Gyro. This is generally the class name of the originating gyro (i.e. "ADXRS450"). + * @param deviceId ID of the Gyro. + */ + public Gyro(String name, int deviceId) { + m_device = SimDevice.create("Gyro:" + name, deviceId); + + m_range = m_device.createDouble("range", Direction.kOutput, 0.0); + m_connected = m_device.createBoolean("connected", Direction.kOutput, false); + m_angleX = m_device.createDouble("angle_x", Direction.kInput, 0.0); + m_angleY = m_device.createDouble("angle_y", Direction.kInput, 0.0); + m_angleZ = m_device.createDouble("angle_z", Direction.kInput, 0.0); + m_rateX = m_device.createDouble("rate_x", Direction.kInput, 0.0); + m_rateY = m_device.createDouble("rate_y", Direction.kInput, 0.0); + m_rateZ = m_device.createDouble("rate_z", Direction.kInput, 0.0); + } + + /** + * Set the range of the gyro. + * + * @param range Range of the gyro + */ + public void setRange(double range) { + if (Double.isNaN(range) || Double.isInfinite(range)) { + range = 0.0; + } + + m_range.set(range); + } + + /** + * Set whether the gyro is connected. + * + * @param connected Whether the gyro is connected + */ + public void setConnected(boolean connected) { + m_connected.set(connected); + } + + /** + * Get the angleX of the gyro. + * + * @return angleX + */ + public double getAngleX() { + return m_angleX.get(); + } + + /** + * Get the angleY of the gyro. + * + * @return angleY + */ + public double getAngleY() { + return m_angleY.get(); + } + + /** + * Get the angleZ of the gyro. + * + * @return angleZ + */ + public double getAngleZ() { + return m_angleZ.get(); + } + + /** + * Get the rateX of the gyro. + * + * @return rateX + */ + public double getRateX() { + return m_rateX.get(); + } + + /** + * Get the rateY of the gyro. + * + * @return rateY + */ + public double getRateY() { + return m_rateY.get(); + } + + /** + * Get the rateZ of the gyro. + * + * @return rateZ + */ + public double getRateZ() { + return m_rateZ.get(); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java index 63ed5b1e9c..3fdc46bf31 100644 --- a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFX.java @@ -1,13 +1,89 @@ package com.autodesk.synthesis.ctre; +import com.autodesk.synthesis.CANEncoder; import com.autodesk.synthesis.CANMotor; +import com.ctre.phoenix6.signals.NeutralModeValue; +import com.ctre.phoenix6.StatusSignal; +import com.ctre.phoenix6.configs.TalonFXConfigurator; +import com.ctre.phoenix6.hardware.DeviceIdentifier; public class TalonFX extends com.ctre.phoenix6.hardware.TalonFX { private CANMotor m_motor; + private CANEncoder m_encoder; - public TalonFX(int deviceId) { - super(deviceId); + /** + * Creates a new TalonFX, wrapped with simulation support. + * + * @param deviceNumber CAN Device ID. + */ + public TalonFX(int deviceNumber) { + super(deviceNumber); - m_motor = new CANMotor("SYN TalonFX", deviceId, 0.0, false, 0.3); + this.m_motor = new CANMotor("SYN TalonFX", deviceNumber, 0.0, false, 0.3); + this.m_encoder = new CANEncoder("SYN TalonFX", deviceNumber); + } + + /** + * Sets the torque of the real and simulated motors + * + * @param percentOutput The torque + */ + @Override + public void set(double percentOutput) { + super.set(percentOutput); + this.m_motor.setPercentOutput(percentOutput); + } + + /** + * Sets both the real and simulated motors to neutral mode + * + * @param mode The neutral mode value + * + */ + @Override + public void setNeutralMode(NeutralModeValue mode) { + super.setNeutralMode(mode); + + this.m_motor.setBrakeMode(mode == NeutralModeValue.Brake); + } + + /** + * Gets and internal configurator for both the simulated and real motors + * + * @return The internal configurator for this Talon motor + */ + @Override + public TalonFXConfigurator getConfigurator() { + DeviceIdentifier id = this.deviceIdentifier; + return new com.autodesk.synthesis.ctre.TalonFXConfigurator(id, this); + } + + // called internally by the configurator to set the deadband, not for user use + public void setNeutralDeadband(double deadband) { + this.m_motor.setNeutralDeadband(deadband); + } + + /** + * Gets the position of the simulated encoder + * + * @return The motor position in revolutions + */ + @Override + public StatusSignal getPosition() { + Double pos = this.m_encoder.getPosition(); + super.setPosition(pos); + return super.getPosition(); + } + + /** + * Gets the velocity of the simulated motor according to the simulated encoder + * + * @return The motor velocity in revolutions per second + */ + @Override + public StatusSignal getVelocity() { + Double velocity = this.m_encoder.getVelocity(); + super.set(velocity); + return super.getVelocity(); } } diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFXConfigurator.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFXConfigurator.java new file mode 100644 index 0000000000..11726331ea --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/ctre/TalonFXConfigurator.java @@ -0,0 +1,38 @@ +package com.autodesk.synthesis.ctre; + +import com.ctre.phoenix6.hardware.DeviceIdentifier; +import com.ctre.phoenix6.configs.TorqueCurrentConfigs; +import com.ctre.phoenix6.StatusCode; + +/** + * TalonFXConfigurator wrapper to add proper WPILib HALSim support. + */ +public class TalonFXConfigurator extends com.ctre.phoenix6.configs.TalonFXConfigurator { + private TalonFX devicePtr; + + /** + * Creates a new TalonFXConfigurator, wrapped with simulation support. + * + * @param id Device ID + * @param device The motor to configure + */ + public TalonFXConfigurator(DeviceIdentifier id, TalonFX device) { + super(id); + // awful, jank solution, please help + // if you know how to get a device from an id, let me know + this.devicePtr = device; + } + + /** + * Applies a torque configuration to a TalonFX motor and passes the new neutral deadband to the simulated motor in fission if applicable + * + * @param newTorqueCurrent The new torque configuration for this motor + */ + @Override + public StatusCode apply(TorqueCurrentConfigs newTorqueCurrent) { + StatusCode code = super.apply(newTorqueCurrent); + double newDeadband = newTorqueCurrent.TorqueNeutralDeadband; + this.devicePtr.setNeutralDeadband(newDeadband); + return code; + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/AnalogInput.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/AnalogInput.java new file mode 100644 index 0000000000..d093a510c4 --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/AnalogInput.java @@ -0,0 +1,90 @@ +package com.autodesk.synthesis.io; + +import edu.wpi.first.hal.SimBoolean; +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDouble; +import edu.wpi.first.hal.SimInt; +import edu.wpi.first.hal.SimDevice.Direction; + +public class AnalogInput extends edu.wpi.first.wpilibj.AnalogInput { + private SimDevice m_device; + + private SimBoolean m_init; + private SimInt m_avgBits; + private SimInt m_oversampleBits; + private SimDouble m_voltage; + private SimBoolean m_accumInit; + private SimInt m_accumValue; + private SimInt m_accumCount; + private SimInt m_accumCenter; + private SimInt m_accumDeadband; + + public AnalogInput(int channel) { + super(channel); + + m_device = SimDevice.create("AI:SYN AI", channel); + + m_init = m_device.createBoolean("init", Direction.kOutput, true); + m_avgBits = m_device.createInt("avg_bits", Direction.kOutput, 0); + m_oversampleBits = m_device.createInt("oversample_bits", Direction.kOutput, 0); + m_voltage = m_device.createDouble("voltage", Direction.kInput, 0.0); + m_accumInit = m_device.createBoolean("accum_init", Direction.kOutput, false); + m_accumValue = m_device.createInt("accum_value", Direction.kInput, 0); + m_accumCount = m_device.createInt("accum_count", Direction.kInput, 0); + m_accumCenter = m_device.createInt("accum_center", Direction.kOutput, 0); + m_accumDeadband = m_device.createInt("accum_deadband", Direction.kOutput, 0); + + this.setSimDevice(m_device); + } + + @Override + public double getVoltage() { + return m_voltage.get(); + } + + @Override + public int getAverageBits() { + return m_avgBits.get(); + } + + @Override + public void setAverageBits(int bits) { + m_avgBits.set(bits); + } + + @Override + public int getOversampleBits() { + return m_oversampleBits.get(); + } + + @Override + public void setOversampleBits(int bits) { + m_oversampleBits.set(bits); + } + + @Override + public void initAccumulator() { + super.initAccumulator(); + m_accumInit.set(true); + } + + @Override + public long getAccumulatorValue() { + return m_accumValue.get(); + } + + @Override + public long getAccumulatorCount() { + return m_accumCount.get(); + } + + @Override + public void setAccumulatorCenter(int center) { + m_accumCenter.set(center); + } + + @Override + public void setAccumulatorDeadband(int deadband) { + m_accumDeadband.set(deadband); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/AnalogOutput.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/AnalogOutput.java new file mode 100644 index 0000000000..46b66b05b9 --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/AnalogOutput.java @@ -0,0 +1,32 @@ +package com.autodesk.synthesis.io; + +import edu.wpi.first.hal.SimBoolean; +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDouble; +import edu.wpi.first.hal.SimDevice.Direction; + +public class AnalogOutput extends edu.wpi.first.wpilibj.AnalogOutput { + private SimDevice m_device; + + private SimBoolean m_init; + private SimDouble m_voltage; + + public AnalogOutput(int channel) { + super(channel); + + m_device = SimDevice.create("AI:SYN AO", channel); + + m_init = m_device.createBoolean("init", Direction.kOutput, true); + m_voltage = m_device.createDouble("voltage", Direction.kOutput, 0.0); + } + + @Override + public void setVoltage(double voltage) { + m_voltage.set(voltage); + } + + @Override + public double getVoltage() { + return m_voltage.get(); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/DigitalInput.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/DigitalInput.java new file mode 100644 index 0000000000..e4df20931f --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/DigitalInput.java @@ -0,0 +1,33 @@ +package com.autodesk.synthesis.io; + +import edu.wpi.first.hal.SimBoolean; +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDouble; +import edu.wpi.first.hal.SimDevice.Direction; + +public class DigitalInput extends edu.wpi.first.wpilibj.DigitalInput { + private SimDevice m_device; + + private SimBoolean m_init; + private SimBoolean m_input; + private SimBoolean m_value; + private SimDouble m_pulseLength; // unused but in HALSim spec + + public DigitalInput(int channel) { + super(channel); + + m_device = SimDevice.create("DIO:SYN DI", channel); + + m_init = m_device.createBoolean("init", Direction.kOutput, true); + m_input = m_device.createBoolean("input", Direction.kOutput, true); + m_value = m_device.createBoolean("value", Direction.kBidir, false); + m_pulseLength = m_device.createDouble("pulse_length", Direction.kOutput, 0.0); + + this.setSimDevice(m_device); + } + + @Override + public boolean get() { + return m_value.get(); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/DigitalOutput.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/DigitalOutput.java new file mode 100644 index 0000000000..ea76679485 --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/io/DigitalOutput.java @@ -0,0 +1,38 @@ +package com.autodesk.synthesis.io; + +import edu.wpi.first.hal.SimBoolean; +import edu.wpi.first.hal.SimDevice; +import edu.wpi.first.hal.SimDouble; +import edu.wpi.first.hal.SimDevice.Direction; + +public class DigitalOutput extends edu.wpi.first.wpilibj.DigitalOutput { + private SimDevice m_device; + + private SimBoolean m_init; + private SimBoolean m_input; + private SimBoolean m_value; + private SimDouble m_pulseLength; // unused but in HALSim spec + + public DigitalOutput(int channel) { + super(channel); + + m_device = SimDevice.create("DIO:SYN DO", channel); + + m_init = m_device.createBoolean("init", Direction.kOutput, true); + m_input = m_device.createBoolean("input", Direction.kOutput, true); + m_value = m_device.createBoolean("value", Direction.kBidir, false); + m_pulseLength = m_device.createDouble("pulse_length", Direction.kOutput, 0.0); + + this.setSimDevice(m_device); + } + + @Override + public boolean get() { + return m_value.get(); + } + + @Override + public void set(boolean value) { + m_value.set(value); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/kauailabs/AHRS.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/kauailabs/AHRS.java new file mode 100644 index 0000000000..a69daec358 --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/kauailabs/AHRS.java @@ -0,0 +1,38 @@ +package com.autodesk.synthesis.kauailabs; + +import com.autodesk.synthesis.Gyro; + +import edu.wpi.first.wpilibj.SPI; +import edu.wpi.first.wpilibj.I2C; +import edu.wpi.first.wpilibj.SerialPort; + +/** + * Outline for a NavX AHRS class. + * TODO + */ +public class AHRS extends com.kauailabs.navx.frc.AHRS { + private Gyro m_gyro; + + public AHRS() { + this(SPI.Port.kMXP); + } + + public AHRS(I2C.Port port) { + super(port); + init("I2C", port.value); + } + + public AHRS(SPI.Port port) { + super(port); + init("SPI", port.value); + } + + public AHRS(SerialPort.Port port) { + super(port); + init("SERIAL", port.value); + } + + private void init(String commType, int port) { + this.m_gyro = new Gyro("SYN AHRS " + commType, port); + } +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java index da2087e7ab..d696fabd26 100644 --- a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/CANSparkMax.java @@ -1,7 +1,10 @@ package com.autodesk.synthesis.revrobotics; +import java.util.ArrayList; + import com.autodesk.synthesis.CANEncoder; import com.autodesk.synthesis.CANMotor; +import com.revrobotics.CANSparkBase; import com.revrobotics.REVLibError; /** @@ -10,34 +13,105 @@ public class CANSparkMax extends com.revrobotics.CANSparkMax { private CANMotor m_motor; - private CANEncoder m_encoder; + public CANEncoder m_encoder; + private ArrayList followers; /** * Creates a new CANSparkMax, wrapped with simulation support. * - * @param deviceId CAN Device ID. - * @param motorType Motortype. For Simulation purposes, this is discarded at the moment. + * @param deviceId CAN Device ID. + * @param motorType Motor type. For Simulation purposes, this is discarded at the + * moment. + * + * See original documentation for more information https://codedocs.revrobotics.com/java/com/revrobotics/cansparkmax */ public CANSparkMax(int deviceId, MotorType motorType) { super(deviceId, motorType); - m_motor = new CANMotor("SYN CANSparkMax", deviceId, 0.0, false, 0.3); - m_encoder = new CANEncoder("SYN CANSparkMax/Encoder", deviceId); + this.m_motor = new CANMotor("SYN CANSparkMax", deviceId, 0.0, false, 0.3); + this.m_encoder = new CANEncoder("SYN CANSparkMax", deviceId); + this.followers = new ArrayList(); } + /** + * Sets the percent output of the real and simulated motors + * Setting a follower doesn't break the simulated follower - leader relationship, which it does for exclusively non-simulated motors + * + * @param percent The new percent output of the motor + * + * See the original documentation for more information + */ @Override public void set(double percent) { super.set(percent); - m_motor.setPercentOutput(percent); + this.m_motor.setPercentOutput(percent); + for (CANSparkMax follower : this.followers) { + follower.set(percent); + } } + /** + * Sets the neutralDeadband of the real and simulated motors + * + * @param n The new neutral deadband + */ + void setNeutralDeadband(double n) { + this.m_motor.setNeutralDeadband(n); + } + + /** + * Sets the real and simulated motors to an idle mode + * + * @param mode The specific idle mode (Brake, Coast) + * + * @return A library error indicating failure or success + */ @Override public REVLibError setIdleMode(com.revrobotics.CANSparkBase.IdleMode mode) { - if (mode != null) { - m_motor.setBrakeMode(mode.equals(com.revrobotics.CANSparkBase.IdleMode.kBrake)); - } + if (mode != null) + this.m_motor.setBrakeMode(mode.equals(com.revrobotics.CANSparkBase.IdleMode.kBrake)); return super.setIdleMode(mode); } + /** + * Gets a simulation-supported SparkAbsoluteEncoder containing the position and velocity of the motor in fission. + * All information returned by this class besides position and velocity is from the real motor. + * Use instead of getAbsoluteEncoder(), everything except for the name of the method works exactly the same. + + * @return The simulation-supported SparkAbsoluteEncoder. + */ + public com.autodesk.synthesis.revrobotics.SparkAbsoluteEncoder getAbsoluteEncoderSim() { + return new SparkAbsoluteEncoder(super.getAbsoluteEncoder(), this.m_encoder); + } + + public com.autodesk.synthesis.revrobotics.RelativeEncoder getEncoderSim() { + return new RelativeEncoder(super.getEncoder(), this.m_encoder); + } + + /** + * Adds a follower to this motor controller. + * + * @param f The new follower + */ + void newFollower(CANSparkMax f) { + this.followers.add(f); + } + + /** + * Causes a simulation-supported leader to follow another simulation-supported leader. + * The real versions of these motors will also follow each other. + * + * @param leader The motor for this robot to follow + * + * @return A library error indicating failure or success + */ + @Override + public REVLibError follow(CANSparkBase leader) { + REVLibError err = super.follow(leader); + if (leader instanceof CANSparkMax) { + ((CANSparkMax) leader).newFollower(this); + } + return err; + } } diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/RelativeEncoder.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/RelativeEncoder.java new file mode 100644 index 0000000000..a25dddaec3 --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/RelativeEncoder.java @@ -0,0 +1,98 @@ +package com.autodesk.synthesis.revrobotics; + +import com.autodesk.synthesis.CANEncoder; +import com.revrobotics.REVLibError; + +public class RelativeEncoder implements com.revrobotics.RelativeEncoder { + + private com.revrobotics.RelativeEncoder m_original; + private CANEncoder m_encoder; + private double m_zero = 0.0; + private double m_positionConversionFactor = 1.0; + private double m_velocityConversionFactor = 1.0; + private double m_invertedFactor = 1.0; + + public RelativeEncoder(com.revrobotics.RelativeEncoder original, CANEncoder encoder) { + m_original = original; + m_encoder = encoder; + + m_positionConversionFactor = m_original.getPositionConversionFactor(); + m_velocityConversionFactor = m_original.getVelocityConversionFactor(); + m_invertedFactor = m_original.getInverted() ? -1.0 : 1.0; + } + + @Override + public double getPosition() { + return m_encoder.getPosition() * m_positionConversionFactor * m_invertedFactor - m_zero; + } + + @Override + public double getVelocity() { + return m_encoder.getVelocity() * m_velocityConversionFactor * m_invertedFactor; + } + + @Override + public REVLibError setPosition(double position) { + m_zero = m_encoder.getPosition() * m_positionConversionFactor * m_invertedFactor - position; + return REVLibError.kOk; + } + + @Override + public REVLibError setPositionConversionFactor(double factor) { + m_positionConversionFactor = factor; + return REVLibError.kOk; + } + + @Override + public REVLibError setVelocityConversionFactor(double factor) { + m_velocityConversionFactor = factor; + return REVLibError.kOk; + } + + @Override + public double getPositionConversionFactor() { + return m_positionConversionFactor; + } + + @Override + public double getVelocityConversionFactor() { + return m_velocityConversionFactor; + } + + @Override + public REVLibError setAverageDepth(int depth) { + return m_original.setAverageDepth(depth); + } + + @Override + public int getAverageDepth() { + return m_original.getAverageDepth(); + } + + @Override + public REVLibError setMeasurementPeriod(int period_ms) { + return m_original.setMeasurementPeriod(period_ms); + } + + @Override + public int getMeasurementPeriod() { + return m_original.getMeasurementPeriod(); + } + + @Override + public int getCountsPerRevolution() { + return 1; + } + + @Override + public REVLibError setInverted(boolean inverted) { + m_invertedFactor = inverted ? -1.0 : 1.0; + return REVLibError.kOk; + } + + @Override + public boolean getInverted() { + return m_invertedFactor < 0.0; + } + +} diff --git a/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/SparkAbsoluteEncoder.java b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/SparkAbsoluteEncoder.java new file mode 100644 index 0000000000..402bdca0b9 --- /dev/null +++ b/simulation/SyntheSimJava/src/main/java/com/autodesk/synthesis/revrobotics/SparkAbsoluteEncoder.java @@ -0,0 +1,144 @@ +package com.autodesk.synthesis.revrobotics; + +import com.autodesk.synthesis.CANEncoder; + +import com.revrobotics.AbsoluteEncoder; +import com.revrobotics.REVLibError; + +/** + * SparkAbsoluteEncoder wrapper to add proper WPILib HALSim support. + */ +public class SparkAbsoluteEncoder implements AbsoluteEncoder { + private CANEncoder simEncoder; + private com.revrobotics.SparkAbsoluteEncoder realEncoder; + + /* + * A SparkAbsoluteEncoder class that returns the motors position and velocity from the simulated motor in fission, rather than the actual motor. + * All other parameters are returned from the real motor, which likely won't exist, not sure what it does then but we'll just call it UB. + */ + public SparkAbsoluteEncoder(com.revrobotics.SparkAbsoluteEncoder realEncoder, CANEncoder simEncoder) { + this.realEncoder = realEncoder; + this.simEncoder = simEncoder; + } + + /** + * Gets the average sampling depth for the real encoder + * + * @return The average sampling depth + */ + public int getAverageDepth() { + return this.realEncoder.getAverageDepth(); + } + + /** + * Gets the phase of the real encoder + * + * @return The phase of the real encoder + */ + public boolean getInverted() { + return this.realEncoder.getInverted(); + } + + /** + * Gets the position of the simulated motor. + * This returns the native units of 'rotations' by default, and can be changed by a scale factor using setPositionConversionFactor(). + * + * @return Number of rotations of the motor + */ + public double getPosition() { + return this.simEncoder.getPosition(); + } + + /** + * Sets the conversion factor for position of the real encoder. Multiplying by the native output units to give you position + * + * @return The conversion factor used by the encoder for position + */ + public double getPositionConversionFactor() { + return this.realEncoder.getPositionConversionFactor(); + } + + + /** + * Gets the velocity of the simulated motor. This returns the native units of 'rotations per second' by default, and can be changed by a scale factor using setVelocityConversionFactor(). + * + * @return Number of rotations per second of the motor + */ + public double getVelocity() { + return this.simEncoder.getVelocity() * this.realEncoder.getVelocityConversionFactor(); + } + + + /** + * Gets the conversion factor for velocity of the real encoder. + * + * @return The conversion factor used by the encoder for position + */ + public double getVelocityConversionFactor() { + return this.realEncoder.getVelocityConversionFactor(); + } + + /** + * Gets the zero offset in revolutions for the real encoder (the position that is reported as zero). + * + * @return The zero offset + */ + public double getZeroOffset() { + return this.realEncoder.getZeroOffset(); + } + + /** + * Sets the average sampling depth for the real encoder. + * + * @param depth The average sampling depth + * + * @return A library error indicating failure or success + */ + public REVLibError setAverageDepth(int depth) { + return this.realEncoder.setAverageDepth(depth); + } + + /** + * Sets the phase of the real encoder + * + * @param inverted Whether the real motor should be inverted + * + * @return A library error indicating failure or success + */ + public REVLibError setInverted(boolean inverted) { + return this.realEncoder.setInverted(inverted); + } + + /** + * Sets the conversion factor for position of the real encoder. + * + * @param factor The new position conversion factor + * + * @return A library error indicating failure or success + */ + public REVLibError setPositionConversionFactor(double factor) { + return this.realEncoder.setPositionConversionFactor(factor); + } + + /** + * Sets the conversion factor for velocity of the real encoder. + * + * @param factor The new velocity conversion factor + * + * @return A library error indicating failure or success + */ + public REVLibError setVelocityConversionFactor(double factor) { + return this.realEncoder.setVelocityConversionFactor(factor); + } + + /** + * Sets the zero offset of the real encoder (the position that is reported as zero). + * + * @param offset The new zero offset + * + * @return A library error indicating failure or success + */ + public REVLibError setZeroOffset(double offset) { + return this.realEncoder.setZeroOffset(offset); + } +} diff --git a/simulation/SyntheSimJava/vendordeps/NavX.json b/simulation/SyntheSimJava/vendordeps/NavX.json new file mode 100644 index 0000000000..e978a5f745 --- /dev/null +++ b/simulation/SyntheSimJava/vendordeps/NavX.json @@ -0,0 +1,40 @@ +{ + "fileName": "NavX.json", + "name": "NavX", + "version": "2024.1.0", + "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", + "frcYear": "2024", + "mavenUrls": [ + "https://dev.studica.com/maven/release/2024/" + ], + "jsonUrl": "https://dev.studica.com/releases/2024/NavX.json", + "javaDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-java", + "version": "2024.1.0" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-cpp", + "version": "2024.1.0", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": false, + "libName": "navx_frc", + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxraspbian", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/SyntheSimJava/vendordeps/Phoenix6.json b/simulation/SyntheSimJava/vendordeps/Phoenix6.json new file mode 100644 index 0000000000..032238505f --- /dev/null +++ b/simulation/SyntheSimJava/vendordeps/Phoenix6.json @@ -0,0 +1,339 @@ +{ + "fileName": "Phoenix6.json", + "name": "CTRE-Phoenix (v6)", + "version": "24.3.0", + "frcYear": 2024, + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "mavenUrls": [ + "https://maven.ctr-electronics.com/release/" + ], + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2024-latest.json", + "conflictsWith": [ + { + "uuid": "3fcf3402-e646-4fa6-971e-18afe8173b1a", + "errorMessage": "The combined Phoenix-6-And-5 vendordep is no longer supported. Please remove the vendordep and instead add both the latest Phoenix 6 vendordep and Phoenix 5 vendordep.", + "offlineFileName": "Phoenix6And5.json" + } + ], + "javaDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-java", + "version": "24.3.0" + } + ], + "jniDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ], + "cppDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-cpp", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPI", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "wpiapi-cpp-sim", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPISim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools_Sim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "libName": "CTRE_SimTalonSRX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "libName": "CTRE_SimVictorSPX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "libName": "CTRE_SimPigeonIMU", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "libName": "CTRE_SimCANCoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimProTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "libName": "CTRE_SimProCANcoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "libName": "CTRE_SimProPigeon2", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ] +} \ No newline at end of file diff --git a/simulation/SyntheSimJava/vendordeps/REVLib.json b/simulation/SyntheSimJava/vendordeps/REVLib.json new file mode 100644 index 0000000000..f85acd4054 --- /dev/null +++ b/simulation/SyntheSimJava/vendordeps/REVLib.json @@ -0,0 +1,74 @@ +{ + "fileName": "REVLib.json", + "name": "REVLib", + "version": "2024.2.4", + "frcYear": "2024", + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "mavenUrls": [ + "https://maven.revrobotics.com/" + ], + "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", + "javaDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-java", + "version": "2024.2.4" + } + ], + "jniDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-cpp", + "version": "2024.2.4", + "libName": "REVLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "libName": "REVLibDriver", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/samples/CppSample/.gitignore b/simulation/samples/CppSample/.gitignore index 11c9fdd738..22d0a533fe 100644 --- a/simulation/samples/CppSample/.gitignore +++ b/simulation/samples/CppSample/.gitignore @@ -1,4 +1,7 @@ .gradle/ .vscode/ .wpilib/ +.settings/ build/ +.settings/ + diff --git a/simulation/samples/CppSample/.project b/simulation/samples/CppSample/.project new file mode 100644 index 0000000000..1a9dd595bf --- /dev/null +++ b/simulation/samples/CppSample/.project @@ -0,0 +1,28 @@ + + + CppSample + Project CppSample created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + + + 1721749622817 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/simulation/samples/JavaAutoSample/.gitignore b/simulation/samples/JavaAutoSample/.gitignore new file mode 100644 index 0000000000..4a2a0df950 --- /dev/null +++ b/simulation/samples/JavaAutoSample/.gitignore @@ -0,0 +1,11 @@ +networktables.json +simgui-ds.json +simgui-window.json +simgui.json + +.gradle/ +.vscode/ +.wpilib/ +build/ +ctre_sim/ +bin/ diff --git a/simulation/samples/JavaAutoSample/WPILib-License.md b/simulation/samples/JavaAutoSample/WPILib-License.md new file mode 100644 index 0000000000..645e54253a --- /dev/null +++ b/simulation/samples/JavaAutoSample/WPILib-License.md @@ -0,0 +1,24 @@ +Copyright (c) 2009-2024 FIRST and other WPILib contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of FIRST, WPILib, nor the names of other WPILib + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/simulation/samples/JavaAutoSample/build.gradle b/simulation/samples/JavaAutoSample/build.gradle new file mode 100644 index 0000000000..5acbea004b --- /dev/null +++ b/simulation/samples/JavaAutoSample/build.gradle @@ -0,0 +1,115 @@ +plugins { + id "java" + id "edu.wpi.first.GradleRIO" version "2024.3.2" +} + +repositories { + mavenLocal() +} + +// wpi.maven.useLocal = false +// wpi.maven.useFrcMavenLocalDevelopment = true +// wpi.versions.wpilibVersion = '2024.424242.+' +// wpi.versions.wpimathVersion = '2024.424242.+' + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +def ROBOT_MAIN_CLASS = "frc.robot.Main" + +// Define my targets (RoboRIO) and artifacts (deployable files) +// This is added by GradleRIO's backing project DeployUtils. +deploy { + targets { + roborio(getTargetTypeClass('RoboRIO')) { + // Team number is loaded either from the .wpilib/wpilib_preferences.json + // or from command line. If not found an exception will be thrown. + // You can use getTeamOrDefault(team) instead of getTeamNumber if you + // want to store a team number in this file. + team = project.frc.getTeamOrDefault(997) + debug = project.frc.getDebugOrDefault(false) + + artifacts { + // First part is artifact name, 2nd is artifact type + // getTargetTypeClass is a shortcut to get the class type using a string + + frcJava(getArtifactTypeClass('FRCJavaArtifact')) { + } + + // Static files artifact + frcStaticFileDeploy(getArtifactTypeClass('FileTreeArtifact')) { + files = project.fileTree('src/main/deploy') + directory = '/home/lvuser/deploy' + } + } + } + } +} + +def deployArtifact = deploy.targets.roborio.artifacts.frcJava + +// Set to true to use debug for JNI. +wpi.java.debugJni = false + +// Set this to true to enable desktop support. +def includeDesktopSupport = true + +// Defining my dependencies. In this case, WPILib (+ friends), and vendor libraries. +// Also defines JUnit 5. +dependencies { + implementation wpi.java.deps.wpilib() + implementation wpi.java.vendor.java() + + roborioDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.roborio) + roborioDebug wpi.java.vendor.jniDebug(wpi.platforms.roborio) + + roborioRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.roborio) + roborioRelease wpi.java.vendor.jniRelease(wpi.platforms.roborio) + + nativeDebug wpi.java.deps.wpilibJniDebug(wpi.platforms.desktop) + nativeDebug wpi.java.vendor.jniDebug(wpi.platforms.desktop) + simulationDebug wpi.sim.enableDebug() + + nativeRelease wpi.java.deps.wpilibJniRelease(wpi.platforms.desktop) + nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) + simulationRelease wpi.sim.enableRelease() + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + implementation "com.autodesk.synthesis:SyntheSimJava:1.0.0" +} + +test { + useJUnitPlatform() + systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' +} + +// Simulation configuration (e.g. environment variables). +wpi.sim.addGui().defaultEnabled = true +wpi.sim.addDriverstation() + +wpi.sim.envVar("HALSIMWS_HOST", "127.0.0.1") +wpi.sim.addWebsocketsServer().defaultEnabled = true + +// Setting up my Jar File. In this case, adding all libraries into the main jar ('fat jar') +// in order to make them all available at runtime. Also adding the manifest so WPILib +// knows where to look for our Robot Class. +jar { + from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } + from sourceSets.main.allSource + manifest edu.wpi.first.gradlerio.GradleRIOPlugin.javaManifest(ROBOT_MAIN_CLASS) + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +// Configure jar and deploy tasks +deployArtifact.jarTask = jar +wpi.java.configureExecutableTasks(jar) +wpi.java.configureTestTasks(test) + +// Configure string concat to always inline compile +tasks.withType(JavaCompile) { + options.compilerArgs.add '-XDstringConcat=inline' +} diff --git a/simulation/samples/JavaAutoSample/gradle/wrapper/gradle-wrapper.jar b/simulation/samples/JavaAutoSample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..d64cd49177 Binary files /dev/null and b/simulation/samples/JavaAutoSample/gradle/wrapper/gradle-wrapper.jar differ diff --git a/simulation/samples/JavaAutoSample/gradle/wrapper/gradle-wrapper.properties b/simulation/samples/JavaAutoSample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..5e82d67b9f --- /dev/null +++ b/simulation/samples/JavaAutoSample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=permwrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=permwrapper/dists diff --git a/simulation/samples/JavaAutoSample/gradlew b/simulation/samples/JavaAutoSample/gradlew new file mode 100644 index 0000000000..1aa94a4269 --- /dev/null +++ b/simulation/samples/JavaAutoSample/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/simulation/samples/JavaAutoSample/gradlew.bat b/simulation/samples/JavaAutoSample/gradlew.bat new file mode 100644 index 0000000000..93e3f59f13 --- /dev/null +++ b/simulation/samples/JavaAutoSample/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/simulation/samples/JavaAutoSample/settings.gradle b/simulation/samples/JavaAutoSample/settings.gradle new file mode 100644 index 0000000000..d94f73c635 --- /dev/null +++ b/simulation/samples/JavaAutoSample/settings.gradle @@ -0,0 +1,30 @@ +import org.gradle.internal.os.OperatingSystem + +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + String frcYear = '2024' + File frcHome + if (OperatingSystem.current().isWindows()) { + String publicFolder = System.getenv('PUBLIC') + if (publicFolder == null) { + publicFolder = "C:\\Users\\Public" + } + def homeRoot = new File(publicFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } else { + def userFolder = System.getProperty("user.home") + def homeRoot = new File(userFolder, "wpilib") + frcHome = new File(homeRoot, frcYear) + } + def frcHomeMaven = new File(frcHome, 'maven') + maven { + name 'frcHome' + url frcHomeMaven + } + } +} + +Properties props = System.getProperties(); +props.setProperty("org.gradle.internal.native.headers.unresolved.dependencies.ignore", "true"); diff --git a/simulation/samples/JavaAutoSample/src/main/deploy/example.txt b/simulation/samples/JavaAutoSample/src/main/deploy/example.txt new file mode 100644 index 0000000000..bb82515dad --- /dev/null +++ b/simulation/samples/JavaAutoSample/src/main/deploy/example.txt @@ -0,0 +1,3 @@ +Files placed in this directory will be deployed to the RoboRIO into the +'deploy' directory in the home folder. Use the 'Filesystem.getDeployDirectory' wpilib function +to get a proper path relative to the deploy directory. \ No newline at end of file diff --git a/simulation/samples/JavaAutoSample/src/main/java/frc/robot/Main.java b/simulation/samples/JavaAutoSample/src/main/java/frc/robot/Main.java new file mode 100644 index 0000000000..8776e5dda7 --- /dev/null +++ b/simulation/samples/JavaAutoSample/src/main/java/frc/robot/Main.java @@ -0,0 +1,25 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot; + +import edu.wpi.first.wpilibj.RobotBase; + +/** + * Do NOT add any static variables to this class, or any initialization at all. Unless you know what + * you are doing, do not modify this file except to change the parameter class to the startRobot + * call. + */ +public final class Main { + private Main() {} + + /** + * Main initialization function. Do not perform any initialization here. + * + *

If you change your main robot class, change the parameter type. + */ + public static void main(String... args) { + RobotBase.startRobot(Robot::new); + } +} diff --git a/simulation/samples/JavaAutoSample/src/main/java/frc/robot/Robot.java b/simulation/samples/JavaAutoSample/src/main/java/frc/robot/Robot.java new file mode 100644 index 0000000000..9452a56343 --- /dev/null +++ b/simulation/samples/JavaAutoSample/src/main/java/frc/robot/Robot.java @@ -0,0 +1,172 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package frc.robot; + +import com.revrobotics.CANSparkLowLevel.MotorType; + +import com.autodesk.synthesis.io.*; + +import edu.wpi.first.wpilibj.SPI; + +import edu.wpi.first.wpilibj.ADXL362; +import edu.wpi.first.wpilibj.TimedRobot; +import edu.wpi.first.wpilibj.motorcontrol.Spark; +import edu.wpi.first.wpilibj.smartdashboard.SendableChooser; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import edu.wpi.first.wpilibj.XboxController; + +import com.autodesk.synthesis.revrobotics.CANSparkMax; +import com.autodesk.synthesis.revrobotics.RelativeEncoder; +import com.autodesk.synthesis.revrobotics.SparkAbsoluteEncoder; +import com.kauailabs.navx.frc.AHRS; +import com.autodesk.synthesis.CANEncoder; +import com.autodesk.synthesis.ctre.TalonFX; + +/** + * The VM is configured to automatically run this class, and to call the + * functions corresponding to + * each mode, as described in the TimedRobot documentation. If you change the + * name of this class or + * the package after creating this project, you must also update the + * build.gradle file in the + * project. + */ +public class Robot extends TimedRobot { + private static final String kDefaultAuto = "Default"; + private static final String kCustomAuto = "My Auto"; + private String m_autoSelected; + private final SendableChooser m_chooser = new SendableChooser<>(); + + private ADXL362 m_Accelerometer = new ADXL362(SPI.Port.kMXP, ADXL362.Range.k8G); + private AHRS m_Gyro = new AHRS(); + + private CANSparkMax m_sparkLeft = new CANSparkMax(1, MotorType.kBrushless); + private CANSparkMax m_sparkRight = new CANSparkMax(2, MotorType.kBrushless); + private CANSparkMax m_sparkArm = new CANSparkMax(3, MotorType.kBrushless); + private RelativeEncoder m_encoder; + + /** + * This function is run when the robot is first started up and should be used + * for any + * initialization code. + */ + @Override + public void robotInit() { + m_chooser.setDefaultOption("Default Auto", kDefaultAuto); + m_chooser.addOption("My Auto", kCustomAuto); + SmartDashboard.putData("Auto choices", m_chooser); + + m_encoder = m_sparkLeft.getEncoderSim(); + // 4 inch diameter wheels, default is 1 unit = 1 radian. + // Following conversion factor is 1 unit = 1 inch travelled. + m_encoder.setPositionConversionFactor(2.0); + } + + /** + * This function is called every 20 ms, no matter the mode. Use this for items + * like diagnostics + * that you want ran during disabled, autonomous, teleoperated and test. + * + *

+ * This runs after the mode specific periodic functions, but before LiveWindow + * and + * SmartDashboard integrated updating. + */ + @Override + public void robotPeriodic() { + } + + /** + * This autonomous (along with the chooser code above) shows how to select + * between different + * autonomous modes using the dashboard. The sendable chooser code works with + * the Java + * SmartDashboard. If you prefer the LabVIEW Dashboard, remove all of the + * chooser code and + * uncomment the getString line to get the auto name from the text box below the + * Gyro + * + *

+ * You can add additional auto modes by adding additional comparisons to the + * switch structure + * below with additional strings. If using the SendableChooser make sure to add + * them to the + * chooser code above as well. + */ + @Override + public void autonomousInit() { + m_autoSelected = m_chooser.getSelected(); + // m_autoSelected = SmartDashboard.getString("Auto Selector", kDefaultAuto); + System.out.println("Auto selected: " + m_autoSelected); + m_encoder.setPosition(0.0); + m_autoState = AutoState.Stage1; + } + + enum AutoState { + Stage1, Stage2 + } + private AutoState m_autoState = AutoState.Stage1; + + /** This function is called periodically during autonomous. */ + @Override + public void autonomousPeriodic() { + switch (m_autoState) { + case Stage1: + m_sparkLeft.set(0.5); + m_sparkRight.set(0.5); + if (m_encoder.getPosition() > 36.0) { + m_autoState = AutoState.Stage2; + System.out.println("--- Transitioning to Stage 2 ---"); + } + break; + case Stage2: + m_sparkLeft.set(0.5); + m_sparkRight.set(-0.5); + break; + default: + break; + } + } + + /** This function is called once when teleop is enabled. */ + @Override + public void teleopInit() { + } + + /** This function is called periodically during operator control. */ + @Override + public void teleopPeriodic() { + } + + /** This function is called once when the robot is disabled. */ + @Override + public void disabledInit() { + } + + /** This function is called periodically when disabled. */ + @Override + public void disabledPeriodic() { + } + + /** This function is called once when test mode is enabled. */ + @Override + public void testInit() { + } + + /** This function is called periodically during test mode. */ + @Override + public void testPeriodic() { + } + + /** This function is called once when the robot is first started up. */ + @Override + public void simulationInit() { + } + + /** This function is called periodically whilst in simulation. */ + @Override + public void simulationPeriodic() { + } +} diff --git a/simulation/samples/JavaAutoSample/vendordeps/NavX.json b/simulation/samples/JavaAutoSample/vendordeps/NavX.json new file mode 100644 index 0000000000..e978a5f745 --- /dev/null +++ b/simulation/samples/JavaAutoSample/vendordeps/NavX.json @@ -0,0 +1,40 @@ +{ + "fileName": "NavX.json", + "name": "NavX", + "version": "2024.1.0", + "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", + "frcYear": "2024", + "mavenUrls": [ + "https://dev.studica.com/maven/release/2024/" + ], + "jsonUrl": "https://dev.studica.com/releases/2024/NavX.json", + "javaDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-java", + "version": "2024.1.0" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-cpp", + "version": "2024.1.0", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": false, + "libName": "navx_frc", + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxraspbian", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/samples/JavaAutoSample/vendordeps/Phoenix6.json b/simulation/samples/JavaAutoSample/vendordeps/Phoenix6.json new file mode 100644 index 0000000000..032238505f --- /dev/null +++ b/simulation/samples/JavaAutoSample/vendordeps/Phoenix6.json @@ -0,0 +1,339 @@ +{ + "fileName": "Phoenix6.json", + "name": "CTRE-Phoenix (v6)", + "version": "24.3.0", + "frcYear": 2024, + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "mavenUrls": [ + "https://maven.ctr-electronics.com/release/" + ], + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2024-latest.json", + "conflictsWith": [ + { + "uuid": "3fcf3402-e646-4fa6-971e-18afe8173b1a", + "errorMessage": "The combined Phoenix-6-And-5 vendordep is no longer supported. Please remove the vendordep and instead add both the latest Phoenix 6 vendordep and Phoenix 5 vendordep.", + "offlineFileName": "Phoenix6And5.json" + } + ], + "javaDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-java", + "version": "24.3.0" + } + ], + "jniDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ], + "cppDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-cpp", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPI", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "wpiapi-cpp-sim", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPISim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools_Sim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "libName": "CTRE_SimTalonSRX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "libName": "CTRE_SimVictorSPX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "libName": "CTRE_SimPigeonIMU", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "libName": "CTRE_SimCANCoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimProTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "libName": "CTRE_SimProCANcoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "libName": "CTRE_SimProPigeon2", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ] +} \ No newline at end of file diff --git a/simulation/samples/JavaAutoSample/vendordeps/REVLib.json b/simulation/samples/JavaAutoSample/vendordeps/REVLib.json new file mode 100644 index 0000000000..f85acd4054 --- /dev/null +++ b/simulation/samples/JavaAutoSample/vendordeps/REVLib.json @@ -0,0 +1,74 @@ +{ + "fileName": "REVLib.json", + "name": "REVLib", + "version": "2024.2.4", + "frcYear": "2024", + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "mavenUrls": [ + "https://maven.revrobotics.com/" + ], + "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", + "javaDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-java", + "version": "2024.2.4" + } + ], + "jniDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-cpp", + "version": "2024.2.4", + "libName": "REVLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "libName": "REVLibDriver", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/samples/JavaSample/.classpath b/simulation/samples/JavaSample/.classpath new file mode 100644 index 0000000000..ea7f567adf --- /dev/null +++ b/simulation/samples/JavaSample/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/simulation/samples/JavaSample/.gitignore b/simulation/samples/JavaSample/.gitignore index 0a7cba8930..ecfc320587 100644 --- a/simulation/samples/JavaSample/.gitignore +++ b/simulation/samples/JavaSample/.gitignore @@ -1,8 +1,12 @@ -*.json +networktables.json +simgui-ds.json +simgui-window.json +simgui.json .gradle/ .vscode/ .wpilib/ +.settings/ build/ ctre_sim/ bin/ diff --git a/simulation/samples/JavaSample/.project b/simulation/samples/JavaSample/.project new file mode 100644 index 0000000000..1ed23b11c9 --- /dev/null +++ b/simulation/samples/JavaSample/.project @@ -0,0 +1,34 @@ + + + JavaSample + Project JavaSample created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + + + 1721749622819 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/simulation/samples/JavaSample/src/main/java/frc/robot/Robot.java b/simulation/samples/JavaSample/src/main/java/frc/robot/Robot.java index 8a9aa5024b..f1b05386fc 100644 --- a/simulation/samples/JavaSample/src/main/java/frc/robot/Robot.java +++ b/simulation/samples/JavaSample/src/main/java/frc/robot/Robot.java @@ -4,22 +4,30 @@ package frc.robot; -import com.ctre.phoenix6.hardware.TalonFX; -import com.revrobotics.CANSparkBase.IdleMode; -// import com.revrobotics.CANSparkMax; import com.revrobotics.CANSparkLowLevel.MotorType; +import com.autodesk.synthesis.io.*; + +import edu.wpi.first.wpilibj.SPI; + +import edu.wpi.first.wpilibj.ADXL362; import edu.wpi.first.wpilibj.TimedRobot; import edu.wpi.first.wpilibj.motorcontrol.Spark; import edu.wpi.first.wpilibj.smartdashboard.SendableChooser; import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import edu.wpi.first.wpilibj.XboxController; import com.autodesk.synthesis.revrobotics.CANSparkMax; +import com.kauailabs.navx.frc.AHRS; +import com.autodesk.synthesis.ctre.TalonFX; /** - * The VM is configured to automatically run this class, and to call the functions corresponding to - * each mode, as described in the TimedRobot documentation. If you change the name of this class or - * the package after creating this project, you must also update the build.gradle file in the + * The VM is configured to automatically run this class, and to call the + * functions corresponding to + * each mode, as described in the TimedRobot documentation. If you change the + * name of this class or + * the package after creating this project, you must also update the + * build.gradle file in the * project. */ public class Robot extends TimedRobot { @@ -28,9 +36,25 @@ public class Robot extends TimedRobot { private String m_autoSelected; private final SendableChooser m_chooser = new SendableChooser<>(); - private Spark m_Spark = new Spark(0); - private CANSparkMax m_SparkMax = new CANSparkMax(1, MotorType.kBrushless); - private TalonFX m_Talon = new TalonFX(2); + private Spark m_Spark1 = new Spark(0); + private Spark m_Spark2 = new Spark(1); + private TalonFX m_Talon = new TalonFX(7); + private XboxController m_Controller = new XboxController(0); + + private ADXL362 m_Accelerometer = new ADXL362(SPI.Port.kMXP, ADXL362.Range.k8G); + private AHRS m_Gyro = new AHRS(); + + private DigitalInput m_DI = new DigitalInput(0); + private DigitalOutput m_DO = new DigitalOutput(1); + private AnalogInput m_AI = new AnalogInput(0); + private AnalogOutput m_AO = new AnalogOutput(1); + + private CANSparkMax m_SparkMax1 = new CANSparkMax(1, MotorType.kBrushless); + private CANSparkMax m_SparkMax2 = new CANSparkMax(2, MotorType.kBrushless); + private CANSparkMax m_SparkMax3 = new CANSparkMax(3, MotorType.kBrushless); + private CANSparkMax m_SparkMax4 = new CANSparkMax(4, MotorType.kBrushless); + private CANSparkMax m_SparkMax5 = new CANSparkMax(5, MotorType.kBrushless); + private CANSparkMax m_SparkMax6 = new CANSparkMax(6, MotorType.kBrushless); /** * This function is run when the robot is first started up and should be used for any @@ -68,15 +92,38 @@ public void autonomousInit() { m_autoSelected = m_chooser.getSelected(); // m_autoSelected = SmartDashboard.getString("Auto Selector", kDefaultAuto); System.out.println("Auto selected: " + m_autoSelected); + m_DO.set(true); + m_AO.setVoltage(0.0); } /** This function is called periodically during autonomous. */ @Override public void autonomousPeriodic() { - m_Spark.set(0.5); - m_SparkMax.set(1.0); - m_Talon.set(-1.0); + m_SparkMax1.set(0.2); + m_SparkMax2.set(-0.2); + + // m_Spark1.set(0.5); + // m_Spark2.set(-0.5); + // m_Talon.set(-1.0); + + // double position = m_SparkMax1.getAbsoluteEncoderSim().getPosition(); + + // if (position >= 20) { + // m_SparkMax1.set(0.0); + // m_SparkMax2.set(0.0); + // m_SparkMax3.set(0.0); + // m_SparkMax4.set(0.0); + // m_SparkMax5.set(0.0); + // m_SparkMax6.set(0.0); + // } else { + // m_SparkMax1.set(1.0); + // m_SparkMax2.set(1.0); + // m_SparkMax3.set(1.0); + // m_SparkMax4.set(1.0); + // m_SparkMax5.set(1.0); + // m_SparkMax6.set(1.0); + // } switch (m_autoSelected) { case kCustomAuto: @@ -91,41 +138,80 @@ public void autonomousPeriodic() { /** This function is called once when teleop is enabled. */ @Override - public void teleopInit() {} + public void teleopInit() { + m_DO.set(false); + m_AO.setVoltage(6.0); + } + + private double clamp(double a, double min, double max) { + return Math.min(Math.max(a, min), max); + } /** This function is called periodically during operator control. */ @Override public void teleopPeriodic() { - m_Spark.set(0.25); - m_SparkMax.set(0.75); - m_Talon.set(-0.5); + double forward = -m_Controller.getLeftY(); + double turn = m_Controller.getRightX(); + if (Math.abs(forward) < 0.2) { + forward = 0.0; + } + if (Math.abs(turn) < 0.2) { + turn = 0.0; + } + + m_SparkMax1.set(clamp(forward + turn, -1, 1)); + m_SparkMax2.set(clamp(forward - turn, -1, 1)); + + m_Talon.set(m_Controller.getLeftX()); + System.out.println("LeftX: " + m_Controller.getLeftX()); + // System.out.println("OUT: " + m_DO.get()); + // System.out.println("AI: " + m_AI.getVoltage()); + // m_Talon.set(-0.5); + // m_SparkMax1.set(-0.75); + // m_SparkMax2.set(-0.75); + m_SparkMax3.set(-0.75); + m_SparkMax4.set(-0.75); + m_SparkMax5.set(-0.75); + m_SparkMax6.set(-0.75); + + m_SparkMax1.getEncoder().setPosition(0.0); } + /** This function is called once when the robot is disabled. */ @Override public void disabledInit() { - m_Spark.set(0.0); - m_SparkMax.set(0.0); - m_Talon.set(0.0); + m_SparkMax1.set(0.0); + m_SparkMax2.set(0.0); + m_SparkMax3.set(0.0); + m_SparkMax4.set(0.0); + m_SparkMax5.set(0.0); + m_SparkMax6.set(0.0); + m_AO.setVoltage(12.0); } /** This function is called periodically when disabled. */ @Override - public void disabledPeriodic() {} + public void disabledPeriodic() { + } /** This function is called once when test mode is enabled. */ @Override - public void testInit() {} + public void testInit() { + } /** This function is called periodically during test mode. */ @Override - public void testPeriodic() {} + public void testPeriodic() { + } /** This function is called once when the robot is first started up. */ @Override - public void simulationInit() {} + public void simulationInit() { + } /** This function is called periodically whilst in simulation. */ @Override - public void simulationPeriodic() {} + public void simulationPeriodic() { + } } diff --git a/simulation/samples/JavaSample/vendordeps/NavX.json b/simulation/samples/JavaSample/vendordeps/NavX.json new file mode 100644 index 0000000000..e978a5f745 --- /dev/null +++ b/simulation/samples/JavaSample/vendordeps/NavX.json @@ -0,0 +1,40 @@ +{ + "fileName": "NavX.json", + "name": "NavX", + "version": "2024.1.0", + "uuid": "cb311d09-36e9-4143-a032-55bb2b94443b", + "frcYear": "2024", + "mavenUrls": [ + "https://dev.studica.com/maven/release/2024/" + ], + "jsonUrl": "https://dev.studica.com/releases/2024/NavX.json", + "javaDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-java", + "version": "2024.1.0" + } + ], + "jniDependencies": [], + "cppDependencies": [ + { + "groupId": "com.kauailabs.navx.frc", + "artifactId": "navx-frc-cpp", + "version": "2024.1.0", + "headerClassifier": "headers", + "sourcesClassifier": "sources", + "sharedLibrary": false, + "libName": "navx_frc", + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "linuxathena", + "linuxraspbian", + "linuxarm32", + "linuxarm64", + "linuxx86-64", + "osxuniversal", + "windowsx86-64" + ] + } + ] +} \ No newline at end of file diff --git a/simulation/samples/JavaSample/vendordeps/Phoenix6.json b/simulation/samples/JavaSample/vendordeps/Phoenix6.json new file mode 100644 index 0000000000..032238505f --- /dev/null +++ b/simulation/samples/JavaSample/vendordeps/Phoenix6.json @@ -0,0 +1,339 @@ +{ + "fileName": "Phoenix6.json", + "name": "CTRE-Phoenix (v6)", + "version": "24.3.0", + "frcYear": 2024, + "uuid": "e995de00-2c64-4df5-8831-c1441420ff19", + "mavenUrls": [ + "https://maven.ctr-electronics.com/release/" + ], + "jsonUrl": "https://maven.ctr-electronics.com/release/com/ctre/phoenix6/latest/Phoenix6-frc2024-latest.json", + "conflictsWith": [ + { + "uuid": "3fcf3402-e646-4fa6-971e-18afe8173b1a", + "errorMessage": "The combined Phoenix-6-And-5 vendordep is no longer supported. Please remove the vendordep and instead add both the latest Phoenix 6 vendordep and Phoenix 5 vendordep.", + "offlineFileName": "Phoenix6And5.json" + } + ], + "javaDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-java", + "version": "24.3.0" + } + ], + "jniDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "isJar": false, + "skipInvalidPlatforms": true, + "validPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ], + "cppDependencies": [ + { + "groupId": "com.ctre.phoenix6", + "artifactId": "wpiapi-cpp", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPI", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6", + "artifactId": "tools", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "linuxathena" + ], + "simMode": "hwsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "wpiapi-cpp-sim", + "version": "24.3.0", + "libName": "CTRE_Phoenix6_WPISim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "tools-sim", + "version": "24.3.0", + "libName": "CTRE_PhoenixTools_Sim", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonSRX", + "version": "24.3.0", + "libName": "CTRE_SimTalonSRX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simVictorSPX", + "version": "24.3.0", + "libName": "CTRE_SimVictorSPX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simPigeonIMU", + "version": "24.3.0", + "libName": "CTRE_SimPigeonIMU", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simCANCoder", + "version": "24.3.0", + "libName": "CTRE_SimCANCoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProTalonFX", + "version": "24.3.0", + "libName": "CTRE_SimProTalonFX", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProCANcoder", + "version": "24.3.0", + "libName": "CTRE_SimProCANcoder", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + }, + { + "groupId": "com.ctre.phoenix6.sim", + "artifactId": "simProPigeon2", + "version": "24.3.0", + "libName": "CTRE_SimProPigeon2", + "headerClassifier": "headers", + "sharedLibrary": true, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "linuxx86-64", + "osxuniversal" + ], + "simMode": "swsim" + } + ] +} \ No newline at end of file diff --git a/simulation/samples/JavaSample/vendordeps/REVLib.json b/simulation/samples/JavaSample/vendordeps/REVLib.json new file mode 100644 index 0000000000..f85acd4054 --- /dev/null +++ b/simulation/samples/JavaSample/vendordeps/REVLib.json @@ -0,0 +1,74 @@ +{ + "fileName": "REVLib.json", + "name": "REVLib", + "version": "2024.2.4", + "frcYear": "2024", + "uuid": "3f48eb8c-50fe-43a6-9cb7-44c86353c4cb", + "mavenUrls": [ + "https://maven.revrobotics.com/" + ], + "jsonUrl": "https://software-metadata.revrobotics.com/REVLib-2024.json", + "javaDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-java", + "version": "2024.2.4" + } + ], + "jniDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "skipInvalidPlatforms": true, + "isJar": false, + "validPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ], + "cppDependencies": [ + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-cpp", + "version": "2024.2.4", + "libName": "REVLib", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + }, + { + "groupId": "com.revrobotics.frc", + "artifactId": "REVLib-driver", + "version": "2024.2.4", + "libName": "REVLibDriver", + "headerClassifier": "headers", + "sharedLibrary": false, + "skipInvalidPlatforms": true, + "binaryPlatforms": [ + "windowsx86-64", + "windowsx86", + "linuxarm64", + "linuxx86-64", + "linuxathena", + "linuxarm32", + "osxuniversal" + ] + } + ] +} \ No newline at end of file diff --git a/tutorials/APS.md b/tutorials/APS.md new file mode 100644 index 0000000000..c4a9608a32 --- /dev/null +++ b/tutorials/APS.md @@ -0,0 +1,59 @@ +author: Synthesis Team +summary: How to use Autodesk Platform Services to store and load Synthesis assemblies. +id: APSCodelab +tags: Autodesk, APS, Autodesk Platform Services, Fusion +categories: Services +environments: Synthesis +status: Draft +feedback link: https://github.com/Autodesk/synthesis/issues + +# Autodesk Platform Services + +## APS (Fusion) + +### Login + +In order to make use of the Autodesk Platform Services (APS) integration, you first have to log in. This can be done with the middle toolbar button. + +![aps toolbar button](img/aps/aps-button-fusion.png) + +![aps login](img/aps/aps-login-fusion.png) + +Once logged in, you'll be able to see your name in the main export panel. + +![aps info](img/aps/aps-info-fusion.png) + +### Export Option + +To store an export in your current project, make sure the "Export Location" option is set to "Upload". + +![upload location option](img/aps/aps-location-fusion.png) + +## APS (Synthesis) + +### Login + +To log into your Autodesk account, open the left menu and click on "APS Login". + +![left menu in synthesis](img/aps/aps-left-menu-fission.png) + +![login page](img/aps/aps-login-fission.png) + +Upon successful login, you should see a notification in the bottom-right, as well as a greeting and your profile picture replacing the "APS Login" button. + +![aps login notification](img/aps/aps-notif-fission.png) + +![aps profile](img/aps/aps-profile-fission.png) + +### Importing + +Once logged in, when you open the "Spawn Asset" panel, the remote assets section will begin to search your projects and show any and all Synthesis assemblies available for importing. + +![spawn asset panel](img/aps/import-fission.png) + +After you've imported them for the first time, they will be saved for later use and won't require APS login to access so long as they stay saved in your browser. + +## Need More Help? + +If you need help with anything regarding Synthesis or it's related features please reach out through our +[discord sever](https://www.discord.gg/hHcF9AVgZA). It's the best way to get in contact with the community and our current developers. diff --git a/tutorials/CodeSimulation.md b/tutorials/CodeSimulation.md index 9686fdf622..61bd61c163 100644 --- a/tutorials/CodeSimulation.md +++ b/tutorials/CodeSimulation.md @@ -9,43 +9,135 @@ feedback link: https://github.com/Autodesk/synthesis/issues # Code Simulation in Synthesis -## Setup - -### Setup (Project Side) +## Setup (Robot Code Side) The Synthesis simulator comes with code simulation already integrated. However, a development environment for what ever code your are trying to simulate will be required. -Synthesis' code simulation relies on the WPILib HALSim extensions, specifically the websocket-client extension. To enable this for your project, add the following lines to -your `build.gradle` file. +Synthesis' code simulation relies on the WPILib HALSim extensions, specifically the websocket-client extension. You'll need to make the following changes to your `build.gradle` in order to properly simulate your code in Synthesis. + +### 1. Desktop Support + +You'll need to enable desktop support for your project in order to run the HALSim: + +```java +def includeDesktopSupport = true +``` + +### 2. Websocket Server Extension + +In order to communicate with your browser, you'll need to enable the websocket server extension with the following: ```java wpi.sim.envVar("HALSIMWS_HOST", "127.0.0.1") -wpi.sim.addWebsocketsClient().defaultEnabled = true +wpi.sim.addWebsocketsServer().defaultEnabled = true +``` + +### 3. SyntheSim (Optional) + +For CAN-based device support (TalonFX, CANSparkMax, most Gyros), you'll need our own library--SyntheSim. Currently only available for Java, SyntheSim adds additional support for third party devices that don't follow WPILib's web socket specification. It's still in early development, so you'll need to clone and install the library locally in order to use it: + +```sh +$ git clone https://github.com/Autodesk/synthesis.git +$ cd synthesis/simulation/SyntheSimJava +$ ./gradlew build && ./gradlew publishToMavenLocal +``` + +Next, you'll need to have the local maven repository is added to your project by making sure the following is included in your `build.gradle` file: + +```java +repositories { + mavenLocal() + ... +} +``` + +Finally, you can add the SyntheSim dependency to your `build.gradle`: + +```java +dependencies { + ... + implementation "com.autodesk.synthesis:SyntheSimJava:1.0.0" + ... +} +``` + +All of these instructions can be found in the [SyntheSim README](https://github.com/Autodesk/synthesis/blob/prod/simulation/SyntheSimJava/README.md). + +SyntheSim is very much a work in progress. If there is a particular device that isn't compatible, feel free to head to our [GitHub](https://github.com/Autodesk/synthesis) to see about contributing. + +### 4. HALSim GUI + +This should be added by default, but in case it isn't, add this to your `build.gradle` to enable the SimGUI extension by default. + +```java +wpi.sim.addGui().defaultEnabled = true +``` + +This will allow you to change the state of the robot, as well as hook up any joysticks you'd like to use during teleop. You must use this GUI in order +to bring your robot out of disconnected mode, otherwise we won't be able to change the state of your robot from within the app. + +### 5. Start your code + +To start your robot code, you can use the following simulate commands with gradle: + +```bash +$ ./gradlew simulateJava +``` + +or for C++: + +```bash +$ ./gradlew simulateNative ``` -NOTE: The GUI extension interfaces really well and add Joystick/Controller support to your code, whereas Synthesis currently only supports controllers for non-code simulation. -If you wish to test using your controllers, I recommend using the GUI extension in conjunction. +WPILib also has a command from within VSCode you can use the start your robot code: + +![image_caption](img/code-sim/wpilib-ext-simulate.png) + +## Setup (Synthesis Web-app Side) + +Once started, make sure in the SimGUI that your robot state is set to "Disabled", **not** "Disconnected". + +### Spawning in a Robot + +Open up [Fission](https://synthesis.autodesk.com/fission/) and spawn in a robot. Once spawned in, place it down and open the config panel. This can be +done by using the left-hand menu and navigating to your robot in the config panel, or by right-clicking on your robot and selecting the "Configure" option. + +Next, switch the brain currently controlling the robot. In order to give the simulation control over the robot, the brain must be switched from "Synthesis" +to "WPILib". At the moment, only one robot can be controlled by the simulation at a time. + +In the top-right, there should be a connection status indicator. If your robot program was running prior to switching to the "WPILib" brain, it should connect +quickly. + +### Simulation Configuration + +Under your robot in the config panel, there should be a Simulation option now. Here you can find all the settings for the code simulation. + +![image_caption](img/code-sim/config-panel-simulation.png) + +#### Auto Reconnect + +You can enabled auto reconnect incase you are having issues with this. In order for it to take affect, you have to enable the setting, then switch back to the "Synthesis" +brain and then back again to the "WPILib" brain. This setting will be saved. + +#### Wiring Panel -### Setup (Robot Side) +This panel can be used to "wire up" your robot. It will show you all the inputs and outputs available from both the simulation and robot. The handles (little circles with +labels) are colored to indicate the type of data they represent. Hover over the information icons for more information. -Inside of Synthesis, you must configure your "IO map" so we know what signals go where. You can access the configuration modal on the side bar. -Once inside the configuration modal, you can add the devices you want to add support for. -
-NOTE: Currently, due to the way the websocket extension works, no CAN devices are supported. We plan on bringing CAN support for the 2023 Summer release or potentially earlier. -At the moment, PWM and Quadrature Encoders (encoders that use 2 DIO ports each) are supported, with more device types and sensors on the way. +![image_caption](img/code-sim/wiring-panel.png) -## Usage +The bottom-left controls can be used to zoom in/out, fit your view to the nodes, and add junction nodes for connection many connections to many connections. -### Synthesis +#### Auto Testing -Inside of Synthesis, open up the DriverStation. Once connected, you can use it like normal. Our DriverStation is currently limited in its features, so if you need anything beyond -enabling and choosing between Telop and Auto, I recommend using the GUI extension for more functionality. +The Auto Testing panel allows for iterative testing of an autonomous routine. I'd recommend making sure that the auto reconnect option is enabled. -### Running the code Simulation +![image_caption](img/code-sim/auto-testing.png) -I recommend using VSCode and the WPILib Suite extension for running the code simulation. Alternatively you can use this command: -`gradlew.bat simulationJava` or `gradlew.bat simulate` +You can specify a max time, alliance station, and game data. Once you've decided on those and have place the robot where you want, you can start your auto routine. +After the specified amount of time, or when the stop button is pressed, the simulation will freeze and you can either reset to where you started, or close the panel. ## Need More Help? If you need help with anything regarding Synthesis or it's related features please reach out through our -[discord sever](https://www.discord.gg/hHcF9AVgZA). It's the best way to get in contact with the community and our current developers. +[discord server](https://www.discord.gg/hHcF9AVgZA). It's the best way to get in contact with the community and our current developers. diff --git a/tutorials/ConfigMode.md b/tutorials/ConfigMode.md deleted file mode 100644 index 1bd43d53a4..0000000000 --- a/tutorials/ConfigMode.md +++ /dev/null @@ -1,65 +0,0 @@ -author: Synthesis Team -summary: Tutorial for using Config Mode -id: ConfigModeCodelab -tags: Config, Robot Config, Game Simulation -categories: Modes -environments: Synthesis -status: Draft -feedback link: https://github.com/Autodesk/synthesis/issues - -# Robot Config Mode in Synthesis - -## Intro - -Configure Mode allows you to change and fine tune aspects of your robot to better test and simulate its behavior on the field. Configure Mode is available in both Practice and Match Modes and is accessible via the side panel. - -![image_caption](img/synthesis/config-mode.png) - -In each configuration panel, you will be able to cancel and save your modifications. You will also be given the option to `Session Save`, saving your changes until you quit the session. If you want the changes you make to that robot to be saved permanently, hit the `Save` button on the far right. - -## Intake and Outtake - -### Pickup - -The first option in Configuration Mode allows you to adjust your intake. You can move the pickup zone in three dimensions and adjust the zone size using the configure panel on the side. By changing the zone size, you can adjust how much room your robot has to intake game pieces. - -**Grounding**: You can select a part of your robot to attach your pickup zone to. - -### Ejector - -Similarly to pickup configuration, you’re able to move your ejection path in three dimensions and ground the ejector to a specific part on the robot. You can also rotate your ejection path and adjust the speed the game piece is shot. - -## Motors - -Motor configuration allows you to modify the motors on your robot. Using the slide bars assigned to each motor, you can adjust the target velocity accordingly. - -![image_caption](img/synthesis/motor-config.png) - -Pictured here are the motors on Team 2471's 2018 robot. - -**RPM**: Revolutions per minute, standard unit used to measure speed of drivetrains. -**M/S**: Meter per second, standard unit of both speed and velocity. - -## Drivetrain - -In drivetrain configuration, you can change the drivetrain your robot uses, which can then be adjusted in motor configuration and controls. - -![image_caption](img/synthesis/change-drivetrain-panel.png) - -The drivetrains available on Synthesis are as the following: - -* **None**: Selecting ‘None’ will disable your robot’s drive. -* **Tank**: A direct-drive control where each joystick is used for one side of the drivetrain -* **Arcade**: A control scheme in which one single joystick controls both the robot’s forward and backward movement, as well as its turning. -* **Swerve**: A specialized drivetrain that allows each wheel to move and rotate independently. -* **Mecanum**: An omnidirectional drivetrain. When using mecanum drive, wheels will switch to a mecanum wheel model. When switching back, the wheels will revert back to standard or omni wheels (whichever you've selected during exportation). -* **Omni**: A general purpose, omnidirectional drivetrain. This includes Kiwi and H drives. All wheels will switch to an omni wheel model while using this drivetrain model. - -### Compatibility - -Not all drivetrains can be equipped by every model. Swerve drive especially is not compatible with robots that haven’t specifically been made to use it. - -## Need More Help? - -If you need help with anything regarding Synthesis or it's related features please reach out through our -[discord sever](https://www.discord.gg/hHcF9AVgZA). It's the best way to get in contact with the community and our current developers. diff --git a/tutorials/ConfigureAssets.md b/tutorials/ConfigureAssets.md new file mode 100644 index 0000000000..c5bb5b23b4 --- /dev/null +++ b/tutorials/ConfigureAssets.md @@ -0,0 +1,68 @@ +author: Synthesis Team +summary: Tutorial for navigating and using the configure assets panel. +id: ConfigureAssets +tags: Configuration, Assets, Options, Customization +categories: Configuration +environments: Synthesis +status: Draft +feedback link: https://github.com/Autodesk/synthesis/issues + +# Configure Assets + +## Main Panel + +Use the configure assets panel to modify assemblies in the scene. You can open it either via the left-hand menu, +or by right clicking on an assembly and selecting "Configure". + +## Robots + +### Brain + +The brain determines what controls the robot. There are currently two options, "Synthesis" and "WPILib". + +### Move + +Add a gizmo tool to your field to orientate it. + +### Intake + +Setup the intake zone for your robot. + +### Ejector + +Setup the ejector position and direction for your robot. + +### Configure Joints + +Edit the joints on your robot and adjust the speed and force behind them. + +### Sequence Joints + +Configure joints to work together. Helpful for multi-stage elevators. + +### Controls (Synthesis Brain) + +Change the controls of the input scheme currently in use, as well as switch which scheme is actively in use. + +### Simulation (WPILib Brain) + +Modify the relation between your robot within Synthesis and your code. + +## Fields + +### Move + +Add a gizmo tool to your field to orientate it. + +### Scoring Zones + +Add, modify, and delete scoring zones. + +## Input + +This works the same as the controls section for the robot. You can modify the controls for the schemes, as well as add and delete them. + +## Need More Help? + +If you need help with anything regarding Synthesis or it's related features please reach out through our +[discord sever](https://www.discord.gg/hHcF9AVgZA). It's the best way to get in contact with the community and our current developers. diff --git a/tutorials/FusionExporter.md b/tutorials/FusionExporter.md index ed9d87e8ad..28140ec5c6 100644 --- a/tutorials/FusionExporter.md +++ b/tutorials/FusionExporter.md @@ -15,11 +15,45 @@ The Synthesis Fusion 360 exporter is the tool used by both developers and users For information regarding the manual install process visit the [Synthesis Fusion 360 Exporter](https://github.com/Autodesk/synthesis/tree/prod/exporter) source code for more information. -### Getting Started - -After installing Synthesis, the exporter addin should automatically start up when you open Fusion 360 (given that it was selected during the Synthesis install process). Navigate to the Utilities tab and you should see a Synthesis button. - -![image_caption](img/fusion/exporter-button.png) +## Getting Started + +### Using an Installer + +- Visit [synthesis.autodesk.com/download](https://synthesis.autodesk.com/download.html) and select the installer for your operating system. + - Note that there is no installer for Linux since Fusion is only supported on Windows and Mac. +- Once you have downloaded the installer for your operating system (`.exe` for Windows and `.pkg` for Mac) go ahead and run the executable. + - Since we do not code sign our installers (as interns of Autodesk we have very little control over this) you may get a warning from your operating system. + - For Mac to get around this see [this](https://support.apple.com/en-tm/guide/mac-help/mh40616/mac) guide for more information. +- If you are at all concerned that we are doing something nefarious please feel free to [install the exporter manually.](#manual-install) + - Alternatively, you can even inspect how we build our installers [here](./exporter/) and build them yourself. + +### Manual Install + +- Navigate to [`synthesis.autodesk.com/download`](https://synthesis.autodesk.com/download.html). +- Find the Exporter source code zip download. + - Note that the source code is platform agnostic, it will work for **both** `Windows` and `Mac`. +- Once the source code for the Exporter is downloaded unzip the folder. +- Next, if you haven't already, install `Autodesk Fusion`. +- Once Fusion is open, navigate to the `Utilities Toolbar`. +![image_caption](img/fusion/fusion-empty.png) +- Click on `Scripts and Add-ins` in the toolbar. +![image_caption](img/fusion/fusion-addins-highlight.png) +- Navigate to `Add-ins` and select the green plus icon. +![image_caption](img/fusion/fusion-addins-panel.png) +- Now navigate to wherever you extracted the original `.zip` source code file you downloaded. + - Make sure to select the folder that contains the `Synthesis.py` file, this is the entry point to the Exporter. +![image_caption](img/fusion/fusion-add-addin.png) +- Once the extension is added you should be able to see it under `My Add-Ins`. +- Select `Synthesis` from the `My Add-Ins` drop down and click `Run` in the bottom right. +![image_caption](img/fusion/fusion-addin-synthesis.png) +- The first time you run the extension it may prompt you to restart Fusion, this is totally normal. +- Once you restart Fusion the extension will run on startup, you will be able to find it on the right side of the toolbar +under the `Utilities` tab. +![image_caption](img/fusion/fusion-utilities-with-synthesis.png) + +Thanks for installing the Synthesis Fusion Exporter! For any additional help visit our [Synthesis Community Discord Server](https://www.discord.gg/hHcF9AVgZA) where you can talk directly to our developers. + +### Launching the Exporter After clicking the button, a panel will open up. This is the exporter. In this panel, you can provide us with most of the extra data we need to properly simulate your robot or field in Synthesis. diff --git a/tutorials/Makefile b/tutorials/Makefile index 6ea0e5d394..8d8358776b 100644 --- a/tutorials/Makefile +++ b/tutorials/Makefile @@ -1,6 +1,6 @@ -.PHONY: all FusionExporter CodeSimulation ConfigMode PracticeMode MatchMode ReplayMode RobotBuilder Utilities +.PHONY: all FusionExporter CodeSimulation APS ConfigureAssets Utilities -all: FusionExporter CodeSimulation ConfigMode PracticeMode MatchMode ReplayMode RobotBuilder Utilities +all: FusionExporter CodeSimulation APS ConfigureAssets Utilities OUTPUT_DIRECTORY := ./out/ @@ -10,20 +10,11 @@ FusionExporter: CodeSimulation: claat export -o $(OUTPUT_DIRECTORY) CodeSimulation.md -ConfigMode: - claat export -o $(OUTPUT_DIRECTORY) ConfigMode.md +APS: + claat export -o $(OUTPUT_DIRECTORY) APS.md -MatchMode: - claat export -o $(OUTPUT_DIRECTORY) MatchMode.md - -PracticeMode: - claat export -o $(OUTPUT_DIRECTORY) PracticeMode.md - -ReplayMode: - claat export -o $(OUTPUT_DIRECTORY) ReplayMode.md - -RobotBuilder: - claat export -o $(OUTPUT_DIRECTORY) RobotBuilder.md +ConfigureAssets: + claat export -o $(OUTPUT_DIRECTORY) ConfigureAssets.md Utilities: claat export -o $(OUTPUT_DIRECTORY) Utilities.md diff --git a/tutorials/MatchMode.md b/tutorials/MatchMode.md deleted file mode 100644 index f42f6eb4cf..0000000000 --- a/tutorials/MatchMode.md +++ /dev/null @@ -1,56 +0,0 @@ -author: Synthesis Team -summary: Tutorial for using Match Mode -id: MatchModeCodelab -tags: Match Mode, Modes, Game Simulation -categories: Modes -environments: Synthesis -status: Draft -feedback link: https://github.com/Autodesk/synthesis/issues - -# Match Mode in Synthesis - -## Intro - -Match Mode is a new mode in Synthesis that allows you to simulate a FIRST match using a field of your choice, complete with alliances and a scoreboard to track points. - -### Accessing Match Mode - -After opening Synthesis, select Single Player. There, you should be able to choose between Practice Mode and Match Mode. - -![image_caption](img/synthesis/choose-mode-modal.png) - -### Preparing a Match - -Once Match Mode is selected, you will be able to choose your robots for each alliance, as well as a field. - -Next, you will be able to view your selected field and set the spawn locations for each robot. You can spawn your robots anywhere within the field by selecting each robot and clicking on the location. - -![image_caption](img/synthesis/match-mode-setup.png) - -## Scoring Zones - -### Setting Up Scoring Zones - -After setting your robot’s spawning locations, you can place down scoring zones anywhere on the field. Using the controls on the panel, you can change the scoring zone’s dimensions and adjust how many points each zone is worth. - -* Parent Object: - * For fields with dynamic field elements, you can anchor a scoring zone to a parent object by selecting a part of the field model, allowing the scoring zone to move with the selected object. -* Destroy Game Piece: - * When enabled, game pieces will disappear once placed in the scoring zone. -* Persistent Points: - * Points earned remain even when the game piece leaves the scoring zone. - -![image_caption](img/synthesis/match-mode-scoring.png) - -### Beginning a Match - -Once you have set your scoring zones, the match will begin. There is a 15 second phase meant to mirror autonomous mode, followed by the 135 second tele-op period. Each phase is marked with FRC field audio. - -At the end of the match, you will get a panel showing the points earned. Here, you have the option to exit out of match mode, restart the match, or reconfigure the match with different bots or a different field. - -![image_caption](img/synthesis/match-mode-results.png) - -## Need More Help? - -If you need help with anything regarding Synthesis or it's related features please reach out through our -[discord sever](https://www.discord.gg/hHcF9AVgZA). It's the best way to get in contact with the community and our current developers. diff --git a/tutorials/PracticeMode.md b/tutorials/PracticeMode.md deleted file mode 100644 index 9a96c0613b..0000000000 --- a/tutorials/PracticeMode.md +++ /dev/null @@ -1,51 +0,0 @@ -author: Synthesis Team -summary: Tutorial for using Practice Mode -id: PracticeModeCodelab -tags: Practice Mode, Modes, Game Simulation -categories: Modes -environments: Synthesis -status: Draft -feedback link: https://github.com/Autodesk/synthesis/issues - -# Practice Mode in Synthesis - -## Intro - -Practice mode is one of Synthesis’ primary modes, made as an open, unrestricted testing zone. -you can load robots and field models into. - -To load in a model, select Spawn Asset. You’ll be given the choice between spawning a robot and a field. Then you’ll be able to choose which model to generate (Note: If running Synthesis for the first time, make sure to go to Download Assets and and download the preloaded assets, otherwise there will be nothing to spawn). - -## View - -View refers to your camera view. In Synthesis, there are four types for you to choose from. - -- **Orbit**: The default camera view, use left click to rotate the camera and scroll to zoom in. -- **Freecam**: Freecam gives you full control of the camera. Use right click to rotate the camera, right click plus WASD to move the camera left, right, forward and backwards, and scroll to zoom. -- **Overview**: Overview is a static camera pointed down at the field. Use scroll to zoom in and out. -- **Driver Station**: A camera view meant to mimic a driver’s view from the Driver station. Use right click and WASD to position the camera, and scroll to zoom. This view is similar to Freecam, but always points towards your robot. - -![image_caption](img/synthesis/view-panel.png) - -## Configure - -See our [Configure Mode Tutorial](https://synthesis.autodesk.com/codelab/ConfigModeCodelab/index.html#0) for more information about config mode. - -## Download Asset - -To download preloaded robots and fields, open Download Assets and select which models you would like to download. - -If this is the first time you are running Synthesis, you won’t be able to spawn anything without downloading the models first. - -![image_caption](img/synthesis/asset-downloader.png) - -## Scoring Zones - -In Practice Mode, you can set up scoring zones in a spawned field to score points. - -For a more indepth view on Scoring Zones, check out our [Scoring Zone Tutorial](https://synthesis.autodesk.com/codelab/MatchModeCodelab/index.html#1). - -## Need More Help? - -If you need help with anything regarding Synthesis or it's related features please reach out through our -[discord sever](https://www.discord.gg/hHcF9AVgZA). It's the best way to get in contact with the community and our current developers. diff --git a/tutorials/ReplayMode.md b/tutorials/ReplayMode.md deleted file mode 100644 index c8ca92343a..0000000000 --- a/tutorials/ReplayMode.md +++ /dev/null @@ -1,21 +0,0 @@ -author: Synthesis Team -summary: Tutorial for using Replay Mode -id: ReplayModeCodelab -tags: Game Simulation, Modes -categories: Modes -environments: Synthesis -status: Draft -feedback link: https://github.com/Autodesk/synthesis/issues - -# Replay Mode in Synthesis - -Replay Mode is a feature that allows you to rewind the **last 5 seconds of game play**. - -By hitting `Tab` on your keyboard, a slider will show up on the bottom of your screen. You can pull the slider in either direction to replay your simulation. Hit `Tab` again to close out of Replay Mode. - -![image_caption](img/synthesis/replay-mode.png) - -## Need More Help? - -If you need help with anything regarding Synthesis or it's related features please reach out through our -[discord sever](https://www.discord.gg/hHcF9AVgZA). It's the best way to get in contact with the community and our current developers. diff --git a/tutorials/RobotBuilder.md b/tutorials/RobotBuilder.md deleted file mode 100644 index 8803c368c4..0000000000 --- a/tutorials/RobotBuilder.md +++ /dev/null @@ -1,49 +0,0 @@ -author: Synthesis Team -summary: Tutorial for using the Robot Builder -id: RobotBuilderCodelab -tags: Robot Builder, Modes, Game Simulation -categories: Modes -environments: Synthesis -status: Draft -feedback link: https://github.com/Autodesk/synthesis/issues - -# Robot Builder in Synthesis - -## Intro - -Robot Builder is a new feature in Synthesis that lets you import parts to put together a custom robot, without having to CAD and export a full model from an external software. - -![image_caption](img/synthesis/robot-builder.png) - -## Part Editor - -Before building a robot in Robot Editor, you must have parts first. After selecting Part Editor, you’ll see a drop down menu where you can select a part that’s already been made or create a new part. - -In Download Assets, you can download preloaded part files to use in Part Editor. To import custom parts, check out [this](https://synthesis.autodesk.com/codelab/FusionExporterCodelab/index.html#0) page on using Synthesis exporter add in. - -### connection Points - -Once you enter Part Editor, a panel will open up. In this panel, you can add and remove connection points. These will act as the joint for your part. - -![image_caption](img/synthesis/robot-editor-1.png) - -When the connection point is selected on the panel, you’ll be able to move and place the connection point as you see fit as well as orient the way an attached part will face by rotating the point. - -The large arrow is an indicator of which way the attached part will face. - -## Robot Editor - -In Robot Editor, you’re able to use the parts created in Part Editor to put together a robot. Just press Add and select the part you want to use. - -![image_caption](img/synthesis/robot-editor-2.png) - -Once you have the parts loaded in, use the panel to select a part you want to join. If a part has multiple connection points, you can choose which one to attach your second part to by moving your cursor over the point. - -### Using Your Custom Robot - -To use the robot you made in Robot Builder, go to Spawn Asset, select Robot, then select Custom. You should then get a drop down menu with your robot listed. Once you hit load, it will spawn in like a regular robot. - -## Need More Help? - -If you need help with anything regarding Synthesis or it's related features please reach out through our -[discord sever](https://www.discord.gg/hHcF9AVgZA). It's the best way to get in contact with the community and our current developers. diff --git a/tutorials/img/aps/aps-button-fusion.png b/tutorials/img/aps/aps-button-fusion.png new file mode 100644 index 0000000000..4c9ce76c13 Binary files /dev/null and b/tutorials/img/aps/aps-button-fusion.png differ diff --git a/tutorials/img/aps/aps-info-fusion.png b/tutorials/img/aps/aps-info-fusion.png new file mode 100644 index 0000000000..654bcd0c3d Binary files /dev/null and b/tutorials/img/aps/aps-info-fusion.png differ diff --git a/tutorials/img/aps/aps-left-menu-fission.png b/tutorials/img/aps/aps-left-menu-fission.png new file mode 100644 index 0000000000..03427bb330 Binary files /dev/null and b/tutorials/img/aps/aps-left-menu-fission.png differ diff --git a/tutorials/img/aps/aps-location-fusion.png b/tutorials/img/aps/aps-location-fusion.png new file mode 100644 index 0000000000..3b0e556f6f Binary files /dev/null and b/tutorials/img/aps/aps-location-fusion.png differ diff --git a/tutorials/img/aps/aps-login-fission.png b/tutorials/img/aps/aps-login-fission.png new file mode 100644 index 0000000000..8c4225e6d8 Binary files /dev/null and b/tutorials/img/aps/aps-login-fission.png differ diff --git a/tutorials/img/aps/aps-login-fusion.png b/tutorials/img/aps/aps-login-fusion.png new file mode 100644 index 0000000000..8d4dede258 Binary files /dev/null and b/tutorials/img/aps/aps-login-fusion.png differ diff --git a/tutorials/img/aps/aps-notif-fission.png b/tutorials/img/aps/aps-notif-fission.png new file mode 100644 index 0000000000..7c189a78f4 Binary files /dev/null and b/tutorials/img/aps/aps-notif-fission.png differ diff --git a/tutorials/img/aps/aps-profile-fission.png b/tutorials/img/aps/aps-profile-fission.png new file mode 100644 index 0000000000..ec089d1f22 Binary files /dev/null and b/tutorials/img/aps/aps-profile-fission.png differ diff --git a/tutorials/img/aps/import-fission.png b/tutorials/img/aps/import-fission.png new file mode 100644 index 0000000000..789169ace9 Binary files /dev/null and b/tutorials/img/aps/import-fission.png differ diff --git a/tutorials/img/code-sim/auto-testing.png b/tutorials/img/code-sim/auto-testing.png new file mode 100644 index 0000000000..32414a2425 Binary files /dev/null and b/tutorials/img/code-sim/auto-testing.png differ diff --git a/tutorials/img/code-sim/can-config.png b/tutorials/img/code-sim/can-config.png new file mode 100644 index 0000000000..332df79204 Binary files /dev/null and b/tutorials/img/code-sim/can-config.png differ diff --git a/tutorials/img/code-sim/config-panel-simulation.png b/tutorials/img/code-sim/config-panel-simulation.png new file mode 100644 index 0000000000..e819ad2294 Binary files /dev/null and b/tutorials/img/code-sim/config-panel-simulation.png differ diff --git a/tutorials/img/code-sim/debug-tools.png b/tutorials/img/code-sim/debug-tools.png new file mode 100644 index 0000000000..70e63490bd Binary files /dev/null and b/tutorials/img/code-sim/debug-tools.png differ diff --git a/tutorials/img/code-sim/encoder-config.png b/tutorials/img/code-sim/encoder-config.png new file mode 100644 index 0000000000..e7d8c68563 Binary files /dev/null and b/tutorials/img/code-sim/encoder-config.png differ diff --git a/tutorials/img/code-sim/wiring-panel.png b/tutorials/img/code-sim/wiring-panel.png new file mode 100644 index 0000000000..ca4e018deb Binary files /dev/null and b/tutorials/img/code-sim/wiring-panel.png differ diff --git a/tutorials/img/code-sim/wpilib-ext-simulate.png b/tutorials/img/code-sim/wpilib-ext-simulate.png new file mode 100644 index 0000000000..354b17e37e Binary files /dev/null and b/tutorials/img/code-sim/wpilib-ext-simulate.png differ diff --git a/tutorials/img/fusion/fusion-add-addin.png b/tutorials/img/fusion/fusion-add-addin.png new file mode 100644 index 0000000000..bd35ee0ce2 Binary files /dev/null and b/tutorials/img/fusion/fusion-add-addin.png differ diff --git a/tutorials/img/fusion/fusion-addin-synthesis.png b/tutorials/img/fusion/fusion-addin-synthesis.png new file mode 100644 index 0000000000..112a0fe07e Binary files /dev/null and b/tutorials/img/fusion/fusion-addin-synthesis.png differ diff --git a/tutorials/img/fusion/fusion-addins-highlight.png b/tutorials/img/fusion/fusion-addins-highlight.png new file mode 100644 index 0000000000..33a5fb9b10 Binary files /dev/null and b/tutorials/img/fusion/fusion-addins-highlight.png differ diff --git a/tutorials/img/fusion/fusion-addins-panel.png b/tutorials/img/fusion/fusion-addins-panel.png new file mode 100644 index 0000000000..26e382781e Binary files /dev/null and b/tutorials/img/fusion/fusion-addins-panel.png differ diff --git a/tutorials/img/fusion/fusion-empty.png b/tutorials/img/fusion/fusion-empty.png new file mode 100644 index 0000000000..b3617d08b7 Binary files /dev/null and b/tutorials/img/fusion/fusion-empty.png differ diff --git a/tutorials/img/fusion/fusion-utilities-with-synthesis.png b/tutorials/img/fusion/fusion-utilities-with-synthesis.png new file mode 100644 index 0000000000..e9e3df0ba4 Binary files /dev/null and b/tutorials/img/fusion/fusion-utilities-with-synthesis.png differ diff --git a/tutorials/img/synthesis/asset-downloader.png b/tutorials/img/synthesis/asset-downloader.png deleted file mode 100644 index 95b03db447..0000000000 Binary files a/tutorials/img/synthesis/asset-downloader.png and /dev/null differ diff --git a/tutorials/img/synthesis/change-drivetrain-panel.png b/tutorials/img/synthesis/change-drivetrain-panel.png deleted file mode 100644 index 37b2aed337..0000000000 Binary files a/tutorials/img/synthesis/change-drivetrain-panel.png and /dev/null differ diff --git a/tutorials/img/synthesis/choose-mode-modal.png b/tutorials/img/synthesis/choose-mode-modal.png deleted file mode 100644 index f42e5e7f27..0000000000 Binary files a/tutorials/img/synthesis/choose-mode-modal.png and /dev/null differ diff --git a/tutorials/img/synthesis/config-mode.png b/tutorials/img/synthesis/config-mode.png deleted file mode 100644 index 44787d319e..0000000000 Binary files a/tutorials/img/synthesis/config-mode.png and /dev/null differ diff --git a/tutorials/img/synthesis/god-mode-dragging.png b/tutorials/img/synthesis/god-mode-dragging.png deleted file mode 100644 index 1ba5087062..0000000000 Binary files a/tutorials/img/synthesis/god-mode-dragging.png and /dev/null differ diff --git a/tutorials/img/synthesis/match-mode-results.png b/tutorials/img/synthesis/match-mode-results.png deleted file mode 100644 index 6f09bf5ccd..0000000000 Binary files a/tutorials/img/synthesis/match-mode-results.png and /dev/null differ diff --git a/tutorials/img/synthesis/match-mode-scoring.png b/tutorials/img/synthesis/match-mode-scoring.png deleted file mode 100644 index 9e25457377..0000000000 Binary files a/tutorials/img/synthesis/match-mode-scoring.png and /dev/null differ diff --git a/tutorials/img/synthesis/match-mode-setup.png b/tutorials/img/synthesis/match-mode-setup.png deleted file mode 100644 index 1bdefbae51..0000000000 Binary files a/tutorials/img/synthesis/match-mode-setup.png and /dev/null differ diff --git a/tutorials/img/synthesis/motor-config.png b/tutorials/img/synthesis/motor-config.png deleted file mode 100644 index 95ba844854..0000000000 Binary files a/tutorials/img/synthesis/motor-config.png and /dev/null differ diff --git a/tutorials/img/synthesis/multibot.png b/tutorials/img/synthesis/multibot.png deleted file mode 100644 index b58bd99d7e..0000000000 Binary files a/tutorials/img/synthesis/multibot.png and /dev/null differ diff --git a/tutorials/img/synthesis/replay-mode.png b/tutorials/img/synthesis/replay-mode.png deleted file mode 100644 index 8483f60ad4..0000000000 Binary files a/tutorials/img/synthesis/replay-mode.png and /dev/null differ diff --git a/tutorials/img/synthesis/robot-builder.png b/tutorials/img/synthesis/robot-builder.png deleted file mode 100644 index 7635bc964b..0000000000 Binary files a/tutorials/img/synthesis/robot-builder.png and /dev/null differ diff --git a/tutorials/img/synthesis/robot-editor-1.png b/tutorials/img/synthesis/robot-editor-1.png deleted file mode 100644 index 18411de565..0000000000 Binary files a/tutorials/img/synthesis/robot-editor-1.png and /dev/null differ diff --git a/tutorials/img/synthesis/robot-editor-2.png b/tutorials/img/synthesis/robot-editor-2.png deleted file mode 100644 index 66ef8397e7..0000000000 Binary files a/tutorials/img/synthesis/robot-editor-2.png and /dev/null differ diff --git a/tutorials/img/synthesis/settings-panel.png b/tutorials/img/synthesis/settings-panel.png deleted file mode 100644 index ecb27565bc..0000000000 Binary files a/tutorials/img/synthesis/settings-panel.png and /dev/null differ diff --git a/tutorials/img/synthesis/theme-editor.png b/tutorials/img/synthesis/theme-editor.png deleted file mode 100644 index e2b676afaa..0000000000 Binary files a/tutorials/img/synthesis/theme-editor.png and /dev/null differ diff --git a/tutorials/img/synthesis/view-panel.png b/tutorials/img/synthesis/view-panel.png deleted file mode 100644 index 18b70e48e9..0000000000 Binary files a/tutorials/img/synthesis/view-panel.png and /dev/null differ