├── .gitignore ├── Add-IN └── ACDC4Robot │ ├── core │ ├── __init__.py │ ├── utils.py │ ├── robot.py │ ├── urdf_plus.py │ ├── write.py │ ├── math_operation.py │ ├── mjcf.py │ ├── joint.py │ ├── sdf.py │ ├── link.py │ └── urdf.py │ ├── commands │ ├── ACDC4Robot │ │ ├── __init__.py │ │ ├── resources │ │ │ ├── 16x16.png │ │ │ ├── 32x32.png │ │ │ └── 64x64.png │ │ ├── constants.py │ │ ├── entry.py │ │ └── acdc4robot.py │ └── __init__.py │ ├── lib │ └── fusion360utils │ │ ├── __init__.py │ │ ├── general_utils.py │ │ └── event_utils.py │ ├── .env │ ├── tools │ ├── Readme.md │ └── FlattenOccs.py │ ├── ACDC4Robot.py │ └── config.py ├── pictures ├── BreakLink.gif ├── RunAdd-In.gif ├── UR5e-Test.png ├── ChangeUnits.png ├── ExcuteAdd-In.gif ├── Github-Survey.png ├── JointOperation.gif ├── TurnIntoBodies.png ├── MakeIndependent.gif ├── ACDC4Robot-Logo-Text.png ├── Four-Bar-Linkages-Test.png ├── Robotiq-Gripper-Test.png ├── SDF-Link-Joint-Model.png ├── ScrewsFixedInComponent.png ├── URDF-Link-Joint-Model.png ├── Assemble-Four-Bar-Linkage.gif ├── DoNotCaptureDesignHistory.PNG └── Fusion360-Link-Joint-Model.png ├── privacy-policy.md ├── LICENSE ├── RobotLibrary.md ├── InstructionsForAssembly.md ├── README.md └── MethodExplanation.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/commands/ACDC4Robot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pictures/BreakLink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/BreakLink.gif -------------------------------------------------------------------------------- /pictures/RunAdd-In.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/RunAdd-In.gif -------------------------------------------------------------------------------- /pictures/UR5e-Test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/UR5e-Test.png -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/lib/fusion360utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .general_utils import * 2 | from .event_utils import * 3 | -------------------------------------------------------------------------------- /pictures/ChangeUnits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/ChangeUnits.png -------------------------------------------------------------------------------- /pictures/ExcuteAdd-In.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/ExcuteAdd-In.gif -------------------------------------------------------------------------------- /pictures/Github-Survey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/Github-Survey.png -------------------------------------------------------------------------------- /pictures/JointOperation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/JointOperation.gif -------------------------------------------------------------------------------- /pictures/TurnIntoBodies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/TurnIntoBodies.png -------------------------------------------------------------------------------- /pictures/MakeIndependent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/MakeIndependent.gif -------------------------------------------------------------------------------- /pictures/ACDC4Robot-Logo-Text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/ACDC4Robot-Logo-Text.png -------------------------------------------------------------------------------- /pictures/Four-Bar-Linkages-Test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/Four-Bar-Linkages-Test.png -------------------------------------------------------------------------------- /pictures/Robotiq-Gripper-Test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/Robotiq-Gripper-Test.png -------------------------------------------------------------------------------- /pictures/SDF-Link-Joint-Model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/SDF-Link-Joint-Model.png -------------------------------------------------------------------------------- /pictures/ScrewsFixedInComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/ScrewsFixedInComponent.png -------------------------------------------------------------------------------- /pictures/URDF-Link-Joint-Model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/URDF-Link-Joint-Model.png -------------------------------------------------------------------------------- /pictures/Assemble-Four-Bar-Linkage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/Assemble-Four-Bar-Linkage.gif -------------------------------------------------------------------------------- /pictures/DoNotCaptureDesignHistory.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/DoNotCaptureDesignHistory.PNG -------------------------------------------------------------------------------- /pictures/Fusion360-Link-Joint-Model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/pictures/Fusion360-Link-Joint-Model.png -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=/Users/aspartame/Library/Application Support/Autodesk/webdeploy/production/0b28106eb374bf811f579480c256234fe9cdc357/Api/Python/packages 2 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/commands/ACDC4Robot/resources/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/Add-IN/ACDC4Robot/commands/ACDC4Robot/resources/16x16.png -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/commands/ACDC4Robot/resources/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/Add-IN/ACDC4Robot/commands/ACDC4Robot/resources/32x32.png -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/commands/ACDC4Robot/resources/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bionicdl-sustech/ACDC4Robot/HEAD/Add-IN/ACDC4Robot/commands/ACDC4Robot/resources/64x64.png -------------------------------------------------------------------------------- /privacy-policy.md: -------------------------------------------------------------------------------- 1 | # ACDC4Robot Privacy Policy 2 | This Add-In only runs **LOCALLY**. 3 | 4 | **NO DATA** will be collected from user. 5 | 6 | **NO DATA** will be shared with any third party. -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/tools/Readme.md: -------------------------------------------------------------------------------- 1 | # ACDC4Robot Tools 2 | This folder contains tools that help users to use ACDC4Robot more convenient. 3 | The usage for each tools described below. 4 | 5 | ## FlattenOcc 6 | Since ACDC4Robot currently does not support nested occurrence, user needs to transfrom nested occurrence into a single occurrence that contains all the geometry bodies in the nested child occurrences. 7 | In simple terms, it means to cut and paste bodies of child occurrences into the target occurrence, and then delete those child occurrences. 8 | So we provide a tool called FlattenOcc to automatically produce this process. 9 | 10 | User just run this script and choose the occurrence that you want to flatten. -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/ACDC4Robot.py: -------------------------------------------------------------------------------- 1 | # Assuming you have not changed the general structure of the template no modification is needed in this file. 2 | from . import commands 3 | from .lib import fusion360utils as futil 4 | 5 | 6 | def run(context): 7 | try: 8 | # This will run the start function in each of your commands as defined in commands/__init__.py 9 | commands.start() 10 | 11 | except: 12 | futil.handle_error('run') 13 | 14 | 15 | def stop(context): 16 | try: 17 | # Remove all of the event handlers your app has created 18 | futil.clear_handlers() 19 | 20 | # This will run the start function in each of your commands as defined in commands/__init__.py 21 | commands.stop() 22 | 23 | except: 24 | futil.handle_error('stop') -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/config.py: -------------------------------------------------------------------------------- 1 | # Application Global Variables 2 | # This module serves as a way to share variables across different 3 | # modules (global variables). 4 | 5 | import os 6 | 7 | # Flag that indicates to run in Debug mode or not. When running in Debug mode 8 | # more information is written to the Text Command window. Generally, it's useful 9 | # to set this to True while developing an add-in and set it to False when you 10 | # are ready to distribute it. 11 | DEBUG = True 12 | 13 | # Gets the name of the add-in from the name of the folder the py file is in. 14 | # This is used when defining unique internal names for various UI elements 15 | # that need a unique name. It's also recommended to use a company name as 16 | # part of the ID to better ensure the ID is unique. 17 | ADDIN_NAME = os.path.basename(os.path.dirname(__file__)) 18 | COMPANY_NAME = 'SUSTech' 19 | 20 | # Palettes 21 | sample_palette_id = f'{COMPANY_NAME}_{ADDIN_NAME}_palette_id' -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # Here you define the commands that will be added to your add-in. 2 | 3 | # TODO Import the modules corresponding to the commands you created. 4 | # If you want to add an additional command, duplicate one of the existing directories and import it here. 5 | # You need to use aliases (import "entry" as "my_module") assuming you have the default module named "entry". 6 | from .ACDC4Robot import entry as RDF 7 | # from .ImportRobot import entry as Model 8 | 9 | # TODO add your imported modules to this list. 10 | # Fusion will automatically call the start() and stop() functions. 11 | commands = [ 12 | RDF, 13 | # Model, 14 | ] 15 | 16 | 17 | # Assumes you defined a "start" function in each of your modules. 18 | # The start function will be run when the add-in is started. 19 | def start(): 20 | for command in commands: 21 | command.start() 22 | 23 | 24 | # Assumes you defined a "stop" function in each of your modules. 25 | # The stop function will be run when the add-in is stopped. 26 | def stop(): 27 | for command in commands: 28 | command.stop() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 BionicDL@SUSTech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | contains common useful functions for this project 4 | """ 5 | import adsk, adsk.core, adsk.fusion 6 | import re 7 | 8 | ## https://github.com/django/django/blob/master/django/utils/text.py 9 | def get_valid_filename(s): 10 | """ 11 | Return the given string converted to a string that can be used for a clean 12 | filename. Remove leading and trailing spaces; convert other spaces to 13 | underscores; and remove anything that is not an alphanumeric, dash, 14 | underscore, or dot. 15 | >>> get_valid_filename("john's portrait in 2004.jpg") 16 | 'johns_portrait_in_2004.jpg' 17 | """ 18 | # Replace the number `:#+` by `-` 19 | s = re.sub(r':.*?\+', '_', s) 20 | # Remove the number at the end of the full path name 21 | s = re.sub(r':.*$', '', s) 22 | 23 | s = str(s).strip().replace(' ', '-') 24 | return re.sub(r'(?u)[^-\w.]', '', s) 25 | 26 | def error_box(message: str): 27 | """ 28 | a message box to show error message 29 | 30 | Args: 31 | message: str 32 | the message to show 33 | """ 34 | app = adsk.core.Application.get() 35 | ui = app.userInterface 36 | box_title = "ACDC4Robot Error" 37 | buttons = adsk.core.MessageBoxButtonTypes.OKButtonType 38 | icon = adsk.core.MessageBoxIconTypes.WarningIconType 39 | 40 | _ = ui.messageBox(message, box_title, buttons, icon) 41 | 42 | def terminate_box(): 43 | """ 44 | terminate currently running command 45 | """ 46 | app = adsk.core.Application.get() 47 | ui = app.userInterface 48 | 49 | _ = ui.terminateActiveCommand() -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/commands/ACDC4Robot/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # initialize global constants 4 | SDF_FILE_DIR = "" 5 | ROBOT_NAME = "" 6 | TEXT_PALETTE = None # adsk.core.Application.palettes 7 | AUTHOR_NAME = "" # author name in model.config 8 | MODEL_DESCRIPTION = "" # model description in model.config 9 | ROBOT_DESCRIPTION_FORMAT = "" # robot description format such as urdf, sdf, mjcf, etc. 10 | SIMULATION_ENVIRONMENT = "" # simulation environment such as Gazebo, PyBullet, MuJoCo, etc. 11 | 12 | def set_sdf_file_dir(sdf_file_dir: str): 13 | global SDF_FILE_DIR 14 | SDF_FILE_DIR = sdf_file_dir 15 | 16 | def get_sdf_file_dir(): 17 | return SDF_FILE_DIR 18 | 19 | def set_robot_name(robot_name: str): 20 | global ROBOT_NAME 21 | ROBOT_NAME = robot_name 22 | 23 | def get_robot_name(): 24 | return ROBOT_NAME 25 | 26 | def set_text_palette(text_palette): 27 | global TEXT_PALETTE 28 | TEXT_PALETTE = text_palette 29 | 30 | def set_rdf(robot_description_format): 31 | global ROBOT_DESCRIPTION_FORMAT 32 | ROBOT_DESCRIPTION_FORMAT = robot_description_format 33 | 34 | def set_sim_env(sim_env): 35 | global SIMULATION_ENVIRONMENT 36 | SIMULATION_ENVIRONMENT = sim_env 37 | 38 | def get_text_palette(): 39 | return TEXT_PALETTE 40 | 41 | def set_author_name(author_name: str): 42 | global AUTHOR_NAME 43 | AUTHOR_NAME = author_name 44 | 45 | def get_author_name(): 46 | return AUTHOR_NAME 47 | 48 | def set_model_description(model_description: str): 49 | global MODEL_DESCRIPTION 50 | MODEL_DESCRIPTION = model_description 51 | 52 | def get_model_description(): 53 | return MODEL_DESCRIPTION 54 | 55 | def set_rdf(robot_description_format: str): 56 | global ROBOT_DESCRIPTION_FORMAT 57 | ROBOT_DESCRIPTION_FORMAT = robot_description_format 58 | 59 | def get_rdf() -> str: 60 | """ 61 | Return: 62 | --------- 63 | ROBOT_DESCRIPTION_FORMAT: str 64 | "URDF", "SDFormat", ... 65 | """ 66 | return ROBOT_DESCRIPTION_FORMAT 67 | 68 | def set_sim_env(sim_env: str): 69 | global SIMULATION_ENVIRONMENT 70 | SIMULATION_ENVIRONMENT = sim_env 71 | 72 | def get_sim_env() -> str: 73 | """ 74 | Return: 75 | --------- 76 | SIMULATION_ENVIRONMENT: str 77 | "Gazebo", "PyBullet", ... 78 | """ 79 | return SIMULATION_ENVIRONMENT -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/lib/fusion360utils/general_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 by Autodesk, Inc. 2 | # Permission to use, copy, modify, and distribute this software in object code form 3 | # for any purpose and without fee is hereby granted, provided that the above copyright 4 | # notice appears in all copies and that both that copyright notice and the limited 5 | # warranty and restricted rights notice below appear in all supporting documentation. 6 | # 7 | # AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS. AUTODESK SPECIFICALLY 8 | # DISCLAIMS ANY IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. 9 | # AUTODESK, INC. DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE 10 | # UNINTERRUPTED OR ERROR FREE. 11 | 12 | import os 13 | import traceback 14 | import adsk.core 15 | 16 | app = adsk.core.Application.get() 17 | ui = app.userInterface 18 | 19 | # Attempt to read DEBUG flag from parent config. 20 | try: 21 | from ... import config 22 | DEBUG = config.DEBUG 23 | except: 24 | DEBUG = False 25 | 26 | 27 | def log(message: str, level: adsk.core.LogLevels = adsk.core.LogLevels.InfoLogLevel, force_console: bool = False): 28 | """Utility function to easily handle logging in your app. 29 | 30 | Arguments: 31 | message -- The message to log. 32 | level -- The logging severity level. 33 | force_console -- Forces the message to be written to the Text Command window. 34 | """ 35 | # Always print to console, only seen through IDE. 36 | print(message) 37 | 38 | # Log all errors to Fusion log file. 39 | if level == adsk.core.LogLevels.ErrorLogLevel: 40 | log_type = adsk.core.LogTypes.FileLogType 41 | app.log(message, level, log_type) 42 | 43 | # If config.DEBUG is True write all log messages to the console. 44 | if DEBUG or force_console: 45 | log_type = adsk.core.LogTypes.ConsoleLogType 46 | app.log(message, level, log_type) 47 | 48 | 49 | def handle_error(name: str, show_message_box: bool = False): 50 | """Utility function to simplify error handling. 51 | 52 | Arguments: 53 | name -- A name used to label the error. 54 | show_message_box -- Indicates if the error should be shown in the message box. 55 | If False, it will only be shown in the Text Command window 56 | and logged to the log file. 57 | """ 58 | 59 | log('===== Error =====', adsk.core.LogLevels.ErrorLogLevel) 60 | log(f'{name}\n{traceback.format_exc()}', adsk.core.LogLevels.ErrorLogLevel) 61 | 62 | # If desired you could show an error as a message box. 63 | if show_message_box: 64 | ui.messageBox(f'{name}\n{traceback.format_exc()}') 65 | -------------------------------------------------------------------------------- /RobotLibrary.md: -------------------------------------------------------------------------------- 1 | # Fusion 360 Robot Library 2 | ## Introduction 3 | We'd like to provide Autodesk Fusion robot models that can be used out-of-box for design, simulation, and learning. 4 | We are continually updating this library with more models, and we also welcome contributions from the community. 5 | 6 | 7 | ## Robot Library 8 | | Robot Name | Robot Type | Source Link | Fusion 360 File | 9 | | ---------- | ---------- | ----------- | --------------- | 10 | | Franka Emika Hand | End Effector | [STEP file](https://www.wiredworkers.io/nl/download-franka-emika-panda-step-file/) | [Fusion 360 Model](https://a360.co/4gJ2Khi) | 11 | | Franka Emika Panda | Robot Arm | [STEP file](https://www.wiredworkers.io/nl/download-franka-emika-panda-step-file/) | [Fusion 360 Model](https://a360.co/4nSkfy6) | 12 | | Kinova Gen3 | Robot Arm | [GrabCAD](https://grabcad.com/library/kinova-gen3-modular-robotic-arm-1) | [Fusion 360 Model](https://a360.co/46zYUlP) | 13 | | Rethink Sawyer Arm | Robot Arm | [GarbCAD](https://grabcad.com/library/sawyer-robot-by-rethink-robotics-1/details?folder_id=10921066) | [Fusion 360 Model](https://a360.co/46NOG1A) | 14 | | Robotiq 2F85 | End Effector | [Robotiq](https://robotiq.com/products/2f85-140-adaptive-robot-gripper?ref=nav_product_new_button) | [Fusion 360 Model](https://a360.co/42KRxqv) | 15 | | UR5e | Robot Arm | [UR5e](https://www.universal-robots.com/products/ur5-robot/) | [Fusion 360 Model](https://a360.co/4pG9H6D) | 16 | | youBot | Mobile Robot | [KUKA youBot STEP](https://www.kuka.com/en-us/services/downloads?terms=Language:en:1;&q=youBot) | [Fusion 360 Model](https://a360.co/4nVMewR)| 17 | | Poppy | Humanoid Robot | [Poppy Github](https://github.com/poppy-project/poppy-humanoid) | [Fusion 360 Model](https://a360.co/3IGHk7L)| 18 | | Barkour | Quadruped Robot | [Deepmind Onshape](https://deepmind.onshape.com/documents/bd3aaf26c384d7d058cee090/w/9bd0468bf4dae717e9b02f17/e/6151d1e161dfa46066201d62?aa=true) | [Fusion 360 Model](https://a360.co/3VFrBc6)| 19 | | Bennett Quadruped | Quadruped Robot | [SUSTech BionicDL](https://bionicdl.ancorasir.com/) | [Fusion 360 Model](https://a360.co/4ni9KUN) | 20 | | Wheeled Bennett Quadruped | Quadruped Robot | [SUSTech BionicDL](https://bionicdl.ancorasir.com/) | [Fusion 360 Model](https://a360.co/4nVMYSF) | 21 | | Unitree Go2 | Quadruped Robot | [Unitree Go2](https://oss-global-cdn.unitree.com/static/GO2%E7%AE%80%E5%8C%96-%E4%B8%8D%E5%B8%A6%E6%8B%93%E5%B1%95%E5%9D%9E-0912.zip) | [Fusion 360 Model](https://a360.co/482HTmQ) | 22 | 23 | ## Using This Robot Library 24 | 1. Download the robot model from the provided link. 25 | 2. Import the model into Fusion 360. 26 | 3. Modify the design as needed, or use the provided model directly. 27 | 4. Export the robot description file in your desired format (URDF, SDFormat, or MJCF) using ACDC4Robot. 28 | 5. Using robot description file for visualization, simulation, or learning tasks. 29 | 30 | ## Asking for Contribution 31 | If you have a properly licensed and well-assembled Autodesk Fusion robot model, you are welcome to contribute it to this library. 32 | Please submit a PR to this repository or contact us by e-mail. -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/lib/fusion360utils/event_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 by Autodesk, Inc. 2 | # Permission to use, copy, modify, and distribute this software in object code form 3 | # for any purpose and without fee is hereby granted, provided that the above copyright 4 | # notice appears in all copies and that both that copyright notice and the limited 5 | # warranty and restricted rights notice below appear in all supporting documentation. 6 | # 7 | # AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS. AUTODESK SPECIFICALLY 8 | # DISCLAIMS ANY IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. 9 | # AUTODESK, INC. DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE 10 | # UNINTERRUPTED OR ERROR FREE. 11 | 12 | import sys 13 | from typing import Callable 14 | 15 | import adsk.core 16 | from .general_utils import handle_error 17 | 18 | 19 | # Global Variable to hold Event Handlers 20 | _handlers = [] 21 | 22 | 23 | def add_handler( 24 | event: adsk.core.Event, 25 | callback: Callable, 26 | *, 27 | name: str = None, 28 | local_handlers: list = None 29 | ): 30 | """Adds an event handler to the specified event. 31 | 32 | Arguments: 33 | event -- The event object you want to connect a handler to. 34 | callback -- The function that will handle the event. 35 | name -- A name to use in logging errors associated with this event. 36 | Otherwise the name of the event object is used. This argument 37 | must be specified by its keyword. 38 | local_handlers -- A list of handlers you manage that is used to maintain 39 | a reference to the handlers so they aren't released. 40 | This argument must be specified by its keyword. If not 41 | specified the handler is added to a global list and can 42 | be cleared using the clear_handlers function. You may want 43 | to maintain your own handler list so it can be managed 44 | independently for each command. 45 | 46 | :returns: 47 | The event handler that was created. You don't often need this reference, but it can be useful in some cases. 48 | """ 49 | module = sys.modules[event.__module__] 50 | handler_type = module.__dict__[event.add.__annotations__['handler']] 51 | handler = _create_handler(handler_type, callback, event, name, local_handlers) 52 | event.add(handler) 53 | return handler 54 | 55 | 56 | def clear_handlers(): 57 | """Clears the global list of handlers. 58 | """ 59 | global _handlers 60 | _handlers = [] 61 | 62 | 63 | def _create_handler( 64 | handler_type, 65 | callback: Callable, 66 | event: adsk.core.Event, 67 | name: str = None, 68 | local_handlers: list = None 69 | ): 70 | handler = _define_handler(handler_type, callback, name)() 71 | (local_handlers if local_handlers is not None else _handlers).append(handler) 72 | return handler 73 | 74 | 75 | def _define_handler(handler_type, callback, name: str = None): 76 | name = name or handler_type.__name__ 77 | 78 | class Handler(handler_type): 79 | def __init__(self): 80 | super().__init__() 81 | 82 | def notify(self, args): 83 | try: 84 | callback(args) 85 | except: 86 | handle_error(name) 87 | 88 | return Handler 89 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/robot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import List, Tuple 3 | import adsk, adsk.fusion, adsk.core 4 | from .link import Link 5 | from .joint import Joint 6 | 7 | class Robot(): 8 | """ 9 | Store robot link joint information into a bigraph structure 10 | """ 11 | 12 | def __init__(self, robot: adsk.fusion.Design): 13 | self.robot = robot 14 | self.rootComp = robot.rootComponent 15 | self.name = self.get_robot_name() 16 | self.nodes = self.get_links() 17 | self.edges = self.get_joints() 18 | self.loop_joints, self.tree_joints = self.seperate_joints(self.edges) 19 | 20 | def get_robot_name(self, ) -> str: 21 | """ 22 | Robot name is the root component name 23 | """ 24 | robot_name = self.rootComp.name.split()[0] 25 | return robot_name 26 | 27 | def get_links(self, ) -> List[Link]: 28 | link_list = [] 29 | occs: adsk.fusion.OccurrenceList = self.rootComp.allOccurrences 30 | 31 | # try to solve the nested components problem 32 | # but still not fully tested 33 | for occ in occs: 34 | # TODO: it seems use occ.joints.count will make it usable with occurrences? Test it 35 | if occ.component.joints.count > 0: 36 | # textPalette.writeText(str(occ.fullPathName)) 37 | continue 38 | else: 39 | # Only occurrence contains zero joint and has zero childOccurrences 40 | # can be seen as a link 41 | if occ.childOccurrences.count > 0: 42 | # textPalette.writeText(str(occ.fullPathName)) 43 | # textPalette.writeText(str(occ.childOccurrences.count)) 44 | continue 45 | else: 46 | # textPalette.writeText(str(occ.fullPathName)) 47 | # textPalette.writeText(str(occ.childOccurrences.count)) 48 | if occ.isLightBulbOn: 49 | # only the occurrence light bulb on that the occurrence will be exported 50 | link_list.append(Link(occ)) # add link objects into link_list 51 | 52 | return link_list 53 | 54 | def get_joints(self, ) -> List[Joint]: 55 | joint_list = [] 56 | 57 | for joint in self.rootComp.allJoints: 58 | joint_list.append(Joint(joint)) # add joint objects into joint_list 59 | 60 | return joint_list 61 | 62 | def get_loop_joints(self,) -> List[Joint]: 63 | return self.loop_joints 64 | 65 | def get_tree_joints(self, ) -> List[Joint]: 66 | return self.tree_joints 67 | 68 | 69 | def get_graph(self, ): 70 | pass 71 | 72 | def seperate_joints(self, joint_list: List[Joint]) -> Tuple[List[Joint], List[Joint]]: 73 | """ 74 | Seperate robot joints into two parts: loop joints, tree joints 75 | 76 | Returns: 77 | loop_joints: List[Joint] 78 | joints construct closed loop 79 | it should be noted by user explicitly by name it with prefix "loop" 80 | tree_joints: List[Joint] 81 | edges connects nodes(links) like a spanning tree 82 | """ 83 | loop_joints = [] 84 | tree_joints = [] 85 | for joint in joint_list: 86 | if joint.name.startswith("loop"): 87 | loop_joints.append(joint) 88 | else: 89 | tree_joints.append(joint) 90 | return loop_joints, tree_joints 91 | 92 | def is_connected_graph(self, ): 93 | pass 94 | -------------------------------------------------------------------------------- /InstructionsForAssembly.md: -------------------------------------------------------------------------------- 1 | # Instructions for Fusion 360 Assembly 2 | We recommend you assemble components in Fusion 360 with a flat structure, here are reasons: 3 | 1. flat structure is clear and make it easier to understand the structure of the robot model 4 | 2. nested structures might construct closed loop mechanisms that are unexpected and not supported by URDF which causes errors in simulation 5 | 3. nested structure is not fully tested with our add-in currently 6 | 7 | Here we present instructions for assembling a robot in Fusion360 that is suitable for this add-in. 8 | 9 | If you are not familiar with Fusion 360, [Fusion 360 Assemblies Tutorials](https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-4008D439-DF98-4D92-AB3D-29D8E02F9BCA) provide good tutorials to practice. 10 | 11 | ## Fusion360 Terminologies 12 | - [Body](https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-C1AB4941-D7AD-4D27-A035-2FA9208635B6): a body is a container for a 3D geometry. 13 | - [Component](https://help.autodesk.com/view/fusion360/ENU/?guid=ASM-COMPONENTS): a component is a container for design elements like sketches, construction geometry, bodies, joints, origins, and even other components. 14 | - root component: each Fusion 360 Document (file) contains a Root Component, represented by the top node in the browser tree. 15 | - Occurrence: an occurrence can be seen as the instance of a component with specified location. 16 | - [Joint](https://help.autodesk.com/view/fusion360/ENU/?guid=ASM-JOINTS): a joint is a mechanical relationship that defines the **relative position** and **motion** between 2 components in an assembly. Fusion360 supports 7 joint types which are `rigid`, `revolute`, `slider`, `cylindrical`, `pin-slot`, `planar`, and `ball`. 17 | - [Assembly](https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-38F2A3B4-CE01-4375-A21D-4CC9A743B2A1): an assembly is a collection of components that function as a single design in Fusion360. Relationship between components in a design can be defined through position, joint, and motion features. 18 | 19 | Fusion360 provides some features which make it convenient for mechanical assembly but **might cause** problems when using this add-in, for example, rigid group feature. It is better to make a copy of an existing assembly and export the robot description files to avoid problems and modifications of the original assembly. 20 | 21 | ## Joint Operation 22 | - For a joint which is defined by component1 and component2, the component1 would be the `child link` in robot description file and the component2 would be the `parent link`. 23 | ![Joint Operation](./pictures/JointOperation.gif) 24 | - Set the joint motion as `Rigid`, `Revolute`, or `Slider`. Other joint motions are not supported yet. 25 | 26 | ## Suggested Assembly Structure 27 | It is *better* for a Fusion 360 Assembly has a **flat** tree structure which means components do not contain sub-components. Here is the example: 28 | ``` 29 | - root component 30 | - Origin 31 | - Joints 32 | - Joint1 33 | - Joint2 34 | - ... 35 | - Compoent1 36 | - Origin 37 | - Bodies 38 | - Component2 39 | - Origin 40 | - Bodies 41 | - ... 42 | ``` 43 | 44 | ## Step by Steps Assembling Example 45 | - Assemble a four bar closed chain linkage: 46 | ![Assemble four bar linkage](./pictures/Assemble-Four-Bar-Linkage.gif) 47 | 48 | 49 | ## Tricks 50 | - Replace rigid joint motion type by transforming components into bodies: 51 | 1. Four screws joint with component1 by fixed joint type: 52 | ![Screws joint with component](./pictures/ScrewsFixedInComponent.png) 53 | 2. Remove all the joints and turn Screw Components into bodies and put them in Component1: 54 | ![Turn into bodies](./pictures/TurnIntoBodies.png) 55 | 3. Because bodies in a same component have a fixed relative position relationship, which provides similar features with fixed joint motion type. This will simplify robot description file without unnecessary joint elements. 56 | 57 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/urdf_plus.py: -------------------------------------------------------------------------------- 1 | # Add support for URDF+: An Enhanced URDF for Robots with Kinematic Loops 2 | # https://arxiv.org/abs/2411.19753 3 | 4 | import adsk, adsk.core, adsk.fusion 5 | from .robot import Robot 6 | from typing import List 7 | from xml.etree.ElementTree import Element, SubElement 8 | import xml.etree.ElementTree as ET 9 | from .link import Link 10 | from .joint import Joint 11 | from . import math_operation as math_op 12 | from . import utils 13 | from ..commands.ACDC4Robot import constants 14 | from .urdf import URDF 15 | 16 | class URDF_PLUS(URDF): 17 | """ 18 | Return an xml.etree.Element which contains robot model information in URDF+ format 19 | """ 20 | 21 | def __init__(self, robot: Robot): 22 | super().__init__(robot) 23 | self.links: list[Link] = robot.get_links() 24 | self.tree_joints: list[Joint] = robot.get_tree_joints() 25 | self.loop_joints: list[Joint] = robot.get_loop_joints() 26 | 27 | def write_file(self, file_path): 28 | robot_ele = self.get_robot_ele() 29 | urdf_tree = ET.ElementTree(robot_ele) 30 | acdc4robot_info = self.get_acdc4robot_info() 31 | comment = ET.Comment(acdc4robot_info) 32 | robot_ele.append(comment) 33 | 34 | for link in self.links: 35 | link_ele = self.get_link_element(link) 36 | robot_ele.append(link_ele) 37 | 38 | for joint in self.tree_joints: 39 | joint_ele = self.get_tree_joint_element(joint) 40 | robot_ele.append(joint_ele) 41 | 42 | for loop in self.loop_joints: 43 | loop_ele = self.get_loop_joint_element(loop) 44 | robot_ele.append(loop_ele) 45 | 46 | # set indent to pretty the xml output 47 | ET.indent(urdf_tree, space=" ", level=0) 48 | 49 | urdf_tree.write(file_path, encoding="utf-8", xml_declaration=True) 50 | 51 | def get_tree_joint_element(self, joint: Joint): 52 | tree_joint_ele = self.get_joint_element(joint) 53 | return tree_joint_ele 54 | 55 | def get_loop_joint_element(self, loop: Joint): 56 | loop_ele = Element("loop") 57 | loop_ele.attrib = {"name": self.get_joint_name(loop), 58 | "type": self.get_joint_type(loop)} 59 | 60 | # add predecessor element 61 | predecessor_ele = SubElement(loop_ele, "predecessor") 62 | predecessor_ele.attrib = {"name": self.get_link_name(self.get_joint_parent(loop))} 63 | pred_origin_ele = SubElement(predecessor_ele, "origin") 64 | 65 | # add successor element 66 | sucessor_ele = SubElement(loop_ele, "sucessor") 67 | sucessor_ele.attrib = {"name": self.get_link_name(self.get_joint_child(loop))} 68 | suc_origin_ele = SubElement(sucessor_ele, "origin") 69 | 70 | # get predecessor and successor origin info 71 | pred_link: Link = Link(loop.parent) 72 | suc_link: Link = Link(loop.child) 73 | pred_joint: adsk.fusion.Joint = pred_link.get_parent_joint() 74 | suc_joint: adsk.fusion.Joint = suc_link.get_parent_joint() 75 | 76 | ## get predecessor frame 77 | if pred_joint is None: 78 | parent_frame: adsk.core.Matrix3D = pred_link.pose 79 | else: 80 | pred_joint: Joint = Joint(pred_joint) 81 | pred_frame = pred_joint.get_joint_frame() 82 | 83 | ## get sucessor frame 84 | if suc_joint is None: 85 | suc_frame: adsk.core.Matrix3D = suc_link.pose 86 | else: 87 | suc_joint: Joint = Joint(suc_joint) 88 | suc_frame = suc_joint.get_joint_frame() 89 | 90 | loop_frame = loop.get_joint_frame() 91 | 92 | pred_transform = math_op.coordinate_transform(pred_frame, loop_frame) 93 | pred_origin = math_op.matrix3d_2_pose(pred_transform) 94 | suc_transform = math_op.coordinate_transform(suc_frame, loop_frame) 95 | suc_origin = math_op.matrix3d_2_pose(suc_transform) 96 | 97 | pred_origin_ele.attrib = {"xyz": f"{pred_origin[0]} {pred_origin[1]} {pred_origin[2]}", 98 | "rpy": f"{pred_origin[3]} {pred_origin[4]} {pred_origin[5]}"} 99 | suc_origin_ele.attrib = {"xyz": f"{suc_origin[0]} {suc_origin[1]} {suc_origin[2]}", 100 | "rpy": f"{suc_origin[3]} {suc_origin[4]} {suc_origin[5]}"} 101 | 102 | # add axis element 103 | # add axis1, urdf only has one axis for joint 104 | axis = self.get_joint_axis(loop) 105 | if axis is not None: 106 | axis_ele = SubElement(loop_ele, "axis") 107 | axis_ele.attrib = {"xyz": "{} {} {}".format(axis[0], axis[1], axis[2])} 108 | 109 | return loop_ele 110 | 111 | def get_robot_ele(self, ): 112 | """ 113 | Root element of URDF+ 114 | """ 115 | robot_ele = Element("robot") 116 | robot_ele.attrib = {"name": self.robot.get_robot_name()} 117 | 118 | return robot_ele 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/write.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Nuofan 3 | import xml.etree.ElementTree as ET 4 | from xml.etree.ElementTree import Element, SubElement 5 | from .link import Link 6 | from .joint import Joint 7 | from . import urdf as URDF 8 | from . import sdf as SDF 9 | from . import mjcf as MJCF 10 | import adsk, adsk.core, adsk.fusion 11 | 12 | def write_sdf(link_list: list[Link], joint_list: list[Joint], dir: str, robot_name: str): 13 | """ 14 | Write all the joint and link elements to the sdf file 15 | """ 16 | sdf_ele = Element("sdf") 17 | sdf_tree = ET.ElementTree(sdf_ele) 18 | sdf_ele.attrib = {"version": "1.7"} 19 | model = SubElement(sdf_ele, "model") 20 | model.attrib = {"name": robot_name} 21 | 22 | for link in link_list: 23 | # link_ele = link.get_link_sdf_element() 24 | link_ele = SDF.get_link_element(link) 25 | model.append(link_ele) 26 | 27 | for joint in joint_list: 28 | # joint_ele = joint.get_joint_sdf_element() 29 | joint_ele = SDF.get_joint_element(joint) 30 | model.append(joint_ele) 31 | 32 | # set indent to pretty the xml output 33 | ET.indent(sdf_tree, space=" ", level=0) 34 | 35 | sdf_file = dir + "/" + robot_name + ".sdf" 36 | sdf_tree.write(sdf_file, encoding="utf-8", xml_declaration=True) 37 | 38 | def write_sdf_config(dir: str, model_name: str, author_name: str, description: str): 39 | """ 40 | Write a config file for sdf 41 | """ 42 | model = Element("model") 43 | config_tree = ET.ElementTree(model) 44 | name = SubElement(model, "name") 45 | name.text = "{}".format(model_name) 46 | author = SubElement(model, "author") 47 | author.text = "{}".format(author_name) 48 | version = SubElement(model, "version") 49 | version.text = "{}".format("1.0.0") 50 | sdf = SubElement(model, "sdf") 51 | sdf.attrib = {"version": "1.7"} 52 | sdf.text = "{}".format(str(model_name+".sdf")) 53 | des = SubElement(model, "description") 54 | des.text = "{}".format(description) 55 | 56 | # set indent to pretty the xml output and write the config file 57 | ET.indent(config_tree, space=" ", level=0) 58 | config_file = dir + "/model.config" 59 | config_tree.write(config_file, encoding="utf-8", xml_declaration=True) 60 | 61 | def write_urdf(link_list: list[Link], joint_list: list[Joint], dir: str, robot_name: str): 62 | """ 63 | Write all the joint and link elements to the urdf file 64 | """ 65 | robot_ele = Element("robot") 66 | robot_ele.attrib = {"name": robot_name} 67 | urdf_tree = ET.ElementTree(robot_ele) 68 | 69 | for link in link_list: 70 | # link_ele = link.get_link_urdf_element() 71 | link_ele = URDF.get_link_element(link) 72 | robot_ele.append(link_ele) 73 | 74 | for joint in joint_list: 75 | # joint_ele = joint.get_joint_urdf_element() 76 | joint_ele = URDF.get_joint_element(joint) 77 | robot_ele.append(joint_ele) 78 | 79 | # set indent to pretty the xml output 80 | ET.indent(urdf_tree, space=" ", level=0) 81 | 82 | urdf_file = dir + "/" + robot_name + ".urdf" 83 | urdf_tree.write(urdf_file, encoding="utf-8", xml_declaration=True) 84 | 85 | def write_hello_pybullet(rdf_type: str, robot_name: str, save_dir: str): 86 | """ 87 | Write a basic python script to automaticlly load robot description file 88 | Parameters: 89 | --------- 90 | rdf_type: str 91 | robot description type: urdf, sdf, 92 | robot_name: str 93 | the original Fusion360 file name 94 | save_dir: str 95 | the directory to save the python script 96 | """ 97 | file_name = save_dir + "/hello_bullet.py" 98 | # template from: PyBullet Quickstart Guide - Hello PyBullet World 99 | pybullet_template: str = """import pybullet as p 100 | import time 101 | import pybullet_data 102 | physicsClient = p.connect(p.GUI)#or p.DIRECT for non-graphical version 103 | p.setAdditionalSearchPath(pybullet_data.getDataPath()) #optionally 104 | p.setGravity(0,0,-10) 105 | planeId = p.loadURDF("plane.urdf") 106 | cubeStartPos = [0,0,0] 107 | cubeStartOrientation = p.getQuaternionFromEuler([0,0,0]) 108 | {} = p.{}("{}",cubeStartPos, cubeStartOrientation, 109 | # useMaximalCoordinates=1, ## New feature in Pybullet 110 | {}) 111 | for i in range (10000): 112 | p.stepSimulation() 113 | time.sleep(1./240.) 114 | cubePos, cubeOrn = p.getBasePositionAndOrientation({}) 115 | print(cubePos,cubeOrn) 116 | p.disconnect() 117 | """ 118 | if rdf_type == "URDF": 119 | rdf_file = robot_name + ".urdf" 120 | hello_pybullet = pybullet_template.format("robotID", "loadURDF", rdf_file, "flags=p.URDF_USE_INERTIA_FROM_FILE", "robotID") 121 | elif rdf_type == "SDFormat": 122 | rdf_file = robot_name + ".sdf" 123 | hello_pybullet = pybullet_template.format("robotIDs", "loadSDF", rdf_file, "", "robotIDs") 124 | 125 | with open(file_name, mode="w") as f: 126 | f.write(hello_pybullet) 127 | f.write("\n") 128 | 129 | 130 | def write_mjcf(rootComp: adsk.fusion.Component, robotName: str, dir: str): 131 | """ 132 | Write all elements in mjcf 133 | 134 | Parameters: 135 | --------- 136 | rootComp: rootComp of the design 137 | robotName: robot name 138 | dir: directory of the mjcf file 139 | """ 140 | mjcf_ele = MJCF.get_mjcf(rootComp, robotName, dir) 141 | mjcf_tree = ET.ElementTree(mjcf_ele) 142 | 143 | # set indent to pretty the xml output 144 | ET.indent(mjcf_tree, space=" ", level=0) 145 | 146 | mjcf_file = dir + "/" + robotName + ".xml" 147 | mjcf_tree.write(mjcf_file, encoding="utf-8") 148 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/tools/FlattenOccs.py: -------------------------------------------------------------------------------- 1 | # Script for moving all the bodies in the nested child occurrences into the target occurrence 2 | 3 | import adsk.core, adsk.fusion, adsk.cam, traceback 4 | 5 | from typing import List 6 | 7 | # Global list to keep all event handlers in scope. 8 | # This is only needed with Python. 9 | handlers = [] 10 | 11 | def run(context): 12 | ui = None 13 | try: 14 | app = adsk.core.Application.get() 15 | ui = app.userInterface 16 | design = adsk.fusion.Design.cast(app.activeProduct) 17 | design.designType = adsk.fusion.DesignTypes.DirectDesignType 18 | 19 | # Get the CommandDefinitions collection. 20 | cmdDefs = ui.commandDefinitions 21 | 22 | # Create a button command definition. 23 | buttonSample = cmdDefs.addButtonDefinition('ACDC4Robot-Tool-FlattenOccs', 24 | 'Flatten Occurrences', 25 | 'Move bodies in child occs into the selected occurrence') 26 | 27 | # Connect to the command created event. 28 | sampleCommandCreated = FlattenOccCommandCreatedEventHandler() 29 | buttonSample.commandCreated.add(sampleCommandCreated) 30 | handlers.append(sampleCommandCreated) 31 | 32 | # Execute the command. 33 | buttonSample.execute() 34 | 35 | # Keep the script running. 36 | adsk.autoTerminate(False) 37 | except: 38 | if ui: 39 | ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) 40 | 41 | 42 | # Event handler for the commandCreated event. 43 | class FlattenOccCommandCreatedEventHandler(adsk.core.CommandCreatedEventHandler): 44 | def __init__(self): 45 | super().__init__() 46 | self.app = adsk.core.Application.get() 47 | self.des = adsk.fusion.Design.cast(self.app.activeProduct) 48 | self.ui = self.app.userInterface 49 | 50 | def notify(self, args): 51 | eventArgs = adsk.core.CommandCreatedEventArgs.cast(args) 52 | cmd = eventArgs.command 53 | 54 | # Create a command input 55 | inputs = cmd.commandInputs 56 | 57 | # Create a SelectionCommandInput, which can only select 1 occurrence 58 | selectInput: adsk.core.SelectionCommandInput = inputs.addSelectionInput( 59 | 'selected-1', 60 | 'Target Occurrence', 61 | 'Selecte one occurrence as input occurrence.' 62 | ) 63 | selectInput.addSelectionFilter("Occurrences") 64 | selectInput.setSelectionLimits(minimum=1, maximum=1) 65 | 66 | # Connect to the execute event. 67 | onExecute = FlattenOccCommandExecuteHandler() 68 | cmd.execute.add(onExecute) 69 | handlers.append(onExecute) 70 | 71 | 72 | # Event handler for the execute event. 73 | class FlattenOccCommandExecuteHandler(adsk.core.CommandEventHandler): 74 | def __init__(self): 75 | super().__init__() 76 | self.app = adsk.core.Application.get() 77 | self.des = adsk.fusion.Design.cast(self.app.activeProduct) 78 | self.ui = self.app.userInterface 79 | # self.textPalatte: adsk.core.TextCommandPalette = self.ui.palettes.itemById("TextCommands") 80 | # if not self.textPalatte.isVisible: 81 | # self.textPalatte.isVisible = True 82 | # self.textPalatte.writeText("Start Command Execution") 83 | 84 | def flatten_occ(self, targetOcc: adsk.fusion.Occurrence): 85 | """ 86 | Cut all sub occurrences' body into the input occurrence 87 | """ 88 | # get list of all sub occurrences 89 | allSubOcc: List[adsk.fusion.Occurrence] = [] # a list to store the occs waiting for operations 90 | def dfs(node: adsk.fusion.Occurrence): 91 | allSubOcc.append(node) 92 | if node.childOccurrences.count == 0: 93 | return 94 | for child in node.childOccurrences: 95 | dfs(child) 96 | 97 | dfs(targetOcc) 98 | 99 | allSubOcc = allSubOcc[1:] # remove targetOcc 100 | 101 | # move all bodies in child occs into target occ 102 | for occ in allSubOcc: 103 | bodyList: adsk.fusion.BRepBodies = occ.bRepBodies 104 | for body in bodyList: 105 | body.moveToComponent(targetOcc) 106 | 107 | # remove occ that has no childOcc and bodies 108 | allSubOcc.reverse() 109 | for occ in allSubOcc: 110 | if occ.childOccurrences.count == 0 and occ.bRepBodies.count == 0: 111 | occ.deleteMe() 112 | 113 | def notify(self, args): 114 | eventArgs = adsk.core.CommandEventArgs.cast(args) 115 | 116 | # Code to react to the event. 117 | 118 | # Get the values from the command inputs 119 | inputs = eventArgs.command.commandInputs 120 | selectInput: adsk.core.SelectionCommandInput = inputs.itemById('selected-1') 121 | selection: adsk.core.Selection = selectInput.selection(0) 122 | entity: adsk.fusion.Occurrence = selection.entity 123 | 124 | try: 125 | 126 | # if self.des: 127 | self.flatten_occ(entity) 128 | message = self.app.userInterface.messageBox( 129 | "Finished flatten occurrence", 130 | "ACDC4Robot Tool" 131 | ) 132 | except: 133 | if self.ui: 134 | self.ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) 135 | 136 | # Force the termination of the command. 137 | adsk.terminate() 138 | 139 | 140 | def stop(context): 141 | try: 142 | app = adsk.core.Application.get() 143 | ui = app.userInterface 144 | 145 | # Delete the command definition. 146 | cmdDef = ui.commandDefinitions.itemById('ACDC4Robot-Tool-FlattenOccs') 147 | if cmdDef: 148 | cmdDef.deleteMe() 149 | except: 150 | if ui: 151 | ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACDC4Robot: Automated Conversion of Description Conventions for Robots from Design to Learning 2 | 3 | 15 | 16 | ## Introduction 17 | Robot description format (RDF) contains information about robot model which is required by simulation, visualization, planning etc. In this project, we provide a Fusion360 Add-In for generating robot description files automatically from robot design. 18 | 19 | Currently, this Add-In supports exporting URDF, SDFormat, and MJCF. 20 | URDF (Unified Robotics Description Format) has been the most widely used robot description format, but has several limitations and lack of update. 21 | SDFormat (Simulation Description Format) has more features than URDF, such as supporting closed loop chain mechanism. SDFormat has been a seperated project from Gazebo aims to be a simulator indenpendt format but still not as popular as URDF. 22 | MJCF is a robot description format used in simulator MuJoCo and has been support by more simulators such as Nvidia Isaac Sim. It also has more features then URDF to provide more robotic system information. 23 | Other robot description formats might be supported in the future. 24 | 25 | Also, we provide a Fusion 360 robot model library that can be used out-of-box for design, simulation, and learning. 26 | 👉[🤖Fusion 360 Robot Library](RobotLibrary.md) 27 | 28 | ## Key Features 29 | Export robot description files from Fusion360 design file directly with GUI panel. 30 | 31 | 35 | - Supported robot description formats: 36 | - [URDF](http://wiki.ros.org/urdf/XML) (Unified Robotics Description Format) 37 | - [SDFormat](http://sdformat.org/spec) (Simulation Description Format) or SDF 38 | - [MJCF](https://mujoco.readthedocs.io/en/latest/XMLreference.html) (MuJoCo Format) 39 | - An [robot model library](RobotLibrary.md) including various robot types: 40 | - Robot Arm 41 | - Gripper 42 | - Mobile Robot 43 | - Quadruped Robot 44 | - Humanoid 45 | 46 | ## Installation 47 | You can install this Add-In form Autodesk Fusion app store or manually. 48 | 49 | ### Installation form Autodesk App Store 50 | You can download [ACDC4Robot](https://apps.autodesk.com/FUSION/en/Detail/Index?id=5028052292896011577) from Autodesk Fusion App Store, Windows and Mac version are provided. 51 | 52 | Due to the review process of app store, ACDC4Robot at app store might not update in the same frequence as this repository. So updates such as new features and bug fix might be late for ACDC4Robot app store version. 53 | 54 | ### Manually Installation 55 | Download and copy `/Add-IN/ACDC4Robot` folder into Fusion360's Add-in directory which can be found at `Preferences -> General -> API -> Default Path for Scripts and Add-Ins`. 56 | 57 | In default it should be at: 58 | 59 | Windows: 60 | ``` 61 | %appdata%\Autodesk\Autodesk Fusion 360\API\AddIns 62 | ``` 63 | 64 | Mac: 65 | ``` 66 | $HOME/Library/Application Support/Autodesk/Autodesk Fusion 360/API/AddIns 67 | ``` 68 | or it can be found at `Preferences -> General -> API -> Default Path for Scripts and Add-Ins`. 69 | 70 | 82 | 83 | ### First Run 84 | After installation for the first time, use `Shift+S` or click `UTILITIES -> Add-Ins -> Scripts and Add-Ins` to open `Scripts and Add-Ins` window. 85 | 86 | Find `Fusion2Robot` at `Add-Ins -> My Add-Ins`, select `Fusion2Robot` and click `Run`(for normally use, please select `Run on Startup`). Then the icon will appear beside `UTILITIES -> Add-Ins icon`. 87 | ![Run the Add-In](./pictures/RunAdd-In.gif) 88 | Click the icon to start exporting process from the current design. 89 | 90 | 91 | ## Usage 92 | ### Before Using This Add-In 93 | Before exporting robot description files, please follow the following instructions to make sure the design file is suitable to execute this add-in. 94 | 95 | To prevent unexpected modification of the original design, it is better to run this add-in in a copy of the design file. 96 | 97 | - Exit parametric mode: right click the root component of the design, choose `Do not capture Design History` 98 | ![Do not capture Design History](./pictures/DoNotCaptureDesignHistory.PNG) 99 | - Set the default unit of the design document to `m` 100 | ![Change Units](./pictures/ChangeUnits.png) 101 | - Using `Break Link` to make *external component* into *internal component* 102 | ![Break Link](./pictures/BreakLink.gif) 103 | - Occurrences refer to the same component are not supported yet, use `Make Independent` to independ those occurrences. 104 | ![Make Independent](./pictures/MakeIndependent.gif) 105 | - - Make sure all the components are named with alphanumeric characters, underscore character `_`, and hyphe character `-`. Other languages and characters might cause problems. 106 | - - Components need to be joint together with a **flat** structure in order to export the robot description files properly. Please check the [instructions for Fusion360 Assembly](./InstructionsForAssembly.md). 107 | 108 | ### After Setting Up Design File 109 | Click the add-in icon, then chose the robot description format and targeted simulation platform to export. 110 | ![Execute Fusion 360 Add-In](./pictures/ExcuteAdd-In.gif) 111 | 112 | ## Tested Examples 113 | ### Closed Chain Linkages 114 | - Test a closed loop linkages in Gazebo to show the ability of SDFormat to describe a closed-chain mechanism 115 | ![Test Four Bar Linkages](./pictures/Four-Bar-Linkages-Test.png) 116 | 117 | ### Robot Manipulator: UR5e 118 | - Test a UR5e manipulator in Gazebo 119 | ![Test UR5e manipulator](./pictures/UR5e-Test.png) 120 | 121 | ### Robot Gripper: Robotiq-2F85-Gripper 122 | - Test Robotiq-2F85 Gripper in Gazebo 123 | ![Test Robotiq-2F85 Gripper](./pictures//Robotiq-Gripper-Test.png) 124 | 125 | ## Robot Library 126 | [🤖Fusion 360 Robot Library](RobotLibrary.md) 127 | 128 | We also provides some robot models that assembled manually, and tested the Add-In with these models. 129 | This robot library is continuing update, it is welcome to contribute this library. 130 | 131 | ## Citation 132 | For BibTex: 133 | ``` 134 | @INPROCEEDINGS{10715835, 135 | author={Qiu, Nuofan and Song, Chaoyang and Wan, Fang}, 136 | booktitle={2024 International Conference on Advanced Robotics and Mechatronics (ICARM)}, 137 | title={Describing Robots from Design to Learning: Towards an Interactive Lifecycle Representation of Robots}, 138 | year={2024}, 139 | volume={}, 140 | number={}, 141 | pages={1081-1086}, 142 | keywords={Solid modeling;Mechatronics;Codes;Automation;Pipelines;Morphology;Transforms;Software;Libraries;Robots}, 143 | doi={10.1109/ICARM62033.2024.10715835}} 144 | ``` 145 | 146 | Plain Text: 147 | ``` 148 | N. Qiu, C. Song and F. Wan, "Describing Robots from Design to Learning: Towards an Interactive Lifecycle Representation of Robots," 2024 International Conference on Advanced Robotics and Mechatronics (ICARM), Tokyo, Japan, 2024, pp. 1081-1086, doi: 10.1109/ICARM62033.2024.10715835. keywords: {Solid modeling;Mechatronics;Codes;Automation;Pipelines;Morphology;Transforms;Software;Libraries;Robots}, 149 | ``` 150 | 151 | ## Reference 152 | - [Fusion2PyBullet](https://github.com/yanshil/Fusion2PyBullet) 153 | - [fusion2urdf](https://github.com/syuntoku14/fusion2urdf) -------------------------------------------------------------------------------- /MethodExplanation.md: -------------------------------------------------------------------------------- 1 | # Method Explanation 2 | Fusion 360 API provides a unified way to return informations that can be used in URDF and SDFormat. All the coordinates, vectors is represented w.r.t world coordinate, which is the coordinate of the root component. 3 | Thus, to make all these information is suitable for robot description format, some coordinates transformations are necessory. 4 | 5 | ## Fusion 360 Assembly Structure 6 | Fusion 360 API provides the ability to access design model kinematics and dynamics information that is necessay for robot description format. All the elements are represented w.r.t root component frame (world frame). 7 | One of the benefits of this way to define elements is that information only depends on the definition of world frame, does not rely on other elements such as parent link. 8 | This decoupled way reduces constraints in the modelling process. 9 | 10 | ![Fusion 360 model structure](./pictures/Fusion360-Link-Joint-Model.png) 11 | 12 | ## URDF Structure 13 | URDF (Unified Robot Description Format) is a xml-based format to describe a robot. It is the most widely-used robot description format and supported by plentiful simulators. Although [URDF's specifications](http://wiki.ros.org/urdf/XML) has been expanded with abilities to model sensors and describe robot state, it is mostly used with `joint` and `link` elements in a tree structure. 14 | ![URDF-Link-Joint-Model](./pictures/URDF-Link-Joint-Model.png) 15 | 16 | Basic elements that match with a Fusion360 design are listed below: 17 | - ``: the root element of a robot, with the required `name` attribute 18 | - ``: defines the properties of a robot link which is a rigid body. Has required `name` attribute 19 | - ``: defines the dynamics information about the link 20 | - ``: defines the link's CoM frame *C* w.r.t the link-frame *L* which is the same as the joint-frame *J* which is defined in `` element by `xyz` and `rpy` attributes 21 | - ``: defines the mass of the link by `value` attribute 22 | - ``: defines the inertia tensor of the link w.r.t the CoM frame *C* by `ixx`, `iyy`, `izz`, `izy`, `ixz`, and `iyz` attributes 23 | - ``: defines the visual properties of the link. Multiple instances of `` elements can exist for the same link that causes the union of the geometry represents the link's visual geometry 24 | - ``: defines the visual element's frame w.r.t the link frame *L* by `xyz` and `rpy` attributes 25 | - ``: defines the shape of the visual object, usually uses the `` sub-element to define. 26 | - ``: defines the collision properties of the link with similar conventions to `` element 27 | - ``: defines the collision element's frame w.r.t the link frame *L* by `xyz` and `rpy` attributes 28 | - ``: defines the shape of the collision object, usually uses the `` sub-element to define. 29 | - ``: defines the kinematics and dynamics properties of a joint, requires `name` and `type` attributes 30 | - ``: defines the transform from the parent link to the child link by `xyz` and `rpy` attributes and the joint is located at the child link 31 | - ``: specify the joint axis with a normalized vector in joint frame 32 | - ``: the parent link which is defined by `link` attribute using the link name 33 | - ``: the child link which is defined by `link` attribute using the link name 34 | - ``: defines the joint limitations by `lower`, `upper`, `effort`, and `velocity` attributes 35 | 36 | URDF defines child link(joint) information relied on parent link information, this makes information representation in URDF concise, but creates constrains to model robot using URDF. 37 | 38 | ## SDF Structure 39 | SDFormat (Simulation Description format) is a xml-based format that can contain information not only about robots but also about environments. Its [specifications](http://sdformat.org/spec) are similar to URDF but with more abilities. Here we focus on the element `` which mostly represents the robot. 40 | 41 | ![SDF-Link-Joint-Model](./pictures/SDF-Link-Joint-Model.png) 42 | 43 | Although the `` element of SDFormat has more sub-elements to describe a robot, here we only introduce basic elements that can match a Fusion360 design. 44 | - ``: model element is used to define a complete robot or any other physical object. The `name` attribute is required. 45 | - ``: represents robot's link inside `` element with the required `name` attribute. 46 | - ``: defines the link-frame *L* **w.r.t the parent element** which is the `` in a ``, ``, ``, ``, `

`, `` form 47 | - ``: defines the dynamics information about the link 48 | - ``: the mass of the link 49 | - ``: defines the link's center-of-mass-frame *C* w.r.t the link-frame *L* 50 | - ``: defines the inertia tensor in center-of-mass-frame *C* 51 | - ``: the link's moment of inertia about the CoM frame *C*'s x-axis 52 | - ``: the link's product of inertia about CoM frame's x-axis and y-axis 53 | - ``: the link's product of inertia about CoM frame's x-axis and z-axis 54 | - ``: the link's moment of inertia about the CoM frame *C*'s y-axis 55 | - ``: the link's product of inertia about CoM frame's y-axis and z-axis 56 | - ``: the link's moment of inertia about the CoM frame *C*'s z-axis 57 | - ``: defines collision geometry of the link with required `name` attribute 58 | - ``: defines visual properties of the link with required `name` attribute 59 | - ``: defines a joint with required `name` and `type` attributes 60 | - ``: defines the name of the parent frame (conventionally the parent link frame) or "world" 61 | - ``: defines the name of the child frame (conventionally the child link frame) 62 | - ``: defines the axis for joint motion 63 | - ``: expressed the axis with a unit vector in `` `` `` form w.r.t the joint frame *J* 64 | - ``: defines the limits of the joint with this axis 65 | - ``: lower joint limit (radians for revolute joints, meters for prismatic joints) 66 | - ``: upper joint limit (radians for revolute joints, meters for prismatic joints) 67 | - ``: the second axis for some special joint type 68 | - ``: expressed the axis with a unit vector in `` `` `` form w.r.t the joint frame *J* 69 | - ``: defines the limits of the joint with this axis 70 | - ``: lower joint limit (radians for revolute joints, meters for prismatic joints) 71 | - ``: upper joint limit (radians for revolute joints, meters for prismatic joints) 72 | - ``: defines the joint frame *J*. By default, the joint frame is expressed in the child link frame 73 | 74 | ## Fusion 360 to URDF 75 | | URDF Element | Fusion 360 Elements | Transformation | 76 | | ------------ | ------------------- | -------------- | 77 | | Joint *i* origin $^{L_{i-1}}T_{J_{i}}$| $^{W}T_{L_{i-1}}$, $^{W}T_{J_{i}}$ | $^{L_{i-1}}T_{J_{i}} = {(^{W}T_{L_{i-1}})}^{T} \cdot {^{W}T_{J_{i}}}$ | 78 | | Joint *i* axis $^{J_{i}}\vec{a}$| $^{W}\vec{a}$, $^{W}T_{J_{i}}$| $^{J_{i}}\vec{a} = {(^{W}T_{J_{i}})}^{T} \cdot ^{W}\vec{a}$ | 79 | | Link *i*'s inertial origin $^{J_{i}}{T_{L_{i}CoM}}$ | $^{W}T_{J_{i}}$, $^{W}{T_{L_{i}CoM}}$ | $^{J_{i}}{T_{L_{i}CoM}} = {(^{W}T_{J_{i}})^{T}} \cdot {^{W}{T_{L_{i}CoM}}}$ | 80 | |Link *i*'s visual origin $^{J_{i}}T_{L_{i}}$ | $^{W}T_{J_{i}}$, $^{W}T_{L_{i}}$ | $^{J_{i}}T_{L_{i}} = {(^{W}T_{J_{i}})^{T}} \cdot {^{W}T_{L_{i}}}$ | 81 | |Link *i*'s collision origin $^{J_{i}}T_{L_{i}}$ | $^{W}T_{J_{i}}$, $^{W}T_{L_{i}}$ | $^{J_{i}}T_{L_{i}} = {(^{W}T_{J_{i}})^{T}} \cdot {^{W}T_{L_{i}}}$ | 82 | 83 | ## Fusion 360 to SDFormat 84 | | SDF Element | Fusion 360 Elements | Transformation | 85 | | ----------- | ------------------- | -------------- | 86 | | Joint *i* pose $^{L_{i}}T_{J_{iC}}$ | $^{W}T_{L_{i}}$, $^{W}T_{J_{iC}}$ | $^{L_{i}}T_{J_{iC}} = {(^{W}T_{L_{i}})^{T}} \cdot {^{W}T_{J_{iC}}}$| 87 | | Joint *i* axis1 $^{J_{iC}}\vec{a_1}$| $^{W}\vec{a_1}$, $^{W}T_{J_{iC}}$ | $^{J_{iC}}\vec{a_1} = {(^{W}T_{J_{iC}})^{T}} \cdot {^{W}\vec{a_1}}$ | 88 | | Joint *i* axis1 $^{J_{iC}}\vec{a_2}$| $^{W}\vec{a_2}$, $^{W}T_{J_{iC}}$ | $^{J_{iC}}\vec{a_2} = {(^{W}T_{J_{iC}})^{T}} \cdot {^{W}\vec{a_2}}$ | 89 | | Link *i*'s pose $^{W}T_{L_{i}}$ | $^{W}T_{L_{i}}$ | $^{W}T_{L_{i}} = {^{W}T_{L_{i}}}$ | 90 | | Link *i*'s inertial pose $^{L_{i}}T_{L_{i}COM}$ | $^{W}T_{L_{i}}$, $^{W}{T_{L_{i}CoM}}$ | $^{L_{i}}T_{L_{i}COM} = {(^{W}T_{L_{i}})^{T}} \cdot {^{W}{T_{L_{i}CoM}}}$ | 91 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/math_operation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Nuofan - 12233200@mail.sustech.edu.cn 3 | """ 4 | contains functions for math operations, 5 | including matrix operations, spacial mathematics, etc 6 | """ 7 | import adsk, adsk.core, adsk.fusion 8 | import math 9 | 10 | def matrix3d_2_pose(matrix: adsk.core.Matrix3D): 11 | """ 12 | Convert configuration matrix (Matrix3D) into pose(translation, rotation) representation 13 | 14 | Parameter: 15 | --------- 16 | matrix: adsk.core.Matric3D 17 | Homogeneous matrix 18 | 19 | Return: 20 | --------- 21 | pose: List 22 | [x, y, z, roll, pitch, yaw] 23 | This euler angle convertion is "XYZ" 24 | """ 25 | x = matrix.translation.x * 0.01 # cm to m 26 | y = matrix.translation.y * 0.01 # cm to m 27 | z = matrix.translation.z * 0.01 # cm to m 28 | 29 | threshold = 10e-10 30 | check = lambda v: v if abs(v) > threshold else 0.0 31 | 32 | r11 = matrix.getCell(0, 0) 33 | r21 = matrix.getCell(1, 0) 34 | r31 = matrix.getCell(2, 0) 35 | r32 = matrix.getCell(2, 1) 36 | r33 = matrix.getCell(2, 2) 37 | r23 = matrix.getCell(1, 2) 38 | r22 = matrix.getCell(1, 1) 39 | sy = math.sqrt(r11**2 + r21**2) 40 | singular = sy < 1e-6 41 | 42 | if not singular: 43 | roll = math.atan2(r32, r33) 44 | pitch = math.atan2(-r31, sy) 45 | yaw = math.atan2(r21, r11) 46 | else: 47 | roll = math.atan2(-r23, r22) 48 | pitch = math.atan2(-r31, sy) 49 | yaw = 0 50 | 51 | pose = [x, y, z, roll, pitch, yaw] 52 | return pose 53 | 54 | def matrix3d_2_euler_xyz(matrix: adsk.core.Matrix3D): 55 | """ 56 | Convert configuration matrix (Matrix3D) into pose(translation, rotation) representation 57 | 58 | Parameter: 59 | --------- 60 | matrix: adsk.core.Matric3D 61 | Homogeneous matrix 62 | 63 | Return: 64 | --------- 65 | pose: List 66 | [x, y, z, roll, pitch, yaw] 67 | This euler angle convertion is "xyz" 68 | """ 69 | 70 | # TODO: Still needs to check if it is right 71 | # Some problem seems related with this function 72 | x = matrix.translation.x * 0.01 # cm to m 73 | y = matrix.translation.y * 0.01 # cm to m 74 | z = matrix.translation.z * 0.01 # cm to m 75 | 76 | threshold = 10e-10 77 | check = lambda v: v if abs(v) > threshold else 0.0 78 | 79 | r11 = matrix.getCell(0, 0) 80 | r12 = matrix.getCell(0, 1) 81 | r13 = matrix.getCell(0, 2) 82 | r22 = matrix.getCell(1, 1) 83 | r23 = matrix.getCell(1, 2) 84 | r32 = matrix.getCell(2, 1) 85 | r33 = matrix.getCell(2, 2) 86 | 87 | sy = math.sqrt(r33**2 + r23**2) 88 | singular = sy < 1e-6 89 | 90 | if not singular: 91 | roll = math.atan2(-r23, r33) 92 | pitch = math.atan2(r13, sy) 93 | yaw = math.atan2(-r12, r11) 94 | else: 95 | roll = math.atan2(-r32, r22) 96 | pitch = math.atan2(r13, sy) 97 | yaw = 0 98 | 99 | pose = [x, y, z, roll, pitch, yaw] 100 | return pose 101 | 102 | def coordinate_transform(w_T_from: adsk.core.Matrix3D, w_T_to: adsk.core.Matrix3D) -> adsk.core.Matrix3D: 103 | """ 104 | Returns a transform matrix to transform a from_frame to to_frame 105 | Parameters 106 | --------- 107 | w_T_from: the from_frame 108 | w_T_to: the to_frame 109 | Return 110 | --------- 111 | from_T_to: adsk.core.Matrix3D 112 | represent to_frame w.r.t from_frame 113 | w_T_from * from_T_to = w_T_to -> 114 | from_T_to = inv(w_T_from) * w_T_to 115 | """ 116 | from_T_to = adsk.core.Matrix3D.create() 117 | w_T_from.invert() # from_T_w 118 | 119 | for i in range(4): 120 | for j in range(4): 121 | value = 0 122 | for k in range(4): 123 | value += w_T_from.getCell(i, k) * w_T_to.getCell(k, j) 124 | from_T_to.setCell(i, j, value) 125 | 126 | return from_T_to 127 | 128 | def change_orientation(a_R_b: list, b_v: list): 129 | """ 130 | Rotate vector v to a different orientation or 131 | Represent vector v in a different frame 132 | 133 | Parameters 134 | --------- 135 | a_R_b: 3*3 array 136 | represent b-frame's orientation w.r.t a-frame 137 | b_v: 3*1 array 138 | v's coordinates w.r.t b-frame 139 | Return: 140 | --------- 141 | a_v: 3*1 array 142 | v's coordinates w.r.t a-frame 143 | """ 144 | a_v = [[a_R_b[0][0]*b_v[0][0]+a_R_b[0][1]*b_v[1][0]+a_R_b[0][2]*b_v[2][0]], 145 | [a_R_b[1][0]*b_v[0][0]+a_R_b[1][1]*b_v[1][0]+a_R_b[1][2]*b_v[2][0]], 146 | [a_R_b[2][0]*b_v[0][0]+a_R_b[2][1]*b_v[1][0]+a_R_b[2][2]*b_v[2][0]]] 147 | 148 | return a_v 149 | 150 | def matrix3D_to_str(M: adsk.core.Matrix3D) -> str: 151 | """ 152 | Return a str for printing Matrix3D object 153 | """ 154 | s = "[{}, {}, {}, {}\n \ 155 | {}, {}, {}, {}\n \ 156 | {}, {}, {}, {}\n \ 157 | {}, {}, {}, {}\n]".format(M.getCell(0,0), M.getCell(0,1), M.getCell(0,2), M.getCell(0,3), 158 | M.getCell(1,0), M.getCell(1,1), M.getCell(1,2), M.getCell(1,3), 159 | M.getCell(2,0), M.getCell(2,1), M.getCell(2,2), M.getCell(2,3), 160 | M.getCell(3,0), M.getCell(3,1), M.getCell(3,2), M.getCell(3,3)) 161 | 162 | return s 163 | 164 | def matrix_multi(M1: list, M2: list): 165 | """ 166 | Return M = M1 * M2 167 | 168 | Parameters: 169 | --------- 170 | M1: 3*3 list 171 | M2: 3*3 list 172 | 173 | Return: 174 | --------- 175 | M: 3*3 list 176 | """ 177 | M = [[0 for i in range(3)] for j in range(3)] # generate a 3*3 list 178 | 179 | for i in range(3): 180 | for j in range(3): 181 | for k in range(3): 182 | M[i][j] += M1[i][k]*M2[k][j] 183 | 184 | return M 185 | 186 | def get_rotation_matrix(H: adsk.core.Matrix3D): 187 | """ 188 | Get rotation matrix R from Matrix3D object 189 | 190 | Parameters: 191 | --------- 192 | H: adsk.core.Matrix3D 193 | 194 | Return: 195 | --------- 196 | R: 3*3 list 197 | Rotation matrix 198 | """ 199 | R = [[H.getCell(0, 0), H.getCell(0, 1), H.getCell(0, 2)], 200 | [H.getCell(1, 0), H.getCell(1, 1), H.getCell(1, 2)], 201 | [H.getCell(2, 0), H.getCell(2, 1), H.getCell(2, 2)]] 202 | 203 | return R 204 | 205 | def matrix_transpose(M: list): 206 | """ 207 | Return transpose of a matrix 208 | 209 | Parameter: 210 | --------- 211 | M: 3*3 list 212 | 213 | Return: 214 | --------- 215 | M_T: 3*3 list 216 | Transpose of M 217 | """ 218 | M_T = [[0 for i in range(3)] for j in range(3)] # generate a 3*3 list 219 | 220 | for i in range(3): 221 | for j in range(3): 222 | M_T[i][j] = M[j][i] 223 | 224 | return M_T 225 | 226 | def matrix_multiply(A, B): 227 | # Matrix multiplication of A and B 228 | result = [[sum(A[i][k] * B[k][j] for k in range(len(B))) for j in range(len(B[0]))] for i in range(len(A))] 229 | return result 230 | 231 | def matrix_subtract(A, B): 232 | # Matrix subtraction of A and B 233 | result = [[A[i][j] - B[i][j] for j in range(len(A[0]))] for i in range(len(A))] 234 | return result 235 | 236 | def determinant_2x2(matrix): 237 | # Calculate determinant of a 2x2 matrix 238 | return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0] 239 | 240 | def determinant_3x3(matrix): 241 | # Calculate determinant of a 3x3 matrix 242 | det = matrix[0][0] * determinant_2x2([[matrix[1][1], matrix[1][2]], [matrix[2][1], matrix[2][2]]]) 243 | det -= matrix[0][1] * determinant_2x2([[matrix[1][0], matrix[1][2]], [matrix[2][0], matrix[2][2]]]) 244 | det += matrix[0][2] * determinant_2x2([[matrix[1][0], matrix[1][1]], [matrix[2][0], matrix[2][1]]]) 245 | return det 246 | 247 | def eigenvalues_and_eigenvectors(matrix): 248 | # Finding eigenvalues and eigenvectors of a 3x3 matrix 249 | detA = determinant_3x3(matrix) 250 | A_minus_lambdaI = matrix_subtract(matrix, [[detA, 0, 0], [0, detA, 0], [0, 0, detA]]) 251 | 252 | eigenvalues = [] 253 | eigenvectors = [] 254 | 255 | # Finding the eigenvalues 256 | eigenvalues.append(detA - matrix[0][0]) 257 | eigenvalues.append(detA - matrix[1][1]) 258 | eigenvalues.append(detA - matrix[2][2]) 259 | 260 | # Finding the eigenvectors for each eigenvalue 261 | for eig_val in eigenvalues: 262 | A_minus_lambdaI[0][0] = matrix[0][0] - eig_val 263 | A_minus_lambdaI[1][1] = matrix[1][1] - eig_val 264 | A_minus_lambdaI[2][2] = matrix[2][2] - eig_val 265 | 266 | # Solve the linear system (A-lambdaI)v = 0 267 | v = [0, 0, 0] 268 | if A_minus_lambdaI[0][0] == 0 and A_minus_lambdaI[0][1] == 0 and A_minus_lambdaI[0][2] == 0: 269 | v[0] = 1 270 | elif A_minus_lambdaI[1][0] == 0 and A_minus_lambdaI[1][1] == 0 and A_minus_lambdaI[1][2] == 0: 271 | v[1] = 1 272 | elif A_minus_lambdaI[2][0] == 0 and A_minus_lambdaI[2][1] == 0 and A_minus_lambdaI[2][2] == 0: 273 | v[2] = 1 274 | else: 275 | v[0] = -(A_minus_lambdaI[0][1]*A_minus_lambdaI[1][2] - A_minus_lambdaI[0][2]*A_minus_lambdaI[1][1]) 276 | v[1] = A_minus_lambdaI[0][0]*A_minus_lambdaI[1][2] - A_minus_lambdaI[0][2]*A_minus_lambdaI[1][0] 277 | v[2] = -(A_minus_lambdaI[0][0]*A_minus_lambdaI[1][1] - A_minus_lambdaI[0][1]*A_minus_lambdaI[1][0]) 278 | 279 | # Normalize the eigenvector 280 | magnitude = (v[0]**2 + v[1]**2 + v[2]**2)**0.5 281 | eigenvectors.append([v[0] / magnitude, v[1] / magnitude, v[2] / magnitude]) 282 | 283 | return eigenvalues, eigenvectors -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/mjcf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Nuofan - 12233200@mail.sustech.edu.cn 3 | # Date: 20231124 4 | # Functions for generating mjcf file 5 | 6 | import adsk, adsk.core, adsk.fusion 7 | from .link import Link 8 | from .joint import Joint 9 | import xml.etree.ElementTree as ET 10 | from xml.etree.ElementTree import Element, SubElement 11 | from . import math_operation as math_op 12 | from . import utils 13 | from ..commands.ACDC4Robot import constants 14 | 15 | def get_mjcf_mesh(link: Link) -> Element: 16 | """ 17 | Get mesh element for mjcf 18 | 19 | Return 20 | --------- 21 | mesh_ele: Element 22 | mesh element in mjcf 23 | """ 24 | mesh_ele = ET.Element("mesh") 25 | name: str = link.get_name() 26 | file_name: str = "meshes/" + link.get_name() + ".stl" 27 | mesh_ele.attrib = {"name":name, "file": file_name, "scale": "0.001 0.001 0.001"} 28 | return mesh_ele 29 | 30 | def get_mjcf_body(link: Link, parent_link: Link = None) -> Element: 31 | """ 32 | Get body element for mjcf 33 | 34 | Return 35 | --------- 36 | body_ele: Element 37 | body element represent a link in mjcf 38 | """ 39 | body_ele = Element("body") 40 | body_name: str = link.get_name() 41 | if parent_link is None: 42 | # a root of a body tree 43 | # pose = math_op.matrix3d_2_euler_xyz(link.pose) 44 | pose = math_op.matrix3d_2_pose(link.pose) 45 | pos_attr = "{} {} {}".format(pose[0], pose[1], pose[2]) 46 | euler_attr = "{} {} {}".format(pose[3], pose[4], pose[5]) 47 | else: 48 | parent_frame: adsk.core.Matrix3D = parent_link.pose # parent_frame w.r.t world_frame 49 | child_frame: adsk.core.Matrix3D = link.pose # child_frame w.r.t world_frame 50 | parent_T_child: adsk.core.Matrix3D = math_op.coordinate_transform(parent_frame, child_frame) 51 | # pose = math_op.matrix3d_2_euler_xyz(parent_T_child) 52 | pose = math_op.matrix3d_2_pose(parent_T_child) 53 | pos_attr = "{} {} {}".format(pose[0], pose[1], pose[2]) 54 | euler_attr = "{} {} {}".format(pose[3], pose[4], pose[5]) 55 | 56 | body_ele.attrib = {"name": body_name, "pos": pos_attr, "euler": euler_attr} 57 | 58 | # insert parent joint if it exists 59 | parent_joint = link.get_parent_joint() 60 | if parent_joint is not None: 61 | joint_ele: Element = get_mjcf_joint(Joint(parent_joint)) 62 | # undifined joint will welded two body 63 | if joint_ele is not None: 64 | body_ele.append(joint_ele) 65 | 66 | geom_ele: Element = get_mjcf_geom(link) 67 | inertial_ele: Element = get_mjcf_inertial(link) 68 | 69 | body_ele.append(geom_ele) 70 | body_ele.append(inertial_ele) 71 | 72 | return body_ele 73 | 74 | def get_mjcf_geom(link: Link) -> Element: 75 | """ 76 | Get geom element for mjcf 77 | 78 | Return 79 | --------- 80 | geom_ele: Element 81 | geom element in mjcf 82 | """ 83 | geom_ele = Element("geom") 84 | geom_name = link.get_name() + "_geom" 85 | # geom_pos = "{} {} {}".format(link.get_pose_sdf()[0], link.get_pose_sdf()[1], link.get_pose_sdf()[2]) 86 | # geom_euler = "{} {} {}".format(link.get_pose_sdf()[3], link.get_pose_sdf()[4], link.get_pose_sdf()[5]) 87 | 88 | # geom should coincide with the body frame? It works 89 | geom_pos = "0 0 0" 90 | geom_euler = "0 0 0" 91 | 92 | visual_body = link.get_visual_body() 93 | col_body = link.get_collision_body() 94 | if (visual_body is None) and (col_body is None): 95 | geom_ele.attrib = {"name": geom_name, "type": "mesh", 96 | "mesh": link.get_name(), "pos": geom_pos, "euler": geom_euler} 97 | elif (visual_body is not None) and (col_body is not None): 98 | error_message = "mjcf will automatically generate geometry for collision. \n" 99 | error_message = error_message + link.get_name() + " does not need to set geometry for visual and collision seperately." 100 | utils.error_box(error_message) 101 | utils.terminate_box() 102 | elif (visual_body is None) and (col_body is not None): 103 | error_message = "mjcf will automatically generate geometry for collision. \n" 104 | error_message = error_message + link.get_name() + " does not need to set geometry for visual and collision seperately." 105 | utils.error_box(error_message) 106 | utils.terminate_box() 107 | elif (visual_body is not None) and (col_body is None): 108 | error_message = "mjcf will automatically generate geometry for collision. \n" 109 | error_message = error_message + link.get_name() + " does not need to set geometry for visual and collision seperately." 110 | utils.error_box(error_message) 111 | utils.terminate_box() 112 | 113 | return geom_ele 114 | 115 | 116 | def get_mjcf_inertial(link: Link) -> Element: 117 | """ 118 | Get inertial element for mjcf 119 | 120 | Return 121 | --------- 122 | inertial_ele: Element 123 | inertial element in mjcf 124 | """ 125 | inertial_ele = Element("inertial") 126 | mass: str = str(link.get_mass()) 127 | CoM: list = link.get_CoM_wrt_link() 128 | pos_att: str = "{} {} {}".format(CoM[0], CoM[1], CoM[2]) 129 | # get error for euler attribute in inertial, so remove it 130 | # euler_att: str = "{} {} {}".format(CoM[3], CoM[4], CoM[5]) 131 | inertial: list = link.get_inertia_mjcf() 132 | # 6 numbers in the following order: M(1,1), M(2,2), M(3,3), M(1,2), M(1,3), M(2,3). 133 | # which is ixx, iyy, izz, ixy, ixz, iyz 134 | fullinertia_att = "{} {} {} {} {} {}".format(inertial[0], inertial[1], inertial[2], 135 | inertial[3], inertial[4], inertial[5]) 136 | # inertial_ele.attrib = {"mass": mass, "pos": pos_att, 137 | # "euler": euler_att, "fullinertia": fullinertia_att} 138 | inertial_ele.attrib = {"mass": mass, "pos": pos_att, 139 | "fullinertia": fullinertia_att} 140 | 141 | return inertial_ele 142 | 143 | def get_mjcf_joint(joint: Joint) -> Element: 144 | """ 145 | Get joint element for mjcf 146 | A joint creates motion degrees of freedom 147 | between the body where it is defined and the body's parent. 148 | 149 | Return 150 | --------- 151 | joint_ele: Element 152 | joint element in mjcf 153 | """ 154 | # If no joints are defined, the body is welded to its parent. 155 | joint_type = joint.get_mjcf_joint_type() 156 | if joint_type is not None: 157 | joint_ele: Element = ET.Element("joint") 158 | name_att = joint.get_name() 159 | pose = joint.get_sdf_origin() 160 | axis = joint.get_axis_mjcf() 161 | joint_ele.attrib = {"name": name_att, "type": joint_type, 162 | "axis": "{} {} {}".format(axis[0], axis[1], axis[2]), 163 | "pos": "{} {} {}".format(pose[0], pose[1], pose[2])} 164 | return joint_ele 165 | else: 166 | return None 167 | 168 | 169 | def get_mjcf(root_comp: adsk.fusion.Component, robot_name: str, dir: str) -> Element: 170 | """ 171 | Get a mjcf format xml file contains robot model 172 | 173 | Parameters: 174 | root_comp: root_comp of the design 175 | robotName: robot name 176 | dir: directory of the mjcf file 177 | 178 | Return: 179 | mujoco_ele: Element 180 | root element of a mujoco xml file 181 | """ 182 | root = ET.Element("mujoco", {"model": robot_name}) 183 | 184 | # add compiler subelement of mujoco 185 | # the default eulerseq conventioin used in URDF corresponds to the default “xyz” in MJCF 186 | # Here we use an extrinsic X-Y-Z rotation 187 | # compiler_ele = ET.SubElement(root, "compiler", 188 | # {"angle": "radian", "meshdir": (dir + "/meshes"), "eulerseq":"XYZ"}) 189 | # Rotation matrix to euler in "xyz" seems have problem, use "XYZ" at temporary 190 | compiler_ele = ET.SubElement(root, "compiler", 191 | {"angle": "radian", "eulerseq":"XYZ"}) 192 | 193 | # add asset subelement of mujoco 194 | asset_ele = ET.SubElement(root, "asset") 195 | 196 | # add worldbody subelement of mujoco 197 | worldbody_ele = ET.SubElement(root, "worldbody") 198 | 199 | # add light subelement of worldbody 200 | light_ele = ET.SubElement(worldbody_ele, "light") 201 | light_ele.attrib = {"directional":"true", "pos":"-0.5 0.5 3", "dir":"0 0 -1"} 202 | 203 | # add floor 204 | floor = ET.SubElement(worldbody_ele, "geom", ) 205 | floor.attrib = {"pos": "0 0 0", "size": "1 1 1", "type": "plane", "rgba": "1 0.83 0.61 0.5"} 206 | 207 | # add body elements to construct a robot 208 | parent_child_dict = {} 209 | joints = root_comp.allJoints 210 | all_occs = root_comp.allOccurrences 211 | 212 | for joint in joints: 213 | parent = joint.occurrenceTwo 214 | child = joint.occurrenceOne 215 | 216 | if parent.fullPathName not in parent_child_dict: 217 | parent_child_dict[parent.fullPathName] = [] 218 | 219 | parent_child_dict[parent.fullPathName].append(child) 220 | 221 | # Function to recursively add body elements to mjcf 222 | def add_body_element(parent: adsk.fusion.Occurrence, parent_body_ele: Element) -> Element: 223 | children = parent_child_dict.get(parent.fullPathName, []) 224 | for child_occ in children: 225 | # Create mjcf element for child 226 | child_link = Link(child_occ) 227 | parent_link = Link(parent) 228 | child_ele = get_mjcf_body(child_link, parent_link) 229 | parent_body_ele.append(child_ele) 230 | # child_ele = ET.SubElement(parent_body_ele, "body") 231 | # child_ele_name: str = child_link.get_name() 232 | # child_ele.attrib = {"name": child_ele_name} 233 | 234 | # Recursively add children of this child_ele 235 | add_body_element(child_occ, child_ele) 236 | 237 | # traverse all the occs for body elements 238 | for occ in all_occs: 239 | asset_ele.append(get_mjcf_mesh(Link(occ))) 240 | if occ.fullPathName not in [item.fullPathName for sublist in parent_child_dict.values() for item in sublist]: 241 | # parent_ele = ET.SubElement(worldbody_ele, "body", name=Link(occ).get_name()) 242 | parent_ele = get_mjcf_body(Link(occ), None) 243 | worldbody_ele.append(parent_ele) 244 | add_body_element(occ, parent_ele) 245 | 246 | return root 247 | 248 | 249 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/commands/ACDC4Robot/entry.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import os 3 | from ...lib import fusion360utils as futil 4 | from ... import config 5 | from . import acdc4robot 6 | from . import constants 7 | app = adsk.core.Application.get() 8 | ui = app.userInterface 9 | 10 | 11 | # TODO *** Specify the command identity information. *** 12 | CMD_ID = f'{config.COMPANY_NAME}_{config.ADDIN_NAME}_ACDC4Robot' 13 | CMD_NAME = 'ACDC4Robot' 14 | CMD_Description = 'Export Autodesk Fusion design model to robot description format' 15 | 16 | # Specify that the command will be promoted to the panel. 17 | IS_PROMOTED = True 18 | 19 | # TODO *** Define the location where the command button will be created. *** 20 | # This is done by specifying the workspace, the tab, and the panel, and the 21 | # command it will be inserted beside. Not providing the command to position it 22 | # will insert it at the end. 23 | WORKSPACE_ID = 'FusionSolidEnvironment' 24 | PANEL_ID = 'SolidScriptsAddinsPanel' 25 | COMMAND_BESIDE_ID = 'ScriptsManagerCommand' 26 | 27 | # Resource location for command icons, here we assume a sub folder in this directory named "resources". 28 | ICON_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', '') 29 | 30 | # Local list of event handlers used to maintain a reference so 31 | # they are not released and garbage collected. 32 | local_handlers = [] 33 | 34 | 35 | # Executed when add-in is run. 36 | def start(): 37 | # Create a command Definition. 38 | cmd_def = ui.commandDefinitions.addButtonDefinition(CMD_ID, CMD_NAME, CMD_Description, ICON_FOLDER) 39 | 40 | # Define an event handler for the command created event. It will be called when the button is clicked. 41 | futil.add_handler(cmd_def.commandCreated, command_created) 42 | 43 | # ******** Add a button into the UI so the user can run the command. ******** 44 | # Get the target workspace the button will be created in. 45 | workspace = ui.workspaces.itemById(WORKSPACE_ID) 46 | 47 | # Get the panel the button will be created in. 48 | panel = workspace.toolbarPanels.itemById(PANEL_ID) 49 | 50 | # Create the button command control in the UI after the specified existing command. 51 | control = panel.controls.addCommand(cmd_def, COMMAND_BESIDE_ID, False) 52 | 53 | # Specify if the command is promoted to the main toolbar. 54 | control.isPromoted = IS_PROMOTED 55 | 56 | 57 | # Executed when add-in is stopped. 58 | def stop(): 59 | # Get the various UI elements for this command 60 | workspace = ui.workspaces.itemById(WORKSPACE_ID) 61 | panel = workspace.toolbarPanels.itemById(PANEL_ID) 62 | command_control = panel.controls.itemById(CMD_ID) 63 | command_definition = ui.commandDefinitions.itemById(CMD_ID) 64 | 65 | # Delete the button command control 66 | if command_control: 67 | command_control.deleteMe() 68 | 69 | # Delete the command definition 70 | if command_definition: 71 | command_definition.deleteMe() 72 | 73 | 74 | # Function that is called when a user clicks the corresponding button in the UI. 75 | # This defines the contents of the command dialog and connects to the command related events. 76 | def command_created(args: adsk.core.CommandCreatedEventArgs): 77 | # General logging for debug. 78 | futil.log(f'{CMD_NAME} Command Created Event') 79 | 80 | # create command inputs for model.config 81 | inputs = args.command.commandInputs 82 | 83 | # create a drop down command input to choose robot description format 84 | rdf_input = inputs.addDropDownCommandInput("robot_description_format", "Robot Description Format", adsk.core.DropDownStyles.LabeledIconDropDownStyle) 85 | rdf_items = rdf_input.listItems 86 | rdf_items.add("None", True) 87 | rdf_items.add("URDF", False) 88 | rdf_items.add("SDFormat", False) 89 | rdf_items.add("MJCF", False) 90 | rdf_items.add("URDF+", False) 91 | 92 | # create a drop down command input to choose simulation environment 93 | sim_env_input = inputs.addDropDownCommandInput("simulation_env", "Simulation Environment", adsk.core.DropDownStyles.LabeledIconDropDownStyle) 94 | sim_env_items = sim_env_input.listItems 95 | sim_env_items.add("None", True) 96 | sim_env_items.add("Gazebo", False) 97 | sim_env_items.add("PyBullet", False) 98 | sim_env_items.add("MuJoCo", False) 99 | sim_env_input.isVisible = False 100 | 101 | # create string value input for sdf info 102 | sdf_author_input = inputs.addStringValueInput("SDF_Author_name", "Author Name", "ACDC4Robot") 103 | sdf_author_input.isVisible = False 104 | sdf_description_input = inputs.addTextBoxCommandInput("SDF_Description", "Description", "Description about the robot model", 3, False) 105 | sdf_description_input.isVisible = False 106 | 107 | # # https://help.autodesk.com/view/fusion360/ENU/?contextId=CommandInputs 108 | # inputs = args.command.commandInputs 109 | 110 | # # TODO Define the dialog for your command by adding different inputs to the command. 111 | 112 | # # Create a simple text box input. 113 | # inputs.addTextBoxCommandInput('text_box', 'Some Text', 'Enter some text.', 1, False) 114 | 115 | # # Create a value input field and set the default using 1 unit of the default length unit. 116 | # defaultLengthUnits = app.activeProduct.unitsManager.defaultLengthUnits 117 | # default_value = adsk.core.ValueInput.createByString('1') 118 | # inputs.addValueInput('value_input', 'Some Value', defaultLengthUnits, default_value) 119 | 120 | # TODO Connect to the events that are needed by this command. 121 | futil.add_handler(args.command.execute, command_execute, local_handlers=local_handlers) 122 | futil.add_handler(args.command.inputChanged, command_input_changed, local_handlers=local_handlers) 123 | # futil.add_handler(args.command.executePreview, command_preview, local_handlers=local_handlers) 124 | # futil.add_handler(args.command.validateInputs, command_validate_input, local_handlers=local_handlers) 125 | futil.add_handler(args.command.destroy, command_destroy, local_handlers=local_handlers) 126 | 127 | 128 | # This event handler is called when the user clicks the OK button in the command dialog or 129 | # is immediately called after the created event not command inputs were created for the dialog. 130 | def command_execute(args: adsk.core.CommandEventArgs): 131 | # General logging for debug. 132 | futil.log(f'{CMD_NAME} Command Execute Event') 133 | 134 | # get the command inputs value 135 | inputs = args.command.commandInputs 136 | sdf_input: adsk.core.DropDownCommandInput = inputs.itemById("robot_description_format") 137 | sim_env_input: adsk.core.DropDownCommandInput = inputs.itemById("simulation_env") 138 | name_input: adsk.core.StringValueCommandInput = inputs.itemById("SDF_Author_name") 139 | text_input: adsk.core.TextBoxCommandInput = inputs.itemById("SDF_Description") 140 | constants.set_rdf(sdf_input.selectedItem.name) 141 | constants.set_sim_env(sim_env_input.selectedItem.name) 142 | constants.set_author_name(name_input.value) 143 | constants.set_model_description(text_input.text) 144 | 145 | acdc4robot.run() 146 | # TODO ******************************** Your code here ******************************** 147 | 148 | # Get a reference to your command's inputs. 149 | # inputs = args.command.commandInputs 150 | # text_box: adsk.core.TextBoxCommandInput = inputs.itemById('text_box') 151 | # value_input: adsk.core.ValueCommandInput = inputs.itemById('value_input') 152 | 153 | # # Do something interesting 154 | # text = text_box.text 155 | # expression = value_input.expression 156 | # msg = f'Your text: {text}
Your value: {expression}' 157 | # ui.messageBox(msg) 158 | 159 | 160 | # This event handler is called when the command needs to compute a new preview in the graphics window. 161 | def command_preview(args: adsk.core.CommandEventArgs): 162 | # General logging for debug. 163 | futil.log(f'{CMD_NAME} Command Preview Event') 164 | inputs = args.command.commandInputs 165 | 166 | 167 | # This event handler is called when the user changes anything in the command dialog 168 | # allowing you to modify values of other inputs based on that change. 169 | def command_input_changed(args: adsk.core.InputChangedEventArgs): 170 | changed_input = args.input 171 | command = changed_input.parentCommand 172 | inputs = command.commandInputs 173 | rdf: adsk.core.DropDownCommandInput = inputs.itemById("robot_description_format") 174 | sim_env: adsk.core.DropDownCommandInput = inputs.itemById("simulation_env") 175 | sim_env_items = sim_env.listItems 176 | sdf_author = inputs.itemById("SDF_Author_name") 177 | sdf_description = inputs.itemById("SDF_Description") 178 | # inputs = args.inputs 179 | 180 | if changed_input.id == "robot_description_format": 181 | if changed_input.selectedItem.name == "None": 182 | sim_env.isVisible = False 183 | 184 | elif changed_input.selectedItem.name == "URDF": 185 | sim_env.isVisible = True 186 | 187 | elif changed_input.selectedItem.name == "SDFormat": 188 | sim_env.isVisible = True 189 | 190 | elif changed_input.selectedItem.name == "MJCF": 191 | sim_env.isVisible = True 192 | 193 | 194 | if (rdf.selectedItem.name == "SDFormat") and (sim_env.selectedItem.name == "Gazebo"): 195 | sdf_author.isVisible = True 196 | sdf_description.isVisible = True 197 | else: 198 | sdf_author.isVisible = False 199 | sdf_description.isVisible = False 200 | 201 | # General logging for debug. 202 | futil.log(f'{CMD_NAME} Input Changed Event fired from a change to {changed_input.id}') 203 | 204 | 205 | # This event handler is called when the user interacts with any of the inputs in the dialog 206 | # which allows you to verify that all of the inputs are valid and enables the OK button. 207 | def command_validate_input(args: adsk.core.ValidateInputsEventArgs): 208 | # General logging for debug. 209 | futil.log(f'{CMD_NAME} Validate Input Event') 210 | 211 | inputs = args.inputs 212 | 213 | # Verify the validity of the input values. This controls if the OK button is enabled or not. 214 | valueInput = inputs.itemById('value_input') 215 | if valueInput.value >= 0: 216 | args.areInputsValid = True 217 | else: 218 | args.areInputsValid = False 219 | 220 | 221 | # This event handler is called when the command terminates. 222 | def command_destroy(args: adsk.core.CommandEventArgs): 223 | # General logging for debug. 224 | futil.log(f'{CMD_NAME} Command Destroy Event') 225 | 226 | global local_handlers 227 | local_handlers = [] 228 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/commands/ACDC4Robot/acdc4robot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: nuofan 3 | """ 4 | Export robot description format files from Fusion360 design 5 | """ 6 | 7 | import adsk, adsk.core, adsk.fusion, traceback 8 | import os, sys 9 | from ...core.link import Link 10 | from ...core.joint import Joint 11 | from . import constants 12 | from ...core import write 13 | from ...core import utils 14 | from ...core.robot import Robot 15 | from ...core.urdf_plus import URDF_PLUS 16 | import time 17 | 18 | def get_link_joint_list(design: adsk.fusion.Design): 19 | """ 20 | Get the link list and joint list to export 21 | 22 | Return: 23 | link_list: [Link] 24 | a list contains all the links that will be exported 25 | joint_list: [Joint] 26 | a list contains all the joint that will be exported 27 | """ 28 | root = design.rootComponent 29 | link_list = [] 30 | joint_list = [] 31 | occs: adsk.fusion.OccurrenceList = root.allOccurrences 32 | 33 | # try to solve the nested components problem 34 | # but still not fully tested 35 | for occ in occs: 36 | # TODO: it seems use occ.joints.count will make it usable with occurrences? Test it 37 | if occ.component.joints.count > 0: 38 | # textPalette.writeText(str(occ.fullPathName)) 39 | continue 40 | else: 41 | # Only occurrence contains zero joint and has zero childOccurrences 42 | # can be seen as a link 43 | if occ.childOccurrences.count > 0: 44 | # textPalette.writeText(str(occ.fullPathName)) 45 | # textPalette.writeText(str(occ.childOccurrences.count)) 46 | continue 47 | else: 48 | # textPalette.writeText(str(occ.fullPathName)) 49 | # textPalette.writeText(str(occ.childOccurrences.count)) 50 | if occ.isLightBulbOn: 51 | # only the occurrence light bulb on that the occurrence will be exported 52 | link_list.append(Link(occ)) # add link objects into link_list 53 | 54 | for joint in root.allJoints: 55 | joint_list.append(Joint(joint)) # add joint objects into joint_list 56 | 57 | return link_list, joint_list 58 | 59 | def export_stl(design: adsk.fusion.Design, save_dir: str, links: list[Link]): 60 | """ 61 | export each component's stl file into "save_dir/mesh" 62 | 63 | Parameters 64 | --------- 65 | design: adsk.fusion.Design 66 | current active design 67 | save_dir: str 68 | the directory to store the export stl file 69 | """ 70 | # create a single exportManager instance 71 | export_manager = design.exportManager 72 | # set the directory for the mesh file 73 | try: os.mkdir(save_dir + "/meshes") 74 | except: pass 75 | mesh_dir = save_dir + "/meshes" 76 | 77 | for link in links: 78 | visual_body: adsk.fusion.BRepBody = link.get_visual_body() 79 | col_body: adsk.fusion.BRepBody = link.get_collision_body() 80 | if (visual_body is None) and (col_body is None): 81 | # export the whole occurrence 82 | mesh_name = mesh_dir + "/" + link.get_name() 83 | occ = link.get_link_occ() 84 | # obj_export_options = export_manager.createOBJExportOptions(occ, mesh_name) 85 | # obj_export_options.unitType = adsk.fusion.DistanceUnits.MillimeterDistanceUnits # set unit to mm 86 | # obj_export_options.meshRefinement = adsk.fusion.MeshRefinementSettings.MeshRefinementLow 87 | # export_manager.execute(obj_export_options) 88 | stl_export_options = export_manager.createSTLExportOptions(occ, mesh_name) 89 | stl_export_options.sendToPrintUtility = False 90 | stl_export_options.isBinaryFormat = True 91 | stl_export_options.meshRefinement = adsk.fusion.MeshRefinementSettings.MeshRefinementLow 92 | export_manager.execute(stl_export_options) 93 | elif (visual_body is not None) and (col_body is not None): 94 | # export visual and collision geometry seperately 95 | visual_mesh_name = mesh_dir + "/" + link.get_name() + "_visual" 96 | visual_exp_options = export_manager.createSTLExportOptions(visual_body, visual_mesh_name) 97 | visual_exp_options.sendToPrintUtility = False 98 | visual_exp_options.isBinaryFormat = True 99 | visual_exp_options.meshRefinement = adsk.fusion.MeshRefinementSettings.MeshRefinementLow 100 | export_manager.execute(visual_exp_options) 101 | 102 | col_mesh_name = mesh_dir + "/" + link.get_name() + "_collision" 103 | col_exp_options = export_manager.createSTLExportOptions(col_body, col_mesh_name) 104 | col_exp_options.sendToPrintUtility = False 105 | col_exp_options.isBinaryFormat = True 106 | col_exp_options.meshRefinement = adsk.fusion.MeshRefinementSettings.MeshRefinementLow 107 | export_manager.execute(col_exp_options) 108 | 109 | elif (visual_body is None) and (col_body is not None): 110 | error_message = "Please set two bodies, one for visual and one for collision. \n" 111 | error_message = error_message + "Body for visual missing." 112 | utils.error_box(error_message) 113 | utils.terminate_box() 114 | elif (visual_body is not None) and (col_body is None): 115 | error_message = "Please set two bodies, one for visual and one for collision. \n" 116 | error_message = error_message + "Body for collision missing." 117 | utils.error_box(error_message) 118 | utils.terminate_box() 119 | 120 | 121 | def run(): 122 | # Initialization 123 | app = adsk.core.Application.get() 124 | ui = app.userInterface 125 | product = app.activeProduct 126 | design = adsk.fusion.Design.cast(product) 127 | 128 | msg_box_title = "ACDC4Robot Message" 129 | 130 | # open a text palette for debuging 131 | textPalette = ui.palettes.itemById("TextCommands") 132 | if not textPalette.isVisible: 133 | textPalette.isVisible = True 134 | constants.set_text_palette(textPalette) 135 | 136 | try: 137 | # Set design type into do not capture design history 138 | design.designType = adsk.fusion.DesignTypes.DirectDesignType 139 | 140 | # # Check the length unit of Fusion360 141 | # if design.unitsManager.defaultLengthUnits != "m": 142 | # ui.messageBox("Please set length unit to 'm'!", msg_box_title) 143 | # return 0 # exit run() function 144 | 145 | root = design.rootComponent # get root component 146 | allComp = design.allComponents 147 | robot_name = root.name.split()[0] 148 | constants.set_robot_name(robot_name) 149 | 150 | # Set the folder to store exported files 151 | folder_dialog = ui.createFolderDialog() 152 | folder_dialog.title = "Chose your folder to export" 153 | dialog_result = folder_dialog.showDialog() # show folder dialog 154 | save_folder = "" 155 | if dialog_result == adsk.core.DialogResults.DialogOK: 156 | save_folder = folder_dialog.folder 157 | else: 158 | ui.messageBox("ACDC4Robot was canceled", msg_box_title) 159 | return 0 # exit run() function 160 | 161 | save_folder = save_folder + "/" + robot_name 162 | try: os.mkdir(save_folder) 163 | except: pass 164 | 165 | ui.messageBox("Start ACDC4Robot Add-IN", msg_box_title) 166 | 167 | # get all the link & joint elements to export 168 | link_list, joint_list = get_link_joint_list(design) 169 | 170 | rdf = constants.get_rdf() 171 | simulator = constants.get_sim_env() 172 | 173 | if rdf == None: 174 | ui.messageBox("Robot description format is None.\n" + 175 | "Please choose one robot description format", msg_box_title) 176 | elif rdf == "URDF": 177 | if simulator == "None": 178 | ui.messageBox("Simulation environment is None.\n" + 179 | "Please select a simulation environment.", msg_box_title) 180 | elif simulator == "Gazebo": 181 | # write to .urdf file 182 | write.write_urdf(link_list, joint_list, save_folder, robot_name) 183 | # export mesh files 184 | export_stl(design, save_folder, link_list) 185 | ui.messageBox("Finished exporting URDF for Gazebo.", msg_box_title) 186 | 187 | elif simulator == "PyBullet": 188 | # write to .urdf file 189 | write.write_urdf(link_list, joint_list, save_folder, robot_name) 190 | # export mesh files 191 | export_stl(design, save_folder, link_list) 192 | # generate pybullet script 193 | write.write_hello_pybullet(rdf, robot_name, save_folder) 194 | ui.messageBox("Finished exporting URDF for PyBullet.", msg_box_title) 195 | 196 | elif simulator == "MuJoCo": 197 | # write to .urdf file 198 | write.write_urdf(link_list, joint_list, save_folder, robot_name) 199 | # export mesh files 200 | export_stl(design, save_folder, link_list) 201 | 202 | ui.messageBox("Finished exporting URDF for MuJoCo.", msg_box_title) 203 | 204 | elif rdf == "SDFormat": 205 | if simulator == "None": 206 | ui.messageBox("Simulation environment is None.\n" + 207 | "Please select a simulation environment.", msg_box_title) 208 | elif simulator == "Gazebo": 209 | # write to .sdf file 210 | write.write_sdf(link_list, joint_list, save_folder, robot_name) 211 | # write a model cofig file 212 | author = constants.get_author_name() 213 | des = constants.get_model_description() 214 | write.write_sdf_config(save_folder, robot_name, author, des) 215 | # export stl files 216 | export_stl(design, save_folder, link_list) 217 | ui.messageBox("Finished exporting SDFormat for Gazebo.", msg_box_title) 218 | elif simulator == "PyBullet": 219 | # write to .sdf file 220 | write.write_sdf(link_list, joint_list, save_folder, robot_name) 221 | # export stl files 222 | export_stl(design, save_folder, link_list) 223 | # generate pybullet script 224 | write.write_hello_pybullet(rdf,robot_name, save_folder) 225 | ui.messageBox("Finished exporting SDFormat for PyBullet.", msg_box_title) 226 | 227 | elif simulator == "MuJoCo": 228 | ui.messageBox("MuJoCo does not support SDFormat. \n" + 229 | "Please select PyBullet or Gazebo as simulation environment.", msg_box_title) 230 | 231 | elif rdf == "MJCF": 232 | if simulator == "None": 233 | ui.messageBox("Simulation environment is None. \n" + 234 | "Please select a simulation environment.", msg_box_title) 235 | elif simulator == "Gazebo": 236 | ui.messageBox("Gazebo does not support MJCF. \n"+ 237 | "Please select MuJoCo for simulation.", msg_box_title) 238 | elif simulator == "PyBullet": 239 | ui.messageBox("PyBullet does not support MJCF. \n" + 240 | "Please select MuJoCo for simulation.", msg_box_title) 241 | elif simulator == "MuJoCo": 242 | # write to .xml file 243 | write.write_mjcf(root, robot_name, save_folder) 244 | # export stl files 245 | export_stl(design, save_folder, link_list) 246 | time.sleep(0.1) 247 | ui.messageBox("Finished exporting MJCF for MuJoCo.", msg_box_title) 248 | 249 | elif rdf == "URDF+": 250 | robot = Robot(design) 251 | urdf_plus = URDF_PLUS(robot) 252 | urdf_plus_path = save_folder + "/{}.urdf".format(robot.get_robot_name()) 253 | urdf_plus.write_file(urdf_plus_path) 254 | stl_list: list[Link] = robot.get_links() 255 | export_stl(design, save_folder, stl_list) 256 | time.sleep(0.1) 257 | ui.messageBox("Finished exporting URDF+.", msg_box_title) 258 | 259 | except: 260 | if ui: 261 | ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) 262 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/joint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Get informations about joints from Fusion360 API 4 | 5 | """ 6 | 7 | import adsk, adsk.fusion, adsk.core 8 | from xml.etree.ElementTree import ElementTree, Element, SubElement 9 | from . import utils 10 | from . import math_operation as math_op 11 | # from .link import Link 12 | 13 | class Joint(): 14 | """ 15 | Joint class for joint 16 | """ 17 | 18 | def __init__(self, joint: adsk.fusion.Joint) -> None: 19 | self.joint = joint 20 | self.name = joint.name 21 | self.parent = joint.occurrenceTwo # parent link of joint 22 | self.child = joint.occurrenceOne 23 | # self.parent = Link(joint.occurrenceTwo) # parent link of joint 24 | # self.child = Link(joint.occurrenceOne) 25 | 26 | def get_name(self): 27 | """ 28 | Return: 29 | name: str 30 | joint's full path name 31 | """ 32 | # joint names inside each occurrence are identical 33 | # but joint names in different occurrences can be the same, in which makes conflict 34 | # parent_name = utils.get_valid_filename(self.parent.fullPathName) # inorder to fix joint's name confliction 35 | # name = parent_name + "_" + utils.get_valid_filename(self.name) 36 | # output name is consistent with the name in Fusion 37 | name = utils.get_valid_filename(self.name) 38 | return name 39 | 40 | def get_parent(self): 41 | """ 42 | Return name of parent link 43 | """ 44 | if self.parent.component.name == "base_link": 45 | parent_name = "base_link" 46 | else: 47 | parent_name = utils.get_valid_filename(self.parent.fullPathName) 48 | 49 | return parent_name 50 | 51 | def get_child(self): 52 | """ 53 | Return name of child link 54 | """ 55 | if self.child.component.name == "base_link": 56 | child_name = "base_link" 57 | else: 58 | child_name = utils.get_valid_filename(self.child.fullPathName) 59 | 60 | return child_name 61 | 62 | def get_sdf_joint_type(self) -> str: 63 | """ 64 | Currently support following joint type: 65 | fixed, revolute, prismatic, continuous 66 | 67 | Return: 68 | joint_type: str 69 | fixed, revolute, prismatic 70 | """ 71 | # currently, only support these three sdf joint type 72 | sdf_joint_type_list = ["fixed", "revolute", "prismatic"] 73 | if self.joint.jointMotion.jointType <= 2: 74 | sdf_joint_type = sdf_joint_type_list[self.joint.jointMotion.jointType] 75 | # # TODO:It seems continuous joint type has some problem with gazebo 76 | # if sdf_joint_type == "revolute" and (self.get_limits() is None): 77 | # sdf_joint_type = "continuous" 78 | else: 79 | pass 80 | return sdf_joint_type 81 | 82 | def get_mjcf_joint_type(self) -> str: 83 | """ 84 | Return joint type for mjcf 85 | Return 86 | --------- 87 | joint_type: str 88 | """ 89 | mjcf_joint_type_list = [None, "hinge", "slide"] 90 | if self.joint.jointMotion.jointType <= 2: 91 | mjcf_joint_type = mjcf_joint_type_list[self.joint.jointMotion.jointType] 92 | else: 93 | pass 94 | return mjcf_joint_type 95 | 96 | def get_urdf_joint_type(self) -> str: 97 | urdf_joint_type_list = ["fixed", "revolute", "prismatic"] 98 | if self.joint.jointMotion.jointType <= 2: 99 | urdf_joint_type = urdf_joint_type_list[self.joint.jointMotion.jointType] 100 | if urdf_joint_type == "revolute" and (self.get_limits() is None): 101 | urdf_joint_type = "continuous" 102 | else: 103 | # other joint types are not supported yet 104 | pass 105 | return urdf_joint_type 106 | 107 | def get_limits(self): 108 | """ 109 | Return joint limits list: [lower_limit, upper_limit] 110 | or None 111 | """ 112 | if self.joint.jointMotion.jointType == 0: # RigidJointType 113 | return None 114 | elif self.joint.jointMotion.jointType == 1: # RevoluteJointType 115 | max_enabled = self.joint.jointMotion.rotationLimits.isMaximumValueEnabled 116 | min_enabled = self.joint.jointMotion.rotationLimits.isMinimumValueEnabled 117 | if max_enabled and min_enabled: 118 | # unit: radians 119 | upper_limit = self.joint.jointMotion.rotationLimits.maximumValue 120 | lower_limit = self.joint.jointMotion.rotationLimits.minimumValue 121 | return [round(lower_limit, 6), round(upper_limit, 6)] 122 | else: 123 | return None 124 | elif self.joint.jointMotion.jointType == 2: # SliderJointType 125 | max_enabled = self.joint.jointMotion.slideLimits.isMaximumValueEnabled 126 | min_enabled = self.joint.jointMotion.slideLimits.isMinimumValueEnabled 127 | if max_enabled and min_enabled: 128 | upper_limit = self.joint.jointMotion.slideLimits.maximumValue * 0.01 # cm -> m 129 | lower_limit = self.joint.jointMotion.slideLimits.minimumValue * 0.01 # cm -> m 130 | return [round(lower_limit, 6), round(upper_limit, 6)] 131 | else: 132 | return None 133 | 134 | def get_sdf_origin(self): 135 | """ 136 | From: http://sdformat.org/tutorials?tut=spec_model_kinematics&cat=specification&#jointpose 137 | For a joint with parent link frame `P` and child link frame `C`, 138 | the joint `` tag specifies the pose `X_CJc` of a joint frame `Jc` rigidly attached to the child link. 139 | """ 140 | # I think the joint frame is defined by the parent joint origin in Fusion360 141 | # Because in Fusion360, a joint motion is defined by the axis in parent joint orign 142 | # and use offset to define the location of child joint origin w.r.t parent joint 143 | # I guess using parent joint origin would solve the offset problem? 144 | # I guess using child joint origin works just because they coincide together for all the text examples 145 | 146 | # get parent joint origin as the child joint 147 | if self.joint.geometryOrOriginTwo == adsk.fusion.JointOrigin: 148 | w_P_Jc = self.joint.geometryOrOriginTwo.geometry.origin.asArray() 149 | else: 150 | w_P_Jc = self.joint.geometryOrOriginTwo.origin.asArray() 151 | 152 | # convert from cm to m 153 | w_P_Jc = [round(i*0.01, 6) for i in w_P_Jc] 154 | # get child link frame's origin point w.r.t world frame 155 | w_P_Lc = [self.child.transform2.translation.x * 0.01, 156 | self.child.transform2.translation.y * 0.01, 157 | self.child.transform2.translation.z * 0.01,] 158 | # vector from child link frame's origin point to child joint origin point w.r.t world frame 159 | w_V_LcJc = [[w_P_Jc[0]-w_P_Lc[0]], 160 | [w_P_Jc[1]-w_P_Lc[1]], 161 | [w_P_Jc[2]-w_P_Lc[2]]] # 3*1 vector 162 | 163 | w_T_Lc = self.child.transform2 # configuration of child-link-frame w.r.t world-frame w 164 | w_R_Lc = math_op.get_rotation_matrix(w_T_Lc) # rotation matrix of child-link-frame w.r.t world-frame w 165 | Lc_R_w = math_op.matrix_transpose(w_R_Lc) # rotation matrix of world-frame w.r.t child-link-frame Lc 166 | # vector from child link frame's origin point to child joint origin point w.r.t child-link-frame Lc 167 | Lc_V_LcJc = math_op.change_orientation(Lc_R_w, w_V_LcJc) # 3*1 array 168 | # assume the joint frame has the same oritation as child link frame 169 | # it seems that rpy of joint doesn't matter, so set them as 0 170 | sdf_origin = [Lc_V_LcJc[0][0], Lc_V_LcJc[1][0], Lc_V_LcJc[2][0], 0.0, 0.0, 0.0] 171 | 172 | return sdf_origin 173 | 174 | def get_joint_frame(self) -> adsk.core.Matrix3D: 175 | """ 176 | Get the joint frame whose origin coincides with parent joint origin, 177 | and has same orientation with parent joint origin frame 178 | 179 | Return: 180 | joint_frame: adsk.core.Matrix3D 181 | a homogeneous matrix represents joint frame J in world frame W 182 | translation unit: cm 183 | """ 184 | # get parent joint origin's coordinate w.r.t world frame 185 | if self.joint.geometryOrOriginTwo == adsk.fusion.JointOrigin: 186 | w_P_J = self.joint.geometryOrOriginTwo.geometry.origin.asArray() 187 | else: 188 | w_P_J = self.joint.geometryOrOriginTwo.origin.asArray() 189 | 190 | w_P_J = [round(i, 6) for i in w_P_J] 191 | 192 | # no matter jointGeometry or jointOrigin object, both have these properties 193 | zAxis: adsk.core.Vector3D = self.joint.geometryOrOriginTwo.primaryAxisVector 194 | xAxis: adsk.core.Vector3D = self.joint.geometryOrOriginTwo.secondaryAxisVector 195 | yAxis: adsk.core.Vector3D = self.joint.geometryOrOriginTwo.thirdAxisVector 196 | 197 | origin = adsk.core.Point3D.create(w_P_J[0], w_P_J[1], w_P_J[2]) 198 | 199 | joint_frame = adsk.core.Matrix3D.create() 200 | joint_frame.setWithCoordinateSystem(origin, xAxis, yAxis, zAxis) 201 | 202 | return joint_frame 203 | 204 | def get_urdf_origin(self): 205 | """ 206 | Get joint origin, which is the transform from the parent frame to this joint 207 | """ 208 | parent_link = self.parent 209 | 210 | # get parent_frame w.r.t world frame 211 | def get_parent_joint(link: adsk.fusion.Occurrence) -> adsk.fusion.Joint: 212 | joint_list: adsk.fusion.JointList = link.joints 213 | for j in joint_list: 214 | if j.occurrenceOne == link: 215 | return j 216 | else: 217 | continue 218 | return None 219 | 220 | parent_joint: adsk.fusion.Joint = get_parent_joint(parent_link) 221 | if parent_joint is None: 222 | # if the parent link does not have parent joint(which means the root link), 223 | # then the parent link frame is the parent frame 224 | parent_frame: adsk.core.Matrix3D = parent_link.transform2 225 | else: 226 | if parent_joint.geometryOrOriginTwo == adsk.fusion.JointOrigin: 227 | w_P_J = parent_joint.geometryOrOriginTwo.geometry.origin.asArray() 228 | else: 229 | w_P_J = parent_joint.geometryOrOriginTwo.origin.asArray() 230 | 231 | w_P_J = [round(i, 6) for i in w_P_J] 232 | 233 | # no matter jointGeometry or jointOrigin object, both have these properties 234 | zAxis: adsk.core.Vector3D = parent_joint.geometryOrOriginTwo.primaryAxisVector 235 | xAxis: adsk.core.Vector3D = parent_joint.geometryOrOriginTwo.secondaryAxisVector 236 | yAxis: adsk.core.Vector3D = parent_joint.geometryOrOriginTwo.thirdAxisVector 237 | 238 | origin = adsk.core.Point3D.create(w_P_J[0], w_P_J[1], w_P_J[2]) 239 | 240 | parent_frame = adsk.core.Matrix3D.create() 241 | parent_frame.setWithCoordinateSystem(origin, xAxis, yAxis, zAxis) 242 | 243 | 244 | # get joint frame w.r.t world frame 245 | joint_frame = self.get_joint_frame() 246 | 247 | # from_origin, from_xAxis, from_yAxis, from_zAxis = parent_frame.getAsCoordinateSystem() 248 | # to_origin, to_xAsix, to_yAxis, to_zAxis = joint_frame.getAsCoordinateSystem() 249 | 250 | transform = adsk.core.Matrix3D.create() 251 | # transform.setToAlignCoordinateSystems(from_origin, from_xAxis, from_yAxis, from_zAxis, 252 | # to_origin, to_xAsix, to_yAxis, to_zAxis) 253 | transform = math_op.coordinate_transform(parent_frame, joint_frame) 254 | 255 | joint_origin = math_op.matrix3d_2_pose(transform) 256 | 257 | return joint_origin 258 | 259 | def get_axes(self): 260 | """ 261 | Return: 262 | --------- 263 | axis1: list or None 264 | list: [x1, y1, z1] 265 | axis2: list or None 266 | list: [x2, y2, z2] 267 | """ 268 | # Fusion360 api returns joint axis w.r.t world frame 269 | if self.joint.jointMotion.jointType == 0: # RigidJointType 270 | axis1 = None 271 | axis2 = None 272 | return axis1, axis2 273 | elif self.joint.jointMotion.jointType == 1: # RevoluteJointType 274 | axis1 = [round(i, 6) for i in self.joint.jointMotion.rotationAxisVector.asArray()] # In Fusion360, returned axis is normalized 275 | axis2 = None 276 | return axis1, axis2 277 | elif self.joint.jointMotion.jointType == 2: # SliderJointType 278 | axis1 = [round(i, 6) for i in self.joint.jointMotion.slideDirectionVector.asArray()] # In Fusion360, returned axis is normalized 279 | axis2 = None 280 | return axis1, axis2 281 | elif self.joint.jointMotion.jointType == 3: # CylindricalJointType 282 | axis1 = [round(i, 6) for i in self.joint.jointMotion.rotationAxisVector.asArray()] # In Fusion360, returned axis is normalized 283 | axis2 = None 284 | return axis1, axis2 285 | elif self.joint.jointMotion.jointType == 4: # PinSlotJointType 286 | axis1 = [round(i, 6) for i in self.joint.jointMotion.rotationAxisVector.asArray()] # rotation axis 287 | axis2 = [round(i, 6) for i in self.joint.jointMotion.slideDirectionVector.asArray()] # slide axis 288 | return axis1, axis2 289 | elif self.joint.jointMotion.jointType == 5: # PlanarJointType 290 | axis1 = [round(i, 6) for i in self.joint.jointMotion.primarySlideDirectionVector.asArray()] # rotation axis 291 | axis2 = [round(i, 6) for i in self.joint.jointMotion.secondarySlideDirectionVector.asArray()] # slide axis 292 | elif self.joint.jointMotion.jointType == 6: # BallJointType 293 | pass 294 | 295 | def get_axes_urdf(self): 296 | """ 297 | Return 298 | --------- 299 | axis1: list or None 300 | list: [x1, y1, z1], w.r.t joint-frame J 301 | axis2: list or None 302 | list: [x2, y2, z2], w.r.t joint-frame J 303 | """ 304 | # both axes are expressed w.r.t world-frame w 305 | w_axis1, w_axis2 = self.get_axes() 306 | J_axis1, J_axis2 = None, None 307 | joint_frame: adsk.core.Matrix3D = self.get_joint_frame() 308 | w_R_J = math_op.get_rotation_matrix(joint_frame) # represent joint-frame J's orientation w.r.t world-frame w 309 | J_R_w = math_op.matrix_transpose(w_R_J) 310 | if w_axis1 is not None: 311 | w_axis1 = [[w_axis1[0]], [w_axis1[1]], [w_axis1[2]]] # from 1*3 to 3*1 list 312 | J_axis1 = math_op.change_orientation(J_R_w, w_axis1) 313 | J_axis1 = [J_axis1[0][0], J_axis1[1][0], J_axis1[2][0]] 314 | if w_axis2 is not None: 315 | w_axis2 = [[w_axis2[0]], [w_axis2[1]], [w_axis2[2]]] 316 | J_axis2 = math_op.change_orientation(J_R_w, w_axis2) 317 | J_axis2 = [J_axis2[0][0], J_axis2[1][0], J_axis2[2][0]] 318 | 319 | return J_axis1, J_axis2 320 | 321 | def get_axis_mjcf(self): 322 | """ 323 | I guess the reference frame of the axis is the body contains the joint element, 324 | which is the child link of the joint 325 | 326 | Return: 327 | axis: list or None 328 | """ 329 | w_axis1, _ = self.get_axes() 330 | C_axis = None 331 | child_link_frame: adsk.core.Matrix3D = self.child.transform2 332 | w_R_C = math_op.get_rotation_matrix(child_link_frame) 333 | C_R_w = math_op.matrix_transpose(w_R_C) 334 | if w_axis1 is not None: 335 | w_axis1 = [[w_axis1[0]], [w_axis1[1]], [w_axis1[2]]] # from 1*3 to 3*1 list 336 | C_axis = math_op.change_orientation(C_R_w, w_axis1) 337 | C_axis = [C_axis[0][0], C_axis[1][0], C_axis[2][0]] 338 | 339 | return C_axis 340 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/sdf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Nuofan 3 | # 20230811 4 | # Contains functions for generating urdf 5 | import adsk, adsk.core, adsk.fusion 6 | from .link import Link 7 | from .joint import Joint 8 | from xml.etree.ElementTree import Element, SubElement 9 | from . import math_operation as math_op 10 | from . import utils 11 | from ..commands.ACDC4Robot import constants 12 | 13 | def get_link_name(link: Link) -> str: 14 | """ 15 | Return 16 | --------- 17 | link_name: str 18 | link's full path name 19 | """ 20 | name = link.get_name() 21 | return name 22 | 23 | def get_link_pose(link: Link) -> list[float]: 24 | """ 25 | Return 26 | --------- 27 | link_pose: [x, y, z, roll, pitch, yaw] 28 | Without @relative_to setting, link pose is expressed in parent element frame, 29 | which is the model frame (world frame in Fusion360) by default 30 | """ 31 | link_pose = math_op.matrix3d_2_pose(link.pose) 32 | return link_pose 33 | 34 | def get_link_mass(link: Link) -> float: 35 | """ 36 | Return 37 | --------- 38 | mass: float 39 | unit: kg 40 | """ 41 | mass = link.phyPro.mass 42 | return mass 43 | 44 | def get_link_CoM(link: Link) -> list[float]: 45 | """ 46 | Return 47 | --------- 48 | CoM: [x, y, z, roll, pitch, yaw] 49 | Center of mass frame C relative to the link frame L 50 | unit: m, radian 51 | """ 52 | # Let the orientation of center-of-mass frame C is same as link-frame L 53 | roll = 0.0 54 | pitch = 0.0 55 | yaw = 0.0 56 | w_CoM_x = link.phyPro.centerOfMass.x # point CoM's x coordinate w.r.t world-frame 57 | w_CoM_y = link.phyPro.centerOfMass.y 58 | w_CoM_z = link.phyPro.centerOfMass.z 59 | w_Lo_x = link.pose.translation.x # link-frame's origin point's x coordinat w.r.t world-frame 60 | w_Lo_y = link.pose.translation.y 61 | w_Lo_z = link.pose.translation.z 62 | # represent vector Lo-CoM which start from link-fram's origin point to CoM point w.r.t world-frame 63 | w_Lo_CoM = [[(w_CoM_x-w_Lo_x)*0.01], [(w_CoM_y-w_Lo_y)*0.01], [(w_CoM_z-w_Lo_z)*0.01]] # cm -> m 64 | # represent world-frame's orientation w.r.t link-frame 65 | L_R_w = [[link.pose.getCell(0, 0), link.pose.getCell(1, 0), link.pose.getCell(2, 0)], 66 | [link.pose.getCell(0, 1), link.pose.getCell(1, 1), link.pose.getCell(2, 1)], 67 | [link.pose.getCell(0, 2), link.pose.getCell(1, 2), link.pose.getCell(2, 2)]] 68 | 69 | L_Lo_CoM = math_op.change_orientation(L_R_w, w_Lo_CoM) 70 | CoM = [L_Lo_CoM[0][0], L_Lo_CoM[1][0], L_Lo_CoM[2][0], roll, pitch, yaw] 71 | 72 | return CoM 73 | 74 | def get_link_inertia(link: Link) -> list[float]: 75 | """ 76 | Return 77 | --------- 78 | inertia_list: [ixx, iyy, izz, ixy, iyz, ixz] 79 | Inertia tensor elements w.r.t CoM frame, which has the same orientation with the link frame L 80 | unit: kg*m^2 81 | """ 82 | (_, w_ixx, w_iyy, w_izz, w_ixy, w_iyz, w_ixz) = link.phyPro.getXYZMomentsOfInertia() # unit: kg*cm^2 83 | # Use parallel axis theorem to change inertia from w.r.t world-frame's origin to w.r.t CoM 84 | x = link.phyPro.centerOfMass.x * 0.01 # get the x coordinate w.r.t world-frame, unit: m 85 | y = link.phyPro.centerOfMass.y * 0.01 86 | z = link.phyPro.centerOfMass.z * 0.01 87 | mass = link.phyPro.mass # unit: kg 88 | com_ixx = w_ixx*0.0001 - mass*(y**2+z**2) # kg*cm^2 -> kg*m^2 89 | com_iyy = w_iyy*0.0001 - mass*(x**2+z**2) 90 | com_izz = w_izz*0.0001 - mass*(x**2+y**2) 91 | com_ixy = w_ixy*0.0001 + mass*(x*y) 92 | com_iyz = w_iyz*0.0001 + mass*(y*z) 93 | com_ixz = w_ixz*0.0001 + mass*(x*z) 94 | 95 | # Change inertia tensor to the orientation of link-frame L 96 | # reference: https://robot.sia.cn/CN/abstract/abstract374.shtml 97 | R = math_op.get_rotation_matrix(link.pose) 98 | R_T = math_op.matrix_transpose(R) 99 | inertia_tensor = [[com_ixx, com_ixy, com_ixz], 100 | [com_ixy, com_iyy, com_iyz], 101 | [com_ixz, com_iyz, com_izz]] 102 | 103 | # moments of inertia has the same orientation of L-frame 104 | I = math_op.matrix_multi(math_op.matrix_multi(R_T, inertia_tensor), R) 105 | # I = math_op.matrix_multi(math_op.matrix_multi(R, inertia_tensor), R_T) 106 | inertia_list = [I[0][0], I[1][1], I[2][2], I[0][1], I[1][2], I[0][2]] 107 | 108 | return inertia_list # unit: kg*m^2 109 | 110 | def get_link_visual_name(link: Link) -> str: 111 | """ 112 | Return 113 | --------- 114 | visual_name: str 115 | """ 116 | visual_name = link.get_name() + "_visual" 117 | return visual_name 118 | 119 | def get_link_collision_name(link: Link) -> str: 120 | """ 121 | Return: 122 | collision_name: str 123 | """ 124 | collision_name = link.get_name() + "_collision" 125 | return collision_name 126 | 127 | def get_link_mesh_origin(link: Link) -> list[float]: 128 | """ 129 | Return: 130 | mesh_origin: [x, y, z, roll, pitch, yaw] 131 | unit: m, radian 132 | """ 133 | collision_name = link.get_name() + "_collision" 134 | return collision_name 135 | 136 | def get_link_visual_geo(link: Link) -> str: 137 | """ 138 | A trimesh file for visual geometry 139 | Return: 140 | --------- 141 | mesh_loc: str 142 | the path to find mesh file 143 | """ 144 | robot_name = constants.get_robot_name() 145 | visual_body = link.get_visual_body() 146 | col_body = link.get_collision_body() 147 | if (visual_body is None) and (col_body is None): 148 | # visual and collision geometry is same 149 | mesh_loc = "model://" + robot_name + "/meshes/" + link.get_name() + ".stl" 150 | return mesh_loc 151 | elif (visual_body is not None) and (col_body is not None): 152 | mesh_loc = "model://" + robot_name + "/meshes/" + link.get_name() + "_visual.stl" 153 | return mesh_loc 154 | elif (visual_body is None) and (col_body is not None): 155 | error_message = "Please set two bodies, one for visual and one for collision. \n" 156 | error_message = error_message + link.get_name() + " body for visual missing." 157 | utils.error_box(error_message) 158 | utils.terminate_box() 159 | elif (visual_body is not None) and (col_body is None): 160 | error_message = "Please set two bodies, one for visual and one for collision. \n" 161 | error_message = error_message + link.get_name() + " body for collision missing." 162 | utils.error_box(error_message) 163 | utils.terminate_box() 164 | 165 | def get_link_collision_geo(link: Link) -> str: 166 | """ 167 | A trimesh file for collision geometry 168 | Return: 169 | --------- 170 | mesh_loc: str 171 | the path to find mesh file 172 | """ 173 | robot_name = constants.get_robot_name() 174 | visual_body = link.get_visual_body() 175 | col_body = link.get_collision_body() 176 | if (visual_body is None) and (col_body is None): 177 | # visual and collision geometry is same 178 | mesh_loc = "model://" + robot_name + "/meshes/" + link.get_name() + ".stl" 179 | return mesh_loc 180 | elif (visual_body is not None) and (col_body is not None): 181 | mesh_loc = "model://" + robot_name + "/meshes/" + link.get_name() + "_collision.stl" 182 | return mesh_loc 183 | elif (visual_body is None) and (col_body is not None): 184 | error_message = "Please set two bodies, one for visual and one for collision. \n" 185 | error_message = error_message + link.get_name() + " body for visual missing." 186 | utils.error_box(error_message) 187 | utils.terminate_box() 188 | elif (visual_body is not None) and (col_body is None): 189 | error_message = "Please set two bodies, one for visual and one for collision. \n" 190 | error_message = error_message + link.get_name() + " body for collision missing." 191 | utils.error_box(error_message) 192 | utils.terminate_box() 193 | 194 | def get_link_element(link: Link) -> Element: 195 | """ 196 | Return 197 | --------- 198 | link_ele: Element 199 | xml elements contains informations for link 200 | """ 201 | link_ele = Element("link") 202 | link_ele.attrib = {"name": get_link_name(link)} 203 | 204 | # pose 205 | pose = SubElement(link_ele, "pose") 206 | pose.text = "{} {} {} {} {} {}".format(get_link_pose(link)[0], get_link_pose(link)[1], 207 | get_link_pose(link)[2], get_link_pose(link)[3], 208 | get_link_pose(link)[4], get_link_pose(link)[5]) 209 | 210 | # inertial 211 | inertial = SubElement(link_ele, "inertial") 212 | mass = SubElement(inertial, "mass") 213 | mass.text = str(get_link_mass(link)) 214 | CoM = SubElement(inertial, "pose") 215 | CoM.text = "{} {} {} {} {} {}".format(get_link_CoM(link)[0], get_link_CoM(link)[1], 216 | get_link_CoM(link)[2], get_link_CoM(link)[3], 217 | get_link_CoM(link)[4], get_link_CoM(link)[5]) 218 | inertia = SubElement(inertial, "inertia") 219 | ixx = SubElement(inertia, "ixx") 220 | iyy = SubElement(inertia, "iyy") 221 | izz = SubElement(inertia, "izz") 222 | ixy = SubElement(inertia, "ixy") 223 | iyz = SubElement(inertia, "iyz") 224 | ixz = SubElement(inertia, "ixz") 225 | ixx.text = "{}".format(get_link_inertia(link)[0]) 226 | iyy.text = "{}".format(get_link_inertia(link)[1]) 227 | izz.text = "{}".format(get_link_inertia(link)[2]) 228 | ixy.text = "{}".format(get_link_inertia(link)[3]) 229 | iyz.text = "{}".format(get_link_inertia(link)[4]) 230 | ixz.text = "{}".format(get_link_inertia(link)[5]) 231 | 232 | # visual 233 | visual = SubElement(link_ele, "visual") 234 | visual_name = get_link_visual_name(link) 235 | visual_uri = get_link_visual_geo(link) 236 | visual.attrib = {"name": visual_name} 237 | vis_geo = SubElement(visual, "geometry") 238 | vis_geo_mesh = SubElement(vis_geo, "mesh") 239 | vis_geo_mesh_uri = SubElement(vis_geo_mesh, "uri") 240 | vis_geo_mesh_uri.text = visual_uri 241 | vis_geo_mesh_scale = SubElement(vis_geo_mesh, "scale") 242 | vis_geo_mesh_scale.text = "0.001 0.001 0.001" 243 | 244 | # collision 245 | collision = SubElement(link_ele, "collision") 246 | collision_name = get_link_collision_name(link) 247 | collision_uri = get_link_collision_geo(link) 248 | collision.attrib = {"name": collision_name} 249 | col_geo = SubElement(collision, "geometry") 250 | col_geo_mesh = SubElement(col_geo, "mesh") 251 | col_geo_mesh_uri = SubElement(col_geo_mesh, "uri") 252 | col_geo_mesh_uri.text = collision_uri 253 | col_geo_mesh_scale = SubElement(col_geo_mesh, "scale") 254 | col_geo_mesh_scale.text = "0.001 0.001 0.001" 255 | 256 | return link_ele 257 | 258 | def get_joint_name(joint: Joint) -> str: 259 | """ 260 | Return 261 | --------- 262 | joint_name: str 263 | """ 264 | joint_name = joint.get_name() 265 | return joint_name 266 | 267 | def get_joint_type(joint: Joint) -> str: 268 | """ 269 | Return 270 | --------- 271 | joint_type: str 272 | fixed, revolute, prismatic, continuous 273 | currently, only support urdf joint types above 274 | """ 275 | # currently, only support these three sdf joint type 276 | sdf_joint_type_list = ["fixed", "revolute", "prismatic"] 277 | if joint.joint.jointMotion.jointType <= 2: 278 | sdf_joint_type = sdf_joint_type_list[joint.joint.jointMotion.jointType] 279 | # # TODO:It seems continuous joint type has some problem with gazebo 280 | # if sdf_joint_type == "revolute" and (self.get_limits() is None): 281 | # sdf_joint_type = "continuous" 282 | else: 283 | pass 284 | return sdf_joint_type 285 | 286 | def get_joint_pose(joint: Joint) -> list[float]: 287 | """ 288 | Return 289 | --------- 290 | joint_pose: [x, y, z, roll, pitch, yaw] 291 | From: http://sdformat.org/tutorials?tut=spec_model_kinematics&cat=specification&#jointpose 292 | For a joint with parent link frame `P` and child link frame `C`, 293 | the joint `` tag specifies the pose `X_CJc` of a joint frame `Jc` rigidly attached to the child link. 294 | """ 295 | # get parent joint origin's coordinate w.r.t world frame 296 | # get parent joint origin as the child joint frame 297 | # NOTE: joint origin is a concept in Fusion360 298 | if joint.joint.geometryOrOriginTwo == adsk.fusion.JointOrigin: 299 | w_P_Jc = joint.joint.geometryOrOriginTwo.geometry.origin.asArray() 300 | else: 301 | w_P_Jc = joint.joint.geometryOrOriginTwo.origin.asArray() 302 | 303 | # convert from cm to m 304 | w_P_Jc = [round(i*0.01, 6) for i in w_P_Jc] 305 | # get child link frame's origin point w.r.t world frame 306 | w_P_Lc = [joint.child.transform2.translation.x * 0.01, 307 | joint.child.transform2.translation.y * 0.01, 308 | joint.child.transform2.translation.z * 0.01,] 309 | # vector from child link frame's origin point to child joint origin point w.r.t world frame 310 | w_V_LcJc = [[w_P_Jc[0]-w_P_Lc[0]], 311 | [w_P_Jc[1]-w_P_Lc[1]], 312 | [w_P_Jc[2]-w_P_Lc[2]]] # 3*1 vector 313 | 314 | w_T_Lc = joint.child.transform2 # configuration of child-link-frame w.r.t world-frame w 315 | w_R_Lc = math_op.get_rotation_matrix(w_T_Lc) # rotation matrix of child-link-frame w.r.t world-frame w 316 | Lc_R_w = math_op.matrix_transpose(w_R_Lc) # rotation matrix of world-frame w.r.t child-link-frame Lc 317 | # vector from child link frame's origin point to child joint origin point w.r.t child-link-frame Lc 318 | Lc_V_LcJc = math_op.change_orientation(Lc_R_w, w_V_LcJc) # 3*1 array 319 | # assume the joint frame has the same oritation as child link frame 320 | # it seems that rpy of joint doesn't matter, so set them as 0 321 | sdf_origin = [Lc_V_LcJc[0][0], Lc_V_LcJc[1][0], Lc_V_LcJc[2][0], 0.0, 0.0, 0.0] 322 | 323 | return sdf_origin 324 | 325 | def get_joint_parent(joint: Joint) -> Link: 326 | """ 327 | Return 328 | --------- 329 | parent_link: Link 330 | parent link of the joint 331 | """ 332 | joint_parent: adsk.fusion.Occurrence = joint.parent 333 | parent_link: Link = Link(joint_parent) 334 | return parent_link 335 | 336 | def get_joint_child(joint: Joint) -> Link: 337 | """ 338 | Return 339 | --------- 340 | child_link: Link 341 | child link of the joint 342 | """ 343 | joint_child: adsk.fusion.Occurrence = joint.child 344 | child_link: Link = Link(joint_child) 345 | return child_link 346 | 347 | def get_joint_axis(joint: Joint) -> list[float]: 348 | """ 349 | Return 350 | --------- 351 | joint_axis: [x, y, z] 352 | joint axis w.r.t world frame 353 | if joint type is fixed, return None 354 | """ 355 | # Fusion360 api returns joint axis w.r.t world frame 356 | if joint.joint.jointMotion.jointType == 0: # RigidJointType 357 | axis = None 358 | return axis 359 | elif joint.joint.jointMotion.jointType == 1: # RevoluteJointType 360 | axis = [round(i, 6) for i in joint.joint.jointMotion.rotationAxisVector.asArray()] # In Fusion360, returned axis is normalized 361 | return axis 362 | elif joint.joint.jointMotion.jointType == 2: # SliderJointType 363 | axis = [round(i, 6) for i in joint.joint.jointMotion.slideDirectionVector.asArray()] # In Fusion360, returned axis is normalized 364 | return axis 365 | 366 | def get_joint_axis2(joint: Joint) -> list[float]: 367 | """ 368 | Return 369 | --------- 370 | joint_axis:2 [x, y, z] 371 | """ 372 | pass 373 | 374 | def get_joint_limit(joint: Joint) -> list[float]: 375 | """ 376 | Return 377 | --------- 378 | joint_limit: [lower, upper] 379 | """ 380 | if joint.joint.jointMotion.jointType == 0: # RigidJointType 381 | return None 382 | elif joint.joint.jointMotion.jointType == 1: # RevoluteJointType 383 | max_enabled = joint.joint.jointMotion.rotationLimits.isMaximumValueEnabled 384 | min_enabled = joint.joint.jointMotion.rotationLimits.isMinimumValueEnabled 385 | if max_enabled and min_enabled: 386 | # unit: radians 387 | lower_limit = joint.joint.jointMotion.rotationLimits.minimumValue 388 | upper_limit = joint.joint.jointMotion.rotationLimits.maximumValue 389 | return [round(lower_limit, 6), round(upper_limit, 6)] 390 | else: 391 | return None 392 | elif joint.joint.jointMotion.jointType == 2: # SliderJointType 393 | max_enabled = joint.joint.jointMotion.slideLimits.isMaximumValueEnabled 394 | min_enabled = joint.joint.jointMotion.slideLimits.isMinimumValueEnabled 395 | if max_enabled and min_enabled: 396 | lower_limit = joint.joint.jointMotion.slideLimits.minimumValue * 0.01 # cm -> m 397 | upper_limit = joint.joint.jointMotion.slideLimits.maximumValue * 0.01 # cm -> m 398 | return [round(lower_limit, 6), round(upper_limit, 6)] 399 | else: 400 | return None 401 | 402 | def get_joint_limit2(joint: Joint) -> list[float]: 403 | """ 404 | Return 405 | --------- 406 | joint_limit2: [lower, upper] 407 | """ 408 | pass 409 | 410 | def get_joint_element(joint: Joint) -> list[float]: 411 | """ 412 | Return 413 | --------- 414 | joint_element: Element 415 | xml elements contains informations for joint 416 | """ 417 | joint_ele = Element("joint") 418 | joint_ele.attrib = {"name": get_joint_name(joint), "type": get_joint_type(joint)} 419 | pose = SubElement(joint_ele, "pose") 420 | pose.text = "{} {} {} {} {} {}".format(get_joint_pose(joint)[0], get_joint_pose(joint)[1], 421 | get_joint_pose(joint)[2], get_joint_pose(joint)[3], 422 | get_joint_pose(joint)[4], get_joint_pose(joint)[5]) 423 | parent = SubElement(joint_ele, "parent") 424 | parent.text = "{}".format(get_joint_parent(joint).get_name()) 425 | child = SubElement(joint_ele, "child") 426 | child.text = "{}".format(get_joint_child(joint).get_name()) 427 | axis1 = get_joint_axis(joint) 428 | axis2 = get_joint_axis2(joint) 429 | if axis1 is not None: 430 | axis1_ele = SubElement(joint_ele, "axis") 431 | axis1_xyz = SubElement(axis1_ele, "xyz") 432 | # Fusion360 api returns joint axis w.r.t world frame, 433 | # which is the model frame here 434 | axis1_xyz.attrib = {"expressed_in": "__model__"} 435 | axis1_xyz.text = "{} {} {}".format(axis1[0], axis1[1], axis1[2]) 436 | joint_limit1 = get_joint_limit(joint) 437 | if joint_limit1 is not None: 438 | limit = SubElement(axis1_ele, "limit") 439 | lower = SubElement(limit, "lower") 440 | lower.text = str(joint_limit1[0]) 441 | upper = SubElement(limit, "upper") 442 | upper.text = str(joint_limit1[1]) 443 | if axis2 is not None: 444 | pass 445 | 446 | return joint_ele -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/link.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Get informations about links which are components from Fusion360 API 4 | 5 | """ 6 | import adsk, adsk.fusion, adsk.core 7 | from xml.etree.ElementTree import ElementTree, Element, SubElement 8 | from ..commands.ACDC4Robot import constants 9 | from . import utils 10 | from .joint import Joint 11 | from . import math_operation as math_op 12 | 13 | # TODO: support for dependent component 14 | # TODO: support for external component 15 | 16 | class Link(): 17 | """ 18 | Data abstruction for link element 19 | with some methods to access commonly used information 20 | 21 | Attributes: 22 | link: adsk.fusion.Occurrence 23 | occurrence in Fusion360 refers to linkage 24 | dir: str 25 | location of the exported files 26 | name: str 27 | identified name of the link 28 | pose: Matrix3D 29 | get by adsk.fusion.Occurrence.transform2 30 | Gets the 3d matrix data that defines this occurrences orientation and position in its assembly context 31 | the assembly context here is the root component's frame 32 | phyPro: adsk.fusion.PhysicalProperties 33 | object for accessing link's physical properties 34 | """ 35 | 36 | dir = "" # location of the exported file 37 | def __init__(self, occurrence: adsk.fusion.Occurrence) -> None: 38 | """ 39 | Initialize attributes for an object 40 | """ 41 | # TODO: maybe this is a better and accurate way for this, not test 42 | # self.link = occurrence.createForAssemblyContext(occurrence) 43 | 44 | self.link: adsk.fusion.Occurrence = occurrence 45 | self.name = None 46 | # pose can have two meanings: 47 | # 1. the homogeneous matrix of link-frame L w.r.t world-frame w 48 | # 2. the coordinates of link-frame L 49 | self.pose: adsk.core.Matrix3D = occurrence.transform2 50 | self.phyPro = occurrence.getPhysicalProperties(adsk.fusion.CalculationAccuracy.VeryHighCalculationAccuracy) 51 | 52 | def get_link_occ(self) -> adsk.fusion.Occurrence: 53 | """ 54 | Return: 55 | occ: adsk.fusion.Occurrence 56 | occurrence the link instance referenced 57 | """ 58 | return self.link 59 | 60 | def get_parent_joint(self) -> adsk.fusion.Joint: 61 | """ 62 | Get the parent joint of a robot link 63 | Every robot link can only have one parent joint, 64 | but can have multiple child joints to connect child links 65 | 66 | Return: 67 | j: parent_joints 68 | """ 69 | # TODO: in closed loop mechanism, there exits one assembly method that 70 | # make one link have two parent joint, write a instruction or fix this bug 71 | # text_palette = constants.get_text_palette() 72 | joint_list: adsk.fusion.JointList = self.link.joints 73 | # text_palette.writeText("Link: {}".format(self.get_name())) 74 | for j in joint_list: 75 | # text_palette.writeText("Joint list item: {}".format(j.name)) 76 | # text_palette.writeText("Joint's child link: {}".format(j.occurrenceOne.fullPathName)) 77 | if j.occurrenceOne == self.link: 78 | # text_palette.writeText("Return j type: {}".format(j.objectType)) 79 | return j 80 | else: 81 | continue 82 | 83 | # text_palette.writeText("Return a None") 84 | return None 85 | 86 | def get_name(self) -> str: 87 | """ 88 | Get the link valid name for link fullPathName 89 | Return: 90 | --------- 91 | name: str 92 | """ 93 | if self.link.component.name == "base_link": 94 | name = "base_link" 95 | else: 96 | name = utils.get_valid_filename(self.link.fullPathName) 97 | 98 | return name 99 | 100 | def get_pose_sdf(self) -> list: 101 | """ 102 | Return a pose(translation, rotation) expressed in the frame named by @relative_to. 103 | The first three components (x, y, z) represent the position of the element's origin(in the @relative_to frame) 104 | The rotation component represents the orientation of the element as either a sequence of Euler rotations(r, p, y) 105 | 106 | Return 107 | --------- 108 | pose: list 109 | [x, y, z, roll, pitch, yaw] 110 | """ 111 | pose = math_op.matrix3d_2_pose(self.pose) 112 | return pose 113 | 114 | def get_inertia_sdf(self) -> list: 115 | """ 116 | Return link's moments of inertia about Co(the link's center of mass) 117 | for the unit vectors Cx, Cy, Cz fixed in the center-of-mass-frame C. 118 | Note: the orientation of Cx, Cy, Cz relative to Lx, Ly, Lz which is specified 119 | by the `pose` tag. 120 | We set center-of-mass frame's orientation to be the same as link-frame L 121 | 122 | Return: 123 | --------- 124 | list: [ixx, iyy, izz, ixy, iyz, ixz] 125 | a inertia tensor w.r.t CoM frame which has the same orientation with parent frame 126 | """ 127 | (_, w_ixx, w_iyy, w_izz, w_ixy, w_iyz, w_ixz) = self.phyPro.getXYZMomentsOfInertia() # unit: kg*cm^2 128 | # Use parallel axis theorem to change inertia from w.r.t world-frame's origin to w.r.t CoM 129 | x = self.phyPro.centerOfMass.x * 0.01 # get the x coordinate w.r.t world-frame, unit: m 130 | y = self.phyPro.centerOfMass.y * 0.01 131 | z = self.phyPro.centerOfMass.z * 0.01 132 | mass = self.phyPro.mass # unit: kg 133 | com_ixx = w_ixx*0.0001 - mass*(y**2+z**2) # kg*cm^2 -> kg*m^2 134 | com_iyy = w_iyy*0.0001 - mass*(x**2+z**2) 135 | com_izz = w_izz*0.0001 - mass*(x**2+y**2) 136 | com_ixy = w_ixy*0.0001 + mass*(x*y) 137 | com_iyz = w_iyz*0.0001 + mass*(y*z) 138 | com_ixz = w_ixz*0.0001 + mass*(x*z) 139 | 140 | # Change inertia tensor to the orientation of link-frame L 141 | # reference: https://robot.sia.cn/CN/abstract/abstract374.shtml 142 | R = math_op.get_rotation_matrix(self.pose) 143 | R_T = math_op.matrix_transpose(R) 144 | inertia_tensor = [[com_ixx, com_ixy, com_ixz], 145 | [com_ixy, com_iyy, com_iyz], 146 | [com_ixz, com_iyz, com_izz]] 147 | 148 | # moments of inertia has the same orientation of L-frame 149 | I = math_op.matrix_multi(math_op.matrix_multi(R_T, inertia_tensor), R) 150 | # I = math_op.matrix_multi(math_op.matrix_multi(R, inertia_tensor), R_T) 151 | 152 | ixx = I[0][0] 153 | iyy = I[1][1] 154 | izz = I[2][2] 155 | ixy = I[0][1] 156 | iyz = I[1][2] 157 | ixz = I[0][2] 158 | 159 | return [ixx, iyy, izz, ixy, iyz, ixz] # unit: kg*m^2 160 | 161 | def get_initia_urdf(self): 162 | """ 163 | Return [ixx, iyy, izz, ixy, iyz, ixz] 164 | the inertia tensor elements of the link w.r.t the CoM frame C 165 | CoM frame C has the same orientation with the parent frame 166 | 167 | Return: 168 | --------- 169 | list: [ixx, iyy, izz, ixy, iyz, ixz] 170 | a inertia tensor w.r.t CoM frame which has the same orientation with parent frame 171 | """ 172 | # First, get the inertia tensor and CoM w.r.t the world frame 173 | (_, w_ixx, w_iyy, w_izz, w_ixy, w_iyz, w_ixz) = self.phyPro.getXYZMomentsOfInertia() # unit: kg*cm^2 174 | x = self.phyPro.centerOfMass.x * 0.01 # get the x coordinate w.r.t world-frame, unit: m 175 | y = self.phyPro.centerOfMass.y * 0.01 176 | z = self.phyPro.centerOfMass.z * 0.01 177 | 178 | # Use parallel axis theorem to change inertia from w.r.t world-frame's origin to w.r.t CoM 179 | mass = self.phyPro.mass # unit: kg 180 | com_ixx = w_ixx*0.0001 - mass*(y**2+z**2) # kg*cm^2 -> kg*m^2 181 | com_iyy = w_iyy*0.0001 - mass*(x**2+z**2) 182 | com_izz = w_izz*0.0001 - mass*(x**2+y**2) 183 | com_ixy = w_ixy*0.0001 + mass*(x*y) 184 | com_iyz = w_iyz*0.0001 + mass*(y*z) 185 | com_ixz = w_ixz*0.0001 + mass*(x*z) 186 | inertia_tensor = [[com_ixx, com_ixy, com_ixz], 187 | [com_ixy, com_iyy, com_iyz], 188 | [com_ixz, com_iyz, com_izz]] 189 | 190 | # get the parent frame 191 | parent_joint = self.get_parent_joint() 192 | # parent frame for a link has parent joint, is the parent joint frame 193 | # else, is the link frame itselt 194 | if parent_joint is None: 195 | parent_frame: adsk.core.Matrix3D = self.pose 196 | else: 197 | joint = Joint(parent_joint) 198 | parent_frame: adsk.core.Matrix3D = joint.get_joint_frame() 199 | 200 | # Change inertia tensor to the orientation of parent frame 201 | 202 | 203 | # # Get eigenvectors, which are the principal axes of the inertia tensor 204 | # _, eigenvectors = math_op.eigenvalues_and_eigenvectors(inertia_tensor) 205 | 206 | # # Get the rotation matrix represents the orientation of the principal axes 207 | # R = math_op.matrix_transpose(eigenvectors) 208 | # R_T = eigenvectors 209 | 210 | R = math_op.get_rotation_matrix(parent_frame) 211 | R_T = math_op.matrix_transpose(R) 212 | 213 | # Express inertia tersor w.r.t CoM frame C with axes Cx, Cy, Cz aligned with principal inertia directions 214 | I = math_op.matrix_multi(math_op.matrix_multi(R_T, inertia_tensor), R) 215 | ixx = I[0][0] 216 | iyy = I[1][1] 217 | izz = I[2][2] 218 | ixy = I[0][1] 219 | iyz = I[1][2] 220 | ixz = I[0][2] 221 | 222 | return [ixx, iyy, izz, ixy, iyz, ixz] # unit: kg*m^2 223 | 224 | def get_inertia_mjcf(self) -> list: 225 | """ 226 | Get inertia tensor as mjcf's body subelement 227 | 6 numbers in the following order: M(1,1), M(2,2), M(3,3), M(1,2), M(1,3), M(2,3). 228 | which is ixx, iyy, izz, ixy, ixz, iyz 229 | 230 | Return 231 | --------- 232 | inertia: list 233 | [ixx, iyy, izz, ixy, ixz, iyz] 234 | """ 235 | inertia = self.get_inertia_sdf() 236 | inertia = [inertia[0], inertia[1], inertia[2], inertia[3], inertia[5], inertia[4]] 237 | return inertia 238 | 239 | def get_mass(self) -> float: 240 | """ 241 | Get mass of the link, which is same in urdf, sdf, mjcf 242 | 243 | Return: 244 | --------- 245 | mass: float 246 | """ 247 | mass = self.phyPro.mass # kg 248 | return mass 249 | 250 | def get_CoM_wrt_link(self): 251 | """ 252 | Get CoM frame w.r.t link frame in [x, y, z roll, pitch, yaw] representation 253 | 254 | Return: 255 | pose_CoM: [x, y, z, roll, pitch, yaw] 256 | Center-of-mass frame C w.r.t to the link-frame L which contains the CoM 257 | """ 258 | # Let the orientation of center-of-mass frame C is same as link-frame L 259 | roll = 0.0 260 | pitch = 0.0 261 | yaw = 0.0 262 | w_CoM_x = self.phyPro.centerOfMass.x # point CoM's x coordinate w.r.t world-frame 263 | w_CoM_y = self.phyPro.centerOfMass.y 264 | w_CoM_z = self.phyPro.centerOfMass.z 265 | w_Lo_x = self.pose.translation.x # link-frame's origin point's x coordinat w.r.t world-frame 266 | w_Lo_y = self.pose.translation.y 267 | w_Lo_z = self.pose.translation.z 268 | # represent vector Lo-CoM which start from link-fram's origin point to CoM point w.r.t world-frame 269 | w_Lo_CoM = [[(w_CoM_x-w_Lo_x)*0.01], [(w_CoM_y-w_Lo_y)*0.01], [(w_CoM_z-w_Lo_z)*0.01]] # cm -> m 270 | # represent world-frame's orientation w.r.t link-frame 271 | L_R_w = [[self.pose.getCell(0, 0), self.pose.getCell(1, 0), self.pose.getCell(2, 0)], 272 | [self.pose.getCell(0, 1), self.pose.getCell(1, 1), self.pose.getCell(2, 1)], 273 | [self.pose.getCell(0, 2), self.pose.getCell(1, 2), self.pose.getCell(2, 2)]] 274 | 275 | L_Lo_CoM = math_op.change_orientation(L_R_w, w_Lo_CoM) 276 | pose_CoM = [L_Lo_CoM[0][0], L_Lo_CoM[1][0], L_Lo_CoM[2][0], roll, pitch, yaw] 277 | 278 | return pose_CoM 279 | 280 | def get_CoM_sdf(self): 281 | """ 282 | Get CoM frame w.r.t link frame in [x, y, z, roll, pitch, yaw] representation 283 | 284 | Return: 285 | pose_CoM: [x, y, z, roll, pitch, yaw] 286 | Center-of-mass frame C relative to the link-frame L 287 | """ 288 | # TODO: to avoid compatibility issues associated with the negative sign convention for product 289 | # of inertia, align CoM frame C's axes to the principal inertia directions, so that all the 290 | # products of inertia are zero 291 | 292 | # Let the orientation of center-of-mass frame C is same as link-frame L 293 | roll = 0.0 294 | pitch = 0.0 295 | yaw = 0.0 296 | w_CoM_x = self.phyPro.centerOfMass.x # point CoM's x coordinate w.r.t world-frame 297 | w_CoM_y = self.phyPro.centerOfMass.y 298 | w_CoM_z = self.phyPro.centerOfMass.z 299 | w_Lo_x = self.pose.translation.x # link-frame's origin point's x coordinat w.r.t world-frame 300 | w_Lo_y = self.pose.translation.y 301 | w_Lo_z = self.pose.translation.z 302 | # represent vector Lo-CoM which start from link-fram's origin point to CoM point w.r.t world-frame 303 | w_Lo_CoM = [[(w_CoM_x-w_Lo_x)*0.01], [(w_CoM_y-w_Lo_y)*0.01], [(w_CoM_z-w_Lo_z)*0.01]] # cm -> m 304 | # represent world-frame's orientation w.r.t link-frame 305 | L_R_w = [[self.pose.getCell(0, 0), self.pose.getCell(1, 0), self.pose.getCell(2, 0)], 306 | [self.pose.getCell(0, 1), self.pose.getCell(1, 1), self.pose.getCell(2, 1)], 307 | [self.pose.getCell(0, 2), self.pose.getCell(1, 2), self.pose.getCell(2, 2)]] 308 | 309 | L_Lo_CoM = math_op.change_orientation(L_R_w, w_Lo_CoM) 310 | pose_CoM = [L_Lo_CoM[0][0], L_Lo_CoM[1][0], L_Lo_CoM[2][0], roll, pitch, yaw] 311 | 312 | return pose_CoM 313 | 314 | def get_CoM_urdf(self): 315 | """ 316 | Get CoM frame w.r.t link frame in [x, y, z, roll, pitch, yaw] representation 317 | In URDF, parent link frame L is the parent joint frame Jp 318 | Return: 319 | --------- 320 | pose_CoM: [x, y, z, roll, pitch, yaw] 321 | """ 322 | if self.get_parent_joint() is None: 323 | # for the first link which does not have parent joint 324 | # do not know the CoM is w.r.t world frame or the link frame 325 | # here let it be w.r.t the link frame 326 | pose_CoM = self.get_CoM_sdf() 327 | else: 328 | textPalette: adsk.core.Palette = constants.get_text_palette() 329 | joint = Joint(self.get_parent_joint()) 330 | textPalette.writeText("Parent joint: {}".format(joint.get_name())) 331 | parent_joint_frame: adsk.core.Matrix3D = joint.get_joint_frame() 332 | from_origin, from_xAxis, from_yAxis, from_zAxis = parent_joint_frame.getAsCoordinateSystem() 333 | textPalette.writeText("Parent joint frame: \n"+math_op.matrix3D_to_str(parent_joint_frame)) 334 | 335 | w_CoM_x = self.phyPro.centerOfMass.x # point CoM's x coordinate w.r.t world-frame 336 | w_CoM_y = self.phyPro.centerOfMass.y 337 | w_CoM_z = self.phyPro.centerOfMass.z 338 | 339 | # Let the CoM-frame C has the same orientation with parent link frame 340 | CoM_frame_O: adsk.core.Point3D = adsk.core.Point3D.create() 341 | CoM_frame_O.set(w_CoM_x, w_CoM_y, w_CoM_z) 342 | CoM_frame: adsk.core.Matrix3D = adsk.core.Matrix3D.create() 343 | CoM_frame_x: adsk.core.Vector3D = from_xAxis 344 | CoM_frame_y: adsk.core.Vector3D = from_yAxis 345 | CoM_frame_z: adsk.core.Vector3D = from_zAxis 346 | CoM_frame.setWithCoordinateSystem(CoM_frame_O, CoM_frame_x, CoM_frame_y, CoM_frame_z) 347 | to_origin, to_xAsix, to_yAxis, to_zAxis = CoM_frame.getAsCoordinateSystem() 348 | textPalette.writeText("CoM frame: \n" + math_op.matrix3D_to_str(CoM_frame)) 349 | 350 | transform = adsk.core.Matrix3D.create() 351 | # transform.setToAlignCoordinateSystems(from_origin, from_xAxis, from_yAxis, from_zAxis, 352 | # to_origin, to_xAsix, to_yAxis, to_zAxis) 353 | transform = math_op.coordinate_transform(parent_joint_frame, CoM_frame) 354 | textPalette.writeText("Transform matrix: \n" + math_op.matrix3D_to_str(transform)) 355 | 356 | pose_CoM = math_op.matrix3d_2_pose(transform) 357 | 358 | return pose_CoM 359 | 360 | def get_mesh_origin(self): 361 | """ 362 | Fusion360 returns the mesh file with the same coordinate frame as the reference occurrence, which is the link fram L 363 | So the mesh origin transforms the parent-joint frame J to the link-frame L 364 | this is used for the mesh element of visual and collision in URDF 365 | Return: 366 | --------- 367 | mesh_orign: list 368 | [x, y, z, roll, pitch, yaw] 369 | """ 370 | if self.get_parent_joint() is None: 371 | # for the first link which does not have parent joint 372 | # haven't found the reference about how to define the mesh origin 373 | # it might be w.r.t world frame or link frame 374 | # here let it be w.r.t link frame, which coincide with the link frame 375 | mesh_origin = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 376 | # transform = self.pose 377 | # mesh_origin = math_op.matrix3d_2_pose(transform) 378 | else: 379 | joint = Joint(self.get_parent_joint()) 380 | parent_joint_frame: adsk.core.Matrix3D = joint.get_joint_frame() 381 | textPalette: adsk.core.Palette = constants.get_text_palette() 382 | textPalette.writeText("Parent joint frame: \n"+math_op.matrix3D_to_str(parent_joint_frame)) 383 | # link frame coincides with the mesh's frame 384 | link_frame: adsk.core.Matrix3D = self.pose 385 | textPalette.writeText("Link frame: \n" + math_op.matrix3D_to_str(link_frame)) 386 | from_origin, from_xAxis, from_yAxis, from_zAxis = parent_joint_frame.getAsCoordinateSystem() 387 | to_origin, to_xAsix, to_yAxis, to_zAxis = link_frame.getAsCoordinateSystem() 388 | 389 | transform = adsk.core.Matrix3D.create() 390 | # transform.setToAlignCoordinateSystems(from_origin, from_xAxis, from_yAxis, from_zAxis, 391 | # to_origin, to_xAsix, to_yAxis, to_zAxis) 392 | transform = math_op.coordinate_transform(parent_joint_frame, link_frame) 393 | # textPalette.writeText("Transform matrix: \n" + math_op.matrix3D_to_str(transform)) 394 | mesh_origin = math_op.matrix3d_2_pose(transform) 395 | 396 | return mesh_origin 397 | 398 | def get_collision_sdf(self): 399 | """ 400 | Get colision info for sdf collision element 401 | Since mesh frame coincide with the parent link frame 402 | Use the default position for colision mesh 403 | Return: 404 | --------- 405 | col_name: a string for the element's name attribute 406 | geometry_uri: a uri to locate the collisioin's mesh geometry 407 | """ 408 | col_name = str(self.get_name()) + "_collision" 409 | robot_name = constants.get_robot_name() 410 | geometry_uri = "model://" + robot_name + "/meshes/" + str(self.get_name()) + ".stl" 411 | return col_name, geometry_uri 412 | 413 | def get_collision_urdf(self): 414 | """ 415 | Get colision info for urdf collision element 416 | Return: 417 | --------- 418 | col_name: a string for the element's name attribute 419 | mesh_loc: the path to the mesh file 420 | """ 421 | col_name = self.get_name() + "_collision" 422 | # robot_name = constants.get_robot_name()s 423 | # mesh_loc = "package://" + robot_name + "/meshes/" + self.get_name() + ".stl" 424 | mesh_loc = "meshes/" + self.get_name() + ".stl" 425 | return col_name, mesh_loc 426 | 427 | def get_visual_sdf(self): 428 | """ 429 | Get colision info for sdf collision element 430 | Since mesh frame coincide with the parent link frame 431 | Use the default position for visualization mesh 432 | Return: 433 | --------- 434 | visual_name: a string for the element's name attribute 435 | geometry_uri: a uri to locate the visual's mesh geometry 436 | """ 437 | visual_name = self.get_name() + "_visual" 438 | robot_name = constants.get_robot_name() 439 | geometry_uri = "model://" + robot_name + "/meshes/" + str(self.get_name()) + ".stl" 440 | return visual_name, geometry_uri 441 | 442 | def get_visual_urdf(self): 443 | """ 444 | Return: 445 | visual_name: a string for the element's name attribute 446 | mesh_loc: str 447 | the path to the mesh file 448 | """ 449 | visual_name = self.get_name() + "_visual" 450 | robot_name = constants.get_robot_name() 451 | # mesh_loc = "package://" + robot_name + "/meshes/" + self.get_name() + ".stl" 452 | mesh_loc = "meshes/" + self.get_name() + ".stl" 453 | return visual_name, mesh_loc 454 | 455 | def get_visual_body(self) -> adsk.fusion.BRepBody: 456 | """ 457 | get body that contains 'visual' in its name 458 | which means they are used as visual geometry 459 | 460 | Return: 461 | visual_geo: BRepBody 462 | the body represents visual geometry 463 | None 464 | """ 465 | for body in self.link.bRepBodies: 466 | b_name: str = body.name 467 | if b_name.find('visual') != -1: 468 | visual_geo: adsk.fusion.BRepBody = body 469 | return visual_geo 470 | 471 | return None 472 | 473 | def get_collision_body(self) -> adsk.fusion.BRepBody: 474 | """ 475 | get body that contains 'collision' in its name 476 | which means they are used as collision geometry 477 | 478 | Return: 479 | col_geo: BRepBody 480 | the body represents collision geometry 481 | None 482 | """ 483 | for body in self.link.bRepBodies: 484 | b_name: str = body.name 485 | if b_name.find('collision') != -1: 486 | col_geo: adsk.fusion.BRepBody = body 487 | return col_geo 488 | 489 | return None 490 | -------------------------------------------------------------------------------- /Add-IN/ACDC4Robot/core/urdf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Nuofan 3 | # 20230811 4 | # Contains functions for generating urdf 5 | import adsk, adsk.core, adsk.fusion 6 | from .link import Link 7 | from .joint import Joint 8 | from typing import List 9 | from xml.etree.ElementTree import Element, SubElement 10 | import xml.etree.ElementTree as ET 11 | from . import math_operation as math_op 12 | from . import utils 13 | from ..commands.ACDC4Robot import constants 14 | from .robot import Robot 15 | 16 | class URDF(): 17 | 18 | def __init__(self, robot: Robot): 19 | self.robot = robot 20 | self.links: list[Link] = robot.get_links() 21 | self.tree_joints: list[Joint] = robot.get_tree_joints() 22 | 23 | def write_file(self, file_path): 24 | robot_ele = self.get_robot_ele() 25 | urdf_tree = ET.ElementTree(robot_ele) 26 | acdc4robot_info = self.get_acdc4robot_info() 27 | comment = ET.Comment(acdc4robot_info) 28 | robot_ele.append(comment) 29 | 30 | for link in self.links: 31 | link_ele = self.get_link_element(link) 32 | robot_ele.append(link_ele) 33 | 34 | for joint in self.tree_joints: 35 | joint_ele = self.get_joint_element(joint) 36 | robot_ele.append(joint_ele) 37 | 38 | # set indent to pretty the xml output 39 | ET.indent(urdf_tree, space=" ", level=0) 40 | 41 | urdf_tree.write(file_path, encoding="utf-8", xml_declaration=True) 42 | 43 | def get_acdc4robot_info(self,) -> str: 44 | info = "This URDF file is generated by ACDC4Robot: https://github.com/bionicdl-sustech/ACDC4Robot" 45 | return info 46 | 47 | def get_link_element(self, link: Link) -> Element: 48 | """ 49 | Return 50 | --------- 51 | link_ele: Element 52 | xml elements contains informations for link 53 | """ 54 | # create a link element 55 | link_ele = Element("link") 56 | link_ele.attrib = {"name": self.get_link_name(link)} 57 | 58 | # add inertia sub-element 59 | inertial = SubElement(link_ele, "inertial") 60 | origin = SubElement(inertial, "origin") 61 | origin.attrib = {"xyz": "{} {} {}".format(self.get_link_inertial_origin(link)[0], self.get_link_inertial_origin(link)[1], self.get_link_inertial_origin(link)[2]), 62 | "rpy": "{} {} {}".format(self.get_link_inertial_origin(link)[3], self.get_link_inertial_origin(link)[4], self.get_link_inertial_origin(link)[5])} 63 | mass = SubElement(inertial, "mass") 64 | mass.attrib = {"value": "{}".format(self.get_link_mass(link))} 65 | inertia = SubElement(inertial, "inertia") 66 | inertia.attrib = {"ixx": "{}".format(self.get_link_inertia(link)[0]), 67 | "iyy": "{}".format(self.get_link_inertia(link)[1]), 68 | "izz": "{}".format(self.get_link_inertia(link)[2]), 69 | "ixy": "{}".format(self.get_link_inertia(link)[3]), 70 | "iyz": "{}".format(self.get_link_inertia(link)[4]), 71 | "ixz": "{}".format(self.get_link_inertia(link)[5])} 72 | 73 | # add visual sub-element 74 | visual = SubElement(link_ele, "visual") 75 | visual.attrib = {"name": "{}".format(self.get_link_visual_name(link))} 76 | origin_v = SubElement(visual, "origin") 77 | origin_v.attrib = {"xyz": "{} {} {}".format(self.get_mesh_origin(link)[0], self.get_mesh_origin(link)[1], self.get_mesh_origin(link)[2]), 78 | "rpy": "{} {} {}".format(self.get_mesh_origin(link)[3], self.get_mesh_origin(link)[4], self.get_mesh_origin(link)[5])} 79 | geometry_v = SubElement(visual, "geometry") 80 | mesh_v = SubElement(geometry_v, "mesh") 81 | mesh_v.attrib = {"filename": self.get_link_visual_geo(link), "scale": "0.001 0.001 0.001"} 82 | 83 | # add collision sub-element 84 | collision = SubElement(link_ele, "collision") 85 | collision.attrib = {"name": "{}".format(self.get_link_collision_name(link))} 86 | origin_c = SubElement(collision, "origin") 87 | origin_c.attrib = {"xyz": "{} {} {}".format(self.get_mesh_origin(link)[0], self.get_mesh_origin(link)[1], self.get_mesh_origin(link)[2]), 88 | "rpy": "{} {} {}".format(self.get_mesh_origin(link)[3], self.get_mesh_origin(link)[4], self.get_mesh_origin(link)[5])} 89 | geometry_c = SubElement(collision, "geometry") 90 | mesh_c = SubElement(geometry_c, "mesh") 91 | mesh_c.attrib = {"filename": self.get_link_collision_geo(link), "scale": "0.001 0.001 0.001"} 92 | 93 | return link_ele 94 | 95 | def get_joint_element(self, joint: Joint) -> Element: 96 | """ 97 | Return 98 | --------- 99 | joint_ele: Element 100 | xml elements contains informations for joint 101 | """ 102 | joint_ele = Element("joint") 103 | joint_ele.attrib = {"name": self.get_joint_name(joint), 104 | "type": self.get_joint_type(joint)} 105 | 106 | # add joint origin element 107 | origin = SubElement(joint_ele, "origin") 108 | origin.attrib = {"xyz": "{} {} {}".format(self.get_joint_origin(joint)[0], self.get_joint_origin(joint)[1], self.get_joint_origin(joint)[2]), 109 | "rpy": "{} {} {}".format(self.get_joint_origin(joint)[3], self.get_joint_origin(joint)[4], self.get_joint_origin(joint)[5])} 110 | 111 | # add parent and child element 112 | parent = SubElement(joint_ele, "parent") 113 | parent.attrib = {"link": self.get_link_name(self.get_joint_parent(joint))} 114 | child = SubElement(joint_ele, "child") 115 | child.attrib = {"link": self.get_link_name(self.get_joint_child(joint))} 116 | 117 | # add axis1, urdf only has one axis for joint 118 | axis = self.get_joint_axis(joint) 119 | if axis is not None: 120 | axis_ele = SubElement(joint_ele, "axis") 121 | axis_ele.attrib = {"xyz": "{} {} {}".format(axis[0], axis[1], axis[2])} 122 | 123 | # add limits 124 | limit = self.get_joint_limit(joint) 125 | if limit is not None: 126 | limit_ele = SubElement(joint_ele, "limit") 127 | limit_ele.attrib = {"lower": "{}".format(limit[0]), 128 | "upper": "{}".format(limit[1]), 129 | "effort": "{}".format(limit[2]), 130 | "velocity": "{}".format(limit[3])} 131 | 132 | return joint_ele 133 | 134 | def get_robot_ele(self, ): 135 | """ 136 | Root element of URDF 137 | """ 138 | robot_ele = Element("robot") 139 | robot_ele.attrib = {"name": self.robot.get_robot_name()} 140 | 141 | return robot_ele 142 | 143 | 144 | def get_link_name(self, link: Link) -> str: 145 | """ 146 | Return 147 | --------- 148 | name: str 149 | link's full path name 150 | """ 151 | name = link.get_name() 152 | return name 153 | 154 | def get_link_inertial_origin(self, link: Link) -> list[float]: 155 | """ 156 | Return 157 | --------- 158 | inertial_origin: [x, y, z, roll, pitch, yaw] 159 | Pose (translation, rotation) of link's CoM frame C w.r.t link-frame L(parent-joint frame J) 160 | unit: m, radian 161 | """ 162 | # the link is the first link so called base-link 163 | if link.get_parent_joint() is None: 164 | # for the first link which does not have parent joint 165 | # do not know the CoM is w.r.t world frame or the link frame 166 | # do not found the details from the description, but according to the figure from: 167 | # http://wiki.ros.org/urdf/XML/model 168 | # it seems to be the link frame 169 | 170 | # Let the orientation of center-of-mass frame C is same as link-frame L 171 | roll = 0.0 172 | pitch = 0.0 173 | yaw = 0.0 174 | w_CoM_x = link.phyPro.centerOfMass.x # point CoM's x coordinate w.r.t world-frame 175 | w_CoM_y = link.phyPro.centerOfMass.y 176 | w_CoM_z = link.phyPro.centerOfMass.z 177 | w_Lo_x = link.pose.translation.x # link-frame's origin point's x coordinat w.r.t world-frame 178 | w_Lo_y = link.pose.translation.y 179 | w_Lo_z = link.pose.translation.z 180 | # represent vector Lo-CoM which start from link-fram's origin point to CoM point w.r.t world-frame 181 | w_Lo_CoM = [[(w_CoM_x-w_Lo_x)*0.01], [(w_CoM_y-w_Lo_y)*0.01], [(w_CoM_z-w_Lo_z)*0.01]] # cm -> m 182 | # represent world-frame's orientation w.r.t link-frame 183 | L_R_w = [[link.pose.getCell(0, 0), link.pose.getCell(1, 0), link.pose.getCell(2, 0)], 184 | [link.pose.getCell(0, 1), link.pose.getCell(1, 1), link.pose.getCell(2, 1)], 185 | [link.pose.getCell(0, 2), link.pose.getCell(1, 2), link.pose.getCell(2, 2)]] 186 | 187 | L_Lo_CoM = math_op.change_orientation(L_R_w, w_Lo_CoM) 188 | inertial_origin = [L_Lo_CoM[0][0], L_Lo_CoM[1][0], L_Lo_CoM[2][0], roll, pitch, yaw] 189 | # the link is not the first link 190 | else: 191 | # for the link is not the first link, CoM frame is w.r.t parent-joint frame J 192 | joint = Joint(link.get_parent_joint()) 193 | parent_joint_frame: adsk.core.Matrix3D = joint.get_joint_frame() 194 | from_origin, from_xAxis, from_yAxis, from_zAxis = parent_joint_frame.getAsCoordinateSystem() 195 | 196 | w_CoM_x = link.phyPro.centerOfMass.x # point CoM's x coordinate w.r.t world-frame 197 | w_CoM_y = link.phyPro.centerOfMass.y 198 | w_CoM_z = link.phyPro.centerOfMass.z 199 | 200 | # Let the CoM-frame C has the same orientation with parent link frame 201 | CoM_frame_O: adsk.core.Point3D = adsk.core.Point3D.create() 202 | CoM_frame_O.set(w_CoM_x, w_CoM_y, w_CoM_z) 203 | CoM_frame: adsk.core.Matrix3D = adsk.core.Matrix3D.create() 204 | CoM_frame_x: adsk.core.Vector3D = from_xAxis 205 | CoM_frame_y: adsk.core.Vector3D = from_yAxis 206 | CoM_frame_z: adsk.core.Vector3D = from_zAxis 207 | CoM_frame.setWithCoordinateSystem(CoM_frame_O, CoM_frame_x, CoM_frame_y, CoM_frame_z) 208 | to_origin, to_xAsix, to_yAxis, to_zAxis = CoM_frame.getAsCoordinateSystem() 209 | transform = adsk.core.Matrix3D.create() 210 | # transform.setToAlignCoordinateSystems(from_origin, from_xAxis, from_yAxis, from_zAxis, 211 | # to_origin, to_xAsix, to_yAxis, to_zAxis) 212 | transform = math_op.coordinate_transform(parent_joint_frame, CoM_frame) 213 | 214 | inertial_origin = math_op.matrix3d_2_pose(transform) 215 | 216 | return inertial_origin 217 | 218 | def get_link_mass(self, link: Link) -> float: 219 | """ 220 | Return 221 | --------- 222 | mass: float 223 | unit: kg 224 | """ 225 | mass = link.get_mass() 226 | return mass 227 | 228 | def get_link_inertia(self, link: Link) -> list[float]: 229 | """ 230 | Get the inertia tensor elements of the link w.r.t the CoM frame C, 231 | CoM frame C has the same orientation with the parent frame: 232 | link-frame L for the first link, else the parent joint frame J 233 | 234 | Return 235 | --------- 236 | inertia_list: [ixx, iyy, izz, ixy, iyz, ixz] 237 | unit: kg*m^2 238 | """ 239 | # First, get the inertia tensor and CoM w.r.t the world frame 240 | (_, w_ixx, w_iyy, w_izz, w_ixy, w_iyz, w_ixz) = link.phyPro.getXYZMomentsOfInertia() # unit: kg*cm^2 241 | x = link.phyPro.centerOfMass.x * 0.01 # get the x coordinate w.r.t world-frame, unit: m 242 | y = link.phyPro.centerOfMass.y * 0.01 243 | z = link.phyPro.centerOfMass.z * 0.01 244 | 245 | # Use parallel axis theorem to change inertia from w.r.t world-frame's origin to w.r.t CoM 246 | mass = link.phyPro.mass # unit: kg 247 | com_ixx = w_ixx*0.0001 - mass*(y**2+z**2) # kg*cm^2 -> kg*m^2 248 | com_iyy = w_iyy*0.0001 - mass*(x**2+z**2) 249 | com_izz = w_izz*0.0001 - mass*(x**2+y**2) 250 | com_ixy = w_ixy*0.0001 + mass*(x*y) 251 | com_iyz = w_iyz*0.0001 + mass*(y*z) 252 | com_ixz = w_ixz*0.0001 + mass*(x*z) 253 | inertia_tensor = [[com_ixx, com_ixy, com_ixz], 254 | [com_ixy, com_iyy, com_iyz], 255 | [com_ixz, com_iyz, com_izz]] 256 | 257 | # get the parent frame 258 | parent_joint = link.get_parent_joint() 259 | # parent frame for a link has parent joint, is the parent joint frame 260 | # else, is the link frame itselt 261 | if parent_joint is None: 262 | parent_frame: adsk.core.Matrix3D = link.pose 263 | else: 264 | joint = Joint(parent_joint) 265 | parent_frame: adsk.core.Matrix3D = joint.get_joint_frame() 266 | 267 | # Change inertia tensor to the orientation of parent frame 268 | R = math_op.get_rotation_matrix(parent_frame) 269 | R_T = math_op.matrix_transpose(R) 270 | 271 | I = math_op.matrix_multi(math_op.matrix_multi(R_T, inertia_tensor), R) 272 | inertia_list = [I[0][0], I[1][1], I[2][2], I[0][1], I[1][2], I[0][2]] 273 | 274 | return inertia_list # unit: kg*m^2 275 | 276 | def get_link_visual_name(self, link: Link) -> str: 277 | """ 278 | Return 279 | --------- 280 | visual_name: str 281 | """ 282 | visual_name = link.get_name() + "_visual" 283 | return visual_name 284 | 285 | def get_link_collision_name(self, link: Link) -> str: 286 | """ 287 | Return 288 | -------- 289 | collision_name: str 290 | """ 291 | collision_name = link.get_name() + "_collision" 292 | return collision_name 293 | 294 | def get_mesh_origin(self, link: Link) -> list[float]: 295 | """ 296 | The reference frame for mesh element of visual and collision w.r.t link frame L 297 | Return 298 | --------- 299 | mesh_origin: [x, y, z, roll, pitch, yaw] 300 | unit: m, radian 301 | """ 302 | if link.get_parent_joint() is None: 303 | # for the first link which does not have parent joint 304 | # the mesh frame coincides with the link frame L 305 | mesh_origin = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 306 | else: 307 | joint = Joint(link.get_parent_joint()) 308 | parent_joint_frame: adsk.core.Matrix3D = joint.get_joint_frame() 309 | # link frame coincides with the mesh's frame 310 | link_frame: adsk.core.Matrix3D = link.pose 311 | from_origin, from_xAxis, from_yAxis, from_zAxis = parent_joint_frame.getAsCoordinateSystem() 312 | to_origin, to_xAsix, to_yAxis, to_zAxis = link_frame.getAsCoordinateSystem() 313 | 314 | transform = adsk.core.Matrix3D.create() 315 | # transform.setToAlignCoordinateSystems(from_origin, from_xAxis, from_yAxis, from_zAxis, 316 | # to_origin, to_xAsix, to_yAxis, to_zAxis) 317 | transform = math_op.coordinate_transform(parent_joint_frame, link_frame) 318 | mesh_origin = math_op.matrix3d_2_pose(transform) 319 | 320 | return mesh_origin 321 | 322 | def get_link_visual_geo(self, link: Link) -> str: 323 | """ 324 | A dir to locate link's visual mesh file 325 | 326 | Return: 327 | mesh_loc: str 328 | the path to find mesh file 329 | """ 330 | visual_body = link.get_visual_body() 331 | col_body = link.get_collision_body() 332 | if (visual_body is None) and (col_body is None): 333 | # visual and collision geometry is same 334 | mesh_loc = "meshes/" + link.get_name() + ".stl" 335 | return mesh_loc 336 | elif (visual_body is not None) and (col_body is not None): 337 | mesh_loc = "meshes/" + link.get_name() + "_visual.stl" 338 | return mesh_loc 339 | elif (visual_body is None) and (col_body is not None): 340 | error_message = "Please set two bodies, one for visual and one for collision. \n" 341 | error_message = error_message + link.get_name() + " body for visual missing." 342 | utils.error_box(error_message) 343 | utils.terminate_box() 344 | elif (visual_body is not None) and (col_body is None): 345 | error_message = "Please set two bodies, one for visual and one for collision. \n" 346 | error_message = error_message + link.get_name() + " body for collision missing." 347 | utils.error_box(error_message) 348 | utils.terminate_box() 349 | 350 | def get_link_collision_geo(self, link: Link) -> str: 351 | """ 352 | A dir to locate link's collision mesh file 353 | 354 | Return: 355 | mesh_loc: str 356 | the path to find mesh file 357 | """ 358 | visual_body = link.get_visual_body() 359 | col_body = link.get_collision_body() 360 | if (visual_body is None) and (col_body is None): 361 | mesh_loc = "meshes/" + link.get_name() + ".stl" 362 | return mesh_loc 363 | elif (visual_body is not None) and (col_body is not None): 364 | mesh_loc = "meshes/" + link.get_name() + "_collision.stl" 365 | return mesh_loc 366 | elif (visual_body is None) and (col_body is not None): 367 | error_message = "Please set two bodies, one for visual and one for collision. \n" 368 | error_message = error_message + link.get_name() + " body for visual missing." 369 | utils.error_box(error_message) 370 | utils.terminate_box() 371 | elif (visual_body is not None) and (col_body is None): 372 | error_message = "Please set two bodies, one for visual and one for collision. \n" 373 | error_message = error_message + link.get_name() + " body for collision missing." 374 | utils.error_box(error_message) 375 | utils.terminate_box() 376 | 377 | 378 | def get_joint_name(self, joint: Joint) -> str: 379 | """ 380 | Return 381 | --------- 382 | joint_name: str 383 | """ 384 | joint_name = joint.get_name() 385 | return joint_name 386 | 387 | def get_joint_type(self, joint: Joint) -> str: 388 | """ 389 | Return 390 | --------- 391 | joint_type: str 392 | fixed, revolute, prismatic, continuous 393 | currently, only support urdf joint types above 394 | """ 395 | urdf_joint_type_list = ["fixed", "revolute", "prismatic"] 396 | if joint.joint.jointMotion.jointType <= 2: 397 | urdf_joint_type = urdf_joint_type_list[joint.joint.jointMotion.jointType] 398 | if urdf_joint_type == "revolute" and (self.get_joint_limit(joint) is None): 399 | urdf_joint_type = "continuous" 400 | else: 401 | # other joint types are not supported yet 402 | pass 403 | return urdf_joint_type 404 | 405 | def get_joint_origin(self, joint: Joint) -> list[float]: 406 | """ 407 | Return 408 | --------- 409 | joint_origin: [x, y, z, roll, pitch, yaw] 410 | describe the pose of the joint frame J w.r.t parent frame(parent link frame L or parent link's parent jont frame J) 411 | unit: m, radian 412 | """ 413 | parent_link: adsk.fusion.Occurrence = joint.parent 414 | parent_link: Link = Link(parent_link) 415 | parent_joint: adsk.fusion.Joint = parent_link.get_parent_joint() 416 | 417 | # get the parent frame 418 | if parent_joint is None: 419 | # if the parent link does not have parent joint(which means the root link), 420 | # then the parent link frame is the parent frame 421 | parent_frame: adsk.core.Matrix3D = parent_link.pose 422 | else: 423 | parent_joint: Joint = Joint(parent_joint) 424 | parent_frame = parent_joint.get_joint_frame() 425 | 426 | # get joint frame w.r.t world frame 427 | joint_frame = joint.get_joint_frame() 428 | 429 | transform = adsk.core.Matrix3D.create() 430 | transform = math_op.coordinate_transform(parent_frame, joint_frame) 431 | joint_origin = math_op.matrix3d_2_pose(transform) 432 | 433 | return joint_origin 434 | 435 | def get_joint_parent(self, joint: Joint) -> Link: 436 | """ 437 | Return 438 | --------- 439 | parent_link: Link 440 | parent link of the joint 441 | """ 442 | joint_parent: adsk.fusion.Occurrence = joint.parent 443 | parent_link: Link = Link(joint_parent) 444 | return parent_link 445 | 446 | def get_joint_child(self, joint: Joint) -> Link: 447 | """ 448 | Return 449 | --------- 450 | child_link: Link 451 | child link of the joint 452 | """ 453 | joint_child: adsk.fusion.Occurrence = joint.child 454 | child_link: Link = Link(joint_child) 455 | return child_link 456 | 457 | def get_joint_axis(self, joint: Joint) -> list[float]: 458 | """ 459 | Return 460 | --------- 461 | axis: [x, y, z] 462 | joint axis specified in the joint frame 463 | if joint type is rigid, return None 464 | """ 465 | # urdf joint axis is expressed w.r.t parent frame, which is the link frame or the parent joint frame 466 | if joint.joint.jointMotion.jointType == 0: # RigidJointType 467 | w_axis = None 468 | axis = None 469 | return axis 470 | elif joint.joint.jointMotion.jointType == 1: # RevoluteJointType 471 | w_axis = [round(i, 6) for i in joint.joint.jointMotion.rotationAxisVector.asArray()] # In Fusion360, returned axis is normalized 472 | elif joint.joint.jointMotion.jointType == 2: # SliderJointType 473 | w_axis = [round(i, 6) for i in joint.joint.jointMotion.slideDirectionVector.asArray()] # In Fusion360, returned axis is normalized 474 | 475 | J_axis = None 476 | joint_frame: adsk.core.Matrix3D = joint.get_joint_frame() 477 | w_R_J = math_op.get_rotation_matrix(joint_frame) # represent joint-frame J's orientation w.r.t world-frame w 478 | J_R_w = math_op.matrix_transpose(w_R_J) # for a rotation matrix, its inverse is its transpose 479 | 480 | w_axis = [[w_axis[0]], [w_axis[1]], [w_axis[2]]] # transform from 1*3 to 3*1 list 481 | J_axis = math_op.change_orientation(J_R_w, w_axis) 482 | axis = [J_axis[0][0], J_axis[1][0], J_axis[2][0]] 483 | 484 | return axis 485 | 486 | def get_joint_limit(self, joint: Joint) -> list[float]: 487 | """ 488 | required only for revolute and prismatic joint 489 | Return 490 | --------- 491 | limit: [lower, upper, effort, velocity] 492 | lower: optional, lower joint limit; unit: radian or m 493 | upper: optional, upper joint limit; unit: radian or m 494 | effort: required, the maximum joint effort, default: 1,000,000, unit: N or Nm 495 | velocity: required, the maximum joint velocity, default: 1,000,000, unit: m/s or rad/s 496 | if joint type is rigid, return None 497 | """ 498 | if joint.joint.jointMotion.jointType == 0: # RigidJointType 499 | return None 500 | elif joint.joint.jointMotion.jointType == 1: # RevoluteJointType 501 | max_enabled = joint.joint.jointMotion.rotationLimits.isMaximumValueEnabled 502 | min_enabled = joint.joint.jointMotion.rotationLimits.isMinimumValueEnabled 503 | if max_enabled and min_enabled: 504 | # unit: radians 505 | lower_limit = joint.joint.jointMotion.rotationLimits.minimumValue 506 | upper_limit = joint.joint.jointMotion.rotationLimits.maximumValue 507 | return [round(lower_limit, 6), round(upper_limit, 6), 1_000_000, 1_000_000] 508 | else: 509 | return None 510 | elif joint.joint.jointMotion.jointType == 2: # SliderJointType 511 | max_enabled = joint.joint.jointMotion.slideLimits.isMaximumValueEnabled 512 | min_enabled = joint.joint.jointMotion.slideLimits.isMinimumValueEnabled 513 | if max_enabled and min_enabled: 514 | lower_limit = joint.joint.jointMotion.slideLimits.minimumValue * 0.01 # cm -> m 515 | upper_limit = joint.joint.jointMotion.slideLimits.maximumValue * 0.01 # cm -> m 516 | return [round(lower_limit, 6), round(upper_limit, 6), 1_000_000, 1_000_000] 517 | else: 518 | return None 519 | 520 | 521 | 522 | 523 | def get_link_name(link: Link) -> str: 524 | """ 525 | Return 526 | --------- 527 | name: str 528 | link's full path name 529 | """ 530 | name = link.get_name() 531 | return name 532 | 533 | def get_link_inertial_origin(link: Link) -> list[float]: 534 | """ 535 | Return 536 | --------- 537 | inertial_origin: [x, y, z, roll, pitch, yaw] 538 | Pose (translation, rotation) of link's CoM frame C w.r.t link-frame L(parent-joint frame J) 539 | unit: m, radian 540 | """ 541 | # the link is the first link so called base-link 542 | if link.get_parent_joint() is None: 543 | # for the first link which does not have parent joint 544 | # do not know the CoM is w.r.t world frame or the link frame 545 | # do not found the details from the description, but according to the figure from: 546 | # http://wiki.ros.org/urdf/XML/model 547 | # it seems to be the link frame 548 | 549 | # Let the orientation of center-of-mass frame C is same as link-frame L 550 | roll = 0.0 551 | pitch = 0.0 552 | yaw = 0.0 553 | w_CoM_x = link.phyPro.centerOfMass.x # point CoM's x coordinate w.r.t world-frame 554 | w_CoM_y = link.phyPro.centerOfMass.y 555 | w_CoM_z = link.phyPro.centerOfMass.z 556 | w_Lo_x = link.pose.translation.x # link-frame's origin point's x coordinat w.r.t world-frame 557 | w_Lo_y = link.pose.translation.y 558 | w_Lo_z = link.pose.translation.z 559 | # represent vector Lo-CoM which start from link-fram's origin point to CoM point w.r.t world-frame 560 | w_Lo_CoM = [[(w_CoM_x-w_Lo_x)*0.01], [(w_CoM_y-w_Lo_y)*0.01], [(w_CoM_z-w_Lo_z)*0.01]] # cm -> m 561 | # represent world-frame's orientation w.r.t link-frame 562 | L_R_w = [[link.pose.getCell(0, 0), link.pose.getCell(1, 0), link.pose.getCell(2, 0)], 563 | [link.pose.getCell(0, 1), link.pose.getCell(1, 1), link.pose.getCell(2, 1)], 564 | [link.pose.getCell(0, 2), link.pose.getCell(1, 2), link.pose.getCell(2, 2)]] 565 | 566 | L_Lo_CoM = math_op.change_orientation(L_R_w, w_Lo_CoM) 567 | inertial_origin = [L_Lo_CoM[0][0], L_Lo_CoM[1][0], L_Lo_CoM[2][0], roll, pitch, yaw] 568 | # the link is not the first link 569 | else: 570 | # for the link is not the first link, CoM frame is w.r.t parent-joint frame J 571 | joint = Joint(link.get_parent_joint()) 572 | parent_joint_frame: adsk.core.Matrix3D = joint.get_joint_frame() 573 | from_origin, from_xAxis, from_yAxis, from_zAxis = parent_joint_frame.getAsCoordinateSystem() 574 | 575 | w_CoM_x = link.phyPro.centerOfMass.x # point CoM's x coordinate w.r.t world-frame 576 | w_CoM_y = link.phyPro.centerOfMass.y 577 | w_CoM_z = link.phyPro.centerOfMass.z 578 | 579 | # Let the CoM-frame C has the same orientation with parent link frame 580 | CoM_frame_O: adsk.core.Point3D = adsk.core.Point3D.create() 581 | CoM_frame_O.set(w_CoM_x, w_CoM_y, w_CoM_z) 582 | CoM_frame: adsk.core.Matrix3D = adsk.core.Matrix3D.create() 583 | CoM_frame_x: adsk.core.Vector3D = from_xAxis 584 | CoM_frame_y: adsk.core.Vector3D = from_yAxis 585 | CoM_frame_z: adsk.core.Vector3D = from_zAxis 586 | CoM_frame.setWithCoordinateSystem(CoM_frame_O, CoM_frame_x, CoM_frame_y, CoM_frame_z) 587 | to_origin, to_xAsix, to_yAxis, to_zAxis = CoM_frame.getAsCoordinateSystem() 588 | transform = adsk.core.Matrix3D.create() 589 | # transform.setToAlignCoordinateSystems(from_origin, from_xAxis, from_yAxis, from_zAxis, 590 | # to_origin, to_xAsix, to_yAxis, to_zAxis) 591 | transform = math_op.coordinate_transform(parent_joint_frame, CoM_frame) 592 | 593 | inertial_origin = math_op.matrix3d_2_pose(transform) 594 | 595 | return inertial_origin 596 | 597 | def get_link_mass(link: Link) -> float: 598 | """ 599 | Return 600 | --------- 601 | mass: float 602 | unit: kg 603 | """ 604 | mass = link.get_mass() 605 | return mass 606 | 607 | def get_link_inertia(link: Link) -> list[float]: 608 | """ 609 | Get the inertia tensor elements of the link w.r.t the CoM frame C, 610 | CoM frame C has the same orientation with the parent frame: 611 | link-frame L for the first link, else the parent joint frame J 612 | 613 | Return 614 | --------- 615 | inertia_list: [ixx, iyy, izz, ixy, iyz, ixz] 616 | unit: kg*m^2 617 | """ 618 | # First, get the inertia tensor and CoM w.r.t the world frame 619 | (_, w_ixx, w_iyy, w_izz, w_ixy, w_iyz, w_ixz) = link.phyPro.getXYZMomentsOfInertia() # unit: kg*cm^2 620 | x = link.phyPro.centerOfMass.x * 0.01 # get the x coordinate w.r.t world-frame, unit: m 621 | y = link.phyPro.centerOfMass.y * 0.01 622 | z = link.phyPro.centerOfMass.z * 0.01 623 | 624 | # Use parallel axis theorem to change inertia from w.r.t world-frame's origin to w.r.t CoM 625 | mass = link.phyPro.mass # unit: kg 626 | com_ixx = w_ixx*0.0001 - mass*(y**2+z**2) # kg*cm^2 -> kg*m^2 627 | com_iyy = w_iyy*0.0001 - mass*(x**2+z**2) 628 | com_izz = w_izz*0.0001 - mass*(x**2+y**2) 629 | com_ixy = w_ixy*0.0001 + mass*(x*y) 630 | com_iyz = w_iyz*0.0001 + mass*(y*z) 631 | com_ixz = w_ixz*0.0001 + mass*(x*z) 632 | inertia_tensor = [[com_ixx, com_ixy, com_ixz], 633 | [com_ixy, com_iyy, com_iyz], 634 | [com_ixz, com_iyz, com_izz]] 635 | 636 | # get the parent frame 637 | parent_joint = link.get_parent_joint() 638 | # parent frame for a link has parent joint, is the parent joint frame 639 | # else, is the link frame itselt 640 | if parent_joint is None: 641 | parent_frame: adsk.core.Matrix3D = link.pose 642 | else: 643 | joint = Joint(parent_joint) 644 | parent_frame: adsk.core.Matrix3D = joint.get_joint_frame() 645 | 646 | # Change inertia tensor to the orientation of parent frame 647 | R = math_op.get_rotation_matrix(parent_frame) 648 | R_T = math_op.matrix_transpose(R) 649 | 650 | I = math_op.matrix_multi(math_op.matrix_multi(R_T, inertia_tensor), R) 651 | inertia_list = [I[0][0], I[1][1], I[2][2], I[0][1], I[1][2], I[0][2]] 652 | 653 | return inertia_list # unit: kg*m^2 654 | 655 | def get_link_visual_name(link: Link) -> str: 656 | """ 657 | Return 658 | --------- 659 | visual_name: str 660 | """ 661 | visual_name = link.get_name() + "_visual" 662 | return visual_name 663 | 664 | def get_link_collision_name(link: Link) -> str: 665 | """ 666 | Return 667 | -------- 668 | collision_name: str 669 | """ 670 | collision_name = link.get_name() + "_collision" 671 | return collision_name 672 | 673 | def get_mesh_origin(link: Link) -> list[float]: 674 | """ 675 | The reference frame for mesh element of visual and collision w.r.t link frame L 676 | Return 677 | --------- 678 | mesh_origin: [x, y, z, roll, pitch, yaw] 679 | unit: m, radian 680 | """ 681 | if link.get_parent_joint() is None: 682 | # for the first link which does not have parent joint 683 | # the mesh frame coincides with the link frame L 684 | mesh_origin = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 685 | else: 686 | joint = Joint(link.get_parent_joint()) 687 | parent_joint_frame: adsk.core.Matrix3D = joint.get_joint_frame() 688 | # link frame coincides with the mesh's frame 689 | link_frame: adsk.core.Matrix3D = link.pose 690 | from_origin, from_xAxis, from_yAxis, from_zAxis = parent_joint_frame.getAsCoordinateSystem() 691 | to_origin, to_xAsix, to_yAxis, to_zAxis = link_frame.getAsCoordinateSystem() 692 | 693 | transform = adsk.core.Matrix3D.create() 694 | # transform.setToAlignCoordinateSystems(from_origin, from_xAxis, from_yAxis, from_zAxis, 695 | # to_origin, to_xAsix, to_yAxis, to_zAxis) 696 | transform = math_op.coordinate_transform(parent_joint_frame, link_frame) 697 | mesh_origin = math_op.matrix3d_2_pose(transform) 698 | 699 | return mesh_origin 700 | 701 | def get_link_visual_geo(link: Link) -> str: 702 | """ 703 | A dir to locate link's visual mesh file 704 | 705 | Return: 706 | mesh_loc: str 707 | the path to find mesh file 708 | """ 709 | visual_body = link.get_visual_body() 710 | col_body = link.get_collision_body() 711 | if (visual_body is None) and (col_body is None): 712 | # visual and collision geometry is same 713 | mesh_loc = "meshes/" + link.get_name() + ".stl" 714 | return mesh_loc 715 | elif (visual_body is not None) and (col_body is not None): 716 | mesh_loc = "meshes/" + link.get_name() + "_visual.stl" 717 | return mesh_loc 718 | elif (visual_body is None) and (col_body is not None): 719 | error_message = "Please set two bodies, one for visual and one for collision. \n" 720 | error_message = error_message + link.get_name() + " body for visual missing." 721 | utils.error_box(error_message) 722 | utils.terminate_box() 723 | elif (visual_body is not None) and (col_body is None): 724 | error_message = "Please set two bodies, one for visual and one for collision. \n" 725 | error_message = error_message + link.get_name() + " body for collision missing." 726 | utils.error_box(error_message) 727 | utils.terminate_box() 728 | 729 | def get_link_collision_geo(link: Link) -> str: 730 | """ 731 | A dir to locate link's collision mesh file 732 | 733 | Return: 734 | mesh_loc: str 735 | the path to find mesh file 736 | """ 737 | visual_body = link.get_visual_body() 738 | col_body = link.get_collision_body() 739 | if (visual_body is None) and (col_body is None): 740 | mesh_loc = "meshes/" + link.get_name() + ".stl" 741 | return mesh_loc 742 | elif (visual_body is not None) and (col_body is not None): 743 | mesh_loc = "meshes/" + link.get_name() + "_collision.stl" 744 | return mesh_loc 745 | elif (visual_body is None) and (col_body is not None): 746 | error_message = "Please set two bodies, one for visual and one for collision. \n" 747 | error_message = error_message + link.get_name() + " body for visual missing." 748 | utils.error_box(error_message) 749 | utils.terminate_box() 750 | elif (visual_body is not None) and (col_body is None): 751 | error_message = "Please set two bodies, one for visual and one for collision. \n" 752 | error_message = error_message + link.get_name() + " body for collision missing." 753 | utils.error_box(error_message) 754 | utils.terminate_box() 755 | 756 | def get_link_element(link: Link) -> Element: 757 | """ 758 | Return 759 | --------- 760 | link_ele: Element 761 | xml elements contains informations for link 762 | """ 763 | # create a link element 764 | link_ele = Element("link") 765 | link_ele.attrib = {"name": get_link_name(link)} 766 | 767 | # add inertia sub-element 768 | inertial = SubElement(link_ele, "inertial") 769 | origin = SubElement(inertial, "origin") 770 | origin.attrib = {"xyz": "{} {} {}".format(get_link_inertial_origin(link)[0], get_link_inertial_origin(link)[1], get_link_inertial_origin(link)[2]), 771 | "rpy": "{} {} {}".format(get_link_inertial_origin(link)[3], get_link_inertial_origin(link)[4], get_link_inertial_origin(link)[5])} 772 | mass = SubElement(inertial, "mass") 773 | mass.attrib = {"value": "{}".format(get_link_mass(link))} 774 | inertia = SubElement(inertial, "inertia") 775 | inertia.attrib = {"ixx": "{}".format(get_link_inertia(link)[0]), 776 | "iyy": "{}".format(get_link_inertia(link)[1]), 777 | "izz": "{}".format(get_link_inertia(link)[2]), 778 | "ixy": "{}".format(get_link_inertia(link)[3]), 779 | "iyz": "{}".format(get_link_inertia(link)[4]), 780 | "ixz": "{}".format(get_link_inertia(link)[5])} 781 | 782 | # add visual sub-element 783 | visual = SubElement(link_ele, "visual") 784 | visual.attrib = {"name": "{}".format(get_link_visual_name(link))} 785 | origin_v = SubElement(visual, "origin") 786 | origin_v.attrib = {"xyz": "{} {} {}".format(get_mesh_origin(link)[0], get_mesh_origin(link)[1], get_mesh_origin(link)[2]), 787 | "rpy": "{} {} {}".format(get_mesh_origin(link)[3], get_mesh_origin(link)[4], get_mesh_origin(link)[5])} 788 | geometry_v = SubElement(visual, "geometry") 789 | mesh_v = SubElement(geometry_v, "mesh") 790 | mesh_v.attrib = {"filename": get_link_visual_geo(link), "scale": "0.001 0.001 0.001"} 791 | 792 | # add collision sub-element 793 | collision = SubElement(link_ele, "collision") 794 | collision.attrib = {"name": "{}".format(get_link_collision_name(link))} 795 | origin_c = SubElement(collision, "origin") 796 | origin_c.attrib = {"xyz": "{} {} {}".format(get_mesh_origin(link)[0], get_mesh_origin(link)[1], get_mesh_origin(link)[2]), 797 | "rpy": "{} {} {}".format(get_mesh_origin(link)[3], get_mesh_origin(link)[4], get_mesh_origin(link)[5])} 798 | geometry_c = SubElement(collision, "geometry") 799 | mesh_c = SubElement(geometry_c, "mesh") 800 | mesh_c.attrib = {"filename": get_link_collision_geo(link), "scale": "0.001 0.001 0.001"} 801 | 802 | return link_ele 803 | 804 | 805 | def get_joint_name(joint: Joint) -> str: 806 | """ 807 | Return 808 | --------- 809 | joint_name: str 810 | """ 811 | joint_name = joint.get_name() 812 | return joint_name 813 | 814 | def get_joint_type(joint: Joint) -> str: 815 | """ 816 | Return 817 | --------- 818 | joint_type: str 819 | fixed, revolute, prismatic, continuous 820 | currently, only support urdf joint types above 821 | """ 822 | urdf_joint_type_list = ["fixed", "revolute", "prismatic"] 823 | if joint.joint.jointMotion.jointType <= 2: 824 | urdf_joint_type = urdf_joint_type_list[joint.joint.jointMotion.jointType] 825 | if urdf_joint_type == "revolute" and (get_joint_limit(joint) is None): 826 | urdf_joint_type = "continuous" 827 | else: 828 | # other joint types are not supported yet 829 | pass 830 | return urdf_joint_type 831 | 832 | def get_joint_origin(joint: Joint) -> list[float]: 833 | """ 834 | Return 835 | --------- 836 | joint_origin: [x, y, z, roll, pitch, yaw] 837 | describe the pose of the joint frame J w.r.t parent frame(parent link frame L or parent link's parent jont frame J) 838 | unit: m, radian 839 | """ 840 | parent_link: adsk.fusion.Occurrence = joint.parent 841 | parent_link: Link = Link(parent_link) 842 | parent_joint: adsk.fusion.Joint = parent_link.get_parent_joint() 843 | 844 | # get the parent frame 845 | if parent_joint is None: 846 | # if the parent link does not have parent joint(which means the root link), 847 | # then the parent link frame is the parent frame 848 | parent_frame: adsk.core.Matrix3D = parent_link.pose 849 | else: 850 | parent_joint: Joint = Joint(parent_joint) 851 | parent_frame = parent_joint.get_joint_frame() 852 | 853 | # get joint frame w.r.t world frame 854 | joint_frame = joint.get_joint_frame() 855 | 856 | transform = adsk.core.Matrix3D.create() 857 | transform = math_op.coordinate_transform(parent_frame, joint_frame) 858 | joint_origin = math_op.matrix3d_2_pose(transform) 859 | 860 | return joint_origin 861 | 862 | def get_joint_parent(joint: Joint) -> Link: 863 | """ 864 | Return 865 | --------- 866 | parent_link: Link 867 | parent link of the joint 868 | """ 869 | joint_parent: adsk.fusion.Occurrence = joint.parent 870 | parent_link: Link = Link(joint_parent) 871 | return parent_link 872 | 873 | def get_joint_child(joint: Joint) -> Link: 874 | """ 875 | Return 876 | --------- 877 | child_link: Link 878 | child link of the joint 879 | """ 880 | joint_child: adsk.fusion.Occurrence = joint.child 881 | child_link: Link = Link(joint_child) 882 | return child_link 883 | 884 | def get_joint_axis(joint: Joint) -> list[float]: 885 | """ 886 | Return 887 | --------- 888 | axis: [x, y, z] 889 | joint axis specified in the joint frame 890 | if joint type is rigid, return None 891 | """ 892 | # urdf joint axis is expressed w.r.t parent frame, which is the link frame or the parent joint frame 893 | if joint.joint.jointMotion.jointType == 0: # RigidJointType 894 | w_axis = None 895 | axis = None 896 | return axis 897 | elif joint.joint.jointMotion.jointType == 1: # RevoluteJointType 898 | w_axis = [round(i, 6) for i in joint.joint.jointMotion.rotationAxisVector.asArray()] # In Fusion360, returned axis is normalized 899 | elif joint.joint.jointMotion.jointType == 2: # SliderJointType 900 | w_axis = [round(i, 6) for i in joint.joint.jointMotion.slideDirectionVector.asArray()] # In Fusion360, returned axis is normalized 901 | 902 | J_axis = None 903 | joint_frame: adsk.core.Matrix3D = joint.get_joint_frame() 904 | w_R_J = math_op.get_rotation_matrix(joint_frame) # represent joint-frame J's orientation w.r.t world-frame w 905 | J_R_w = math_op.matrix_transpose(w_R_J) # for a rotation matrix, its inverse is its transpose 906 | 907 | w_axis = [[w_axis[0]], [w_axis[1]], [w_axis[2]]] # transform from 1*3 to 3*1 list 908 | J_axis = math_op.change_orientation(J_R_w, w_axis) 909 | axis = [J_axis[0][0], J_axis[1][0], J_axis[2][0]] 910 | 911 | return axis 912 | 913 | def get_joint_limit(joint: Joint) -> list[float]: 914 | """ 915 | required only for revolute and prismatic joint 916 | Return 917 | --------- 918 | limit: [lower, upper, effort, velocity] 919 | lower: optional, lower joint limit; unit: radian or m 920 | upper: optional, upper joint limit; unit: radian or m 921 | effort: required, the maximum joint effort, default: 1,000,000, unit: N or Nm 922 | velocity: required, the maximum joint velocity, default: 1,000,000, unit: m/s or rad/s 923 | if joint type is rigid, return None 924 | """ 925 | if joint.joint.jointMotion.jointType == 0: # RigidJointType 926 | return None 927 | elif joint.joint.jointMotion.jointType == 1: # RevoluteJointType 928 | max_enabled = joint.joint.jointMotion.rotationLimits.isMaximumValueEnabled 929 | min_enabled = joint.joint.jointMotion.rotationLimits.isMinimumValueEnabled 930 | if max_enabled and min_enabled: 931 | # unit: radians 932 | lower_limit = joint.joint.jointMotion.rotationLimits.minimumValue 933 | upper_limit = joint.joint.jointMotion.rotationLimits.maximumValue 934 | return [round(lower_limit, 6), round(upper_limit, 6), 1_000_000, 1_000_000] 935 | else: 936 | return None 937 | elif joint.joint.jointMotion.jointType == 2: # SliderJointType 938 | max_enabled = joint.joint.jointMotion.slideLimits.isMaximumValueEnabled 939 | min_enabled = joint.joint.jointMotion.slideLimits.isMinimumValueEnabled 940 | if max_enabled and min_enabled: 941 | lower_limit = joint.joint.jointMotion.slideLimits.minimumValue * 0.01 # cm -> m 942 | upper_limit = joint.joint.jointMotion.slideLimits.maximumValue * 0.01 # cm -> m 943 | return [round(lower_limit, 6), round(upper_limit, 6), 1_000_000, 1_000_000] 944 | else: 945 | return None 946 | 947 | def get_joint_element(joint: Joint) -> Element: 948 | """ 949 | Return 950 | --------- 951 | joint_ele: Element 952 | xml elements contains informations for joint 953 | """ 954 | joint_ele = Element("joint") 955 | joint_ele.attrib = {"name": get_joint_name(joint), 956 | "type": get_joint_type(joint)} 957 | 958 | # add joint origin element 959 | origin = SubElement(joint_ele, "origin") 960 | origin.attrib = {"xyz": "{} {} {}".format(get_joint_origin(joint)[0], get_joint_origin(joint)[1], get_joint_origin(joint)[2]), 961 | "rpy": "{} {} {}".format(get_joint_origin(joint)[3], get_joint_origin(joint)[4], get_joint_origin(joint)[5])} 962 | 963 | # add parent and child element 964 | parent = SubElement(joint_ele, "parent") 965 | parent.attrib = {"link": get_link_name(get_joint_parent(joint))} 966 | child = SubElement(joint_ele, "child") 967 | child.attrib = {"link": get_link_name(get_joint_child(joint))} 968 | 969 | # add axis1, urdf only has one axis for joint 970 | axis = get_joint_axis(joint) 971 | if axis is not None: 972 | axis_ele = SubElement(joint_ele, "axis") 973 | axis_ele.attrib = {"xyz": "{} {} {}".format(axis[0], axis[1], axis[2])} 974 | 975 | # add limits 976 | limit = get_joint_limit(joint) 977 | if limit is not None: 978 | limit_ele = SubElement(joint_ele, "limit") 979 | limit_ele.attrib = {"lower": "{}".format(limit[0]), 980 | "upper": "{}".format(limit[1]), 981 | "effort": "{}".format(limit[2]), 982 | "velocity": "{}".format(limit[3])} 983 | 984 | return joint_ele 985 | 986 | def get_urdf(joint: Joint, link: Link) -> Element: 987 | """ 988 | 989 | """ 990 | pass --------------------------------------------------------------------------------