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(