├── .gitignore ├── CreateJoint.py ├── ExportSDF.py ├── ExportURDF.py ├── GazeboSDFExport.py ├── GazeboSDFExportStatic.py ├── Init.py ├── InitGui.py ├── Macros ├── JointCreator.FCMacro └── sdfExport.FCMacro ├── README.md ├── docs └── images │ ├── screenshot.png │ └── screenshot_rviz.png ├── icons ├── SDFexport.png ├── SDFexportStatic.png └── createJoint.png └── scripts └── initProject.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CreateJoint.py: -------------------------------------------------------------------------------- 1 | from FreeCAD import Gui 2 | from FreeCAD import Base 3 | import FreeCAD, FreeCADGui, Part 4 | import sys, os, math 5 | 6 | import FreeCAD as App 7 | 8 | import FreeCADGui 9 | import FreeCAD 10 | import Part 11 | from PySide import QtGui, QtCore 12 | 13 | def getComboView(): 14 | mw = Gui.getMainWindow() 15 | dw = mw.findChildren(QtGui.QDockWidget) 16 | for i in dw: 17 | if str(i.objectName()) == "Combo View": 18 | return i.findChild(QtGui.QTabWidget) 19 | elif str(i.objectName()) == "Python Console": 20 | return i.findChild(QtGui.QTabWidget) 21 | raise Exception("No tab widget found") 22 | 23 | 24 | class CreateRevoluteJointForm(QtGui.QDialog): 25 | """""" 26 | 27 | def __init__(self, parent_ok_callback): 28 | super(CreateRevoluteJointForm, self).__init__() 29 | self.parent_ok_callback = parent_ok_callback 30 | self.initUI() 31 | 32 | def initUI(self): 33 | 34 | self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) 35 | 36 | option1Button = QtGui.QPushButton("OK") 37 | option1Button.clicked.connect(self.onOK) 38 | option2Button = QtGui.QPushButton("Cancel") 39 | option2Button.clicked.connect(self.onCancel) 40 | 41 | labelPosition = QtGui.QLabel("Position") 42 | labelRotation = QtGui.QLabel("Rotation") 43 | 44 | hbox_pcj = QtGui.QVBoxLayout() 45 | self.parent_label = QtGui.QLabel("Parent") 46 | self.child_label = QtGui.QLabel("Child") 47 | self.joint_label = QtGui.QLabel("Joint") 48 | self.joint_type_select = QtGui.QComboBox() 49 | 50 | self.joint_type_select.addItems(['Revolute', 'Continuous', 'Prismatic', 'Fixed']) #, 'Floating', 'Planar']) 51 | 52 | self.button_set_to_selection = QtGui.QPushButton("Set to current selection") 53 | self.button_set_to_selection.clicked.connect(self.on_set_to_selection) 54 | 55 | hbox_pcj.addWidget(self.parent_label) 56 | hbox_pcj.addWidget(self.child_label) 57 | hbox_pcj.addWidget(self.joint_label) 58 | hbox_pcj.addWidget(self.joint_type_select) 59 | hbox_pcj.addWidget(self.button_set_to_selection) 60 | 61 | onlyDouble = QtGui.QDoubleValidator() 62 | self.posX = QtGui.QLineEdit("0") 63 | self.posX.setValidator(onlyDouble) 64 | self.posY = QtGui.QLineEdit("0") 65 | self.posY.setValidator(onlyDouble) 66 | self.posZ = QtGui.QLineEdit("0") 67 | self.posZ.setValidator(onlyDouble) 68 | self.rotX = QtGui.QLineEdit("1") 69 | self.rotX.setValidator(onlyDouble) 70 | self.rotY = QtGui.QLineEdit("0") 71 | self.rotY.setValidator(onlyDouble) 72 | self.rotZ = QtGui.QLineEdit("0") 73 | self.rotZ.setValidator(onlyDouble) 74 | 75 | # buttonBox = QtGui.QDialogButtonBox() 76 | buttonBox = QtGui.QDialogButtonBox(QtCore.Qt.Horizontal) 77 | buttonBox.addButton(option1Button, QtGui.QDialogButtonBox.ActionRole) 78 | buttonBox.addButton(option2Button, QtGui.QDialogButtonBox.ActionRole) 79 | # 80 | posLabelLayout = QtGui.QHBoxLayout() 81 | posLabelLayout.addWidget(labelPosition) 82 | 83 | posValueLayout = QtGui.QHBoxLayout() 84 | posValueLayout.addWidget(QtGui.QLabel("X")) 85 | posValueLayout.addWidget(self.posX) 86 | posValueLayout.addWidget(QtGui.QLabel("Y")) 87 | posValueLayout.addWidget(self.posY) 88 | posValueLayout.addWidget(QtGui.QLabel("Z")) 89 | posValueLayout.addWidget(self.posZ) 90 | 91 | rotLabelLayout = QtGui.QHBoxLayout() 92 | rotLabelLayout.addWidget(labelRotation) 93 | 94 | hbox = QtGui.QHBoxLayout() 95 | hbox.addWidget(QtGui.QLabel("X")) 96 | hbox.addWidget(self.rotX) 97 | hbox.addWidget(QtGui.QLabel("Y")) 98 | hbox.addWidget(self.rotY) 99 | hbox.addWidget(QtGui.QLabel("Z")) 100 | hbox.addWidget(self.rotZ) 101 | 102 | mainLayout = QtGui.QVBoxLayout() 103 | mainLayout.addLayout(hbox_pcj) 104 | mainLayout.addLayout(posLabelLayout) 105 | mainLayout.addLayout(posValueLayout) 106 | mainLayout.addLayout(rotLabelLayout) 107 | mainLayout.addLayout(hbox) 108 | mainLayout.addWidget(buttonBox) 109 | self.setLayout(mainLayout) 110 | self.form = mainLayout 111 | # define window xLoc,yLoc,xDim,yDim 112 | self.retStatus = 0 113 | 114 | def onOK(self): 115 | self.retStatus = 1 116 | self.parent_ok_callback() 117 | self.close() 118 | 119 | def onCancel(self): 120 | self.retStatus = 2 121 | self.close() 122 | 123 | def on_set_to_selection(self): 124 | selection = Gui.Selection.getSelection() 125 | sel_ex = Gui.Selection.getSelectionEx() 126 | if len(selection) == 1 and len(sel_ex) == 1 and sel_ex[0].HasSubObjects: 127 | sel_ex = sel_ex[0] 128 | if len(sel_ex.SubObjects) != 1: 129 | FreeCAD.Console.PrintMessage("Too many faces selected :/") 130 | else: 131 | face = sel_ex.SubObjects[0] 132 | # if type(face) != Part.Face: 133 | # FreeCAD.Console.PrintMessage("Need to select a face!") 134 | center = face.CenterOfMass 135 | normal = face.normalAt(0, 0) 136 | self.posX.setText(str(round(center[0], 6))) 137 | self.posY.setText(str(round(center[1], 6))) 138 | self.posZ.setText(str(round(center[2], 6))) 139 | self.rotX.setText(str(normal[0])) 140 | self.rotY.setText(str(normal[1])) 141 | self.rotZ.setText(str(normal[2])) 142 | 143 | for el in selection: 144 | print((el.Label)) 145 | self.parent_label.setText("Parent: " + selection[0].Label) 146 | self.child_label.setText("Child: " + selection[1].Label) 147 | if len(selection) == 3: 148 | self.joint_label.setText("Joint: " + selection[2].Label) 149 | else: 150 | self.joint_label.setText("Joint: ") 151 | 152 | 153 | class JointBase(object): 154 | 155 | def __init__(self, obj, parent, child, set_proxy=True): 156 | obj.addProperty("App::PropertyString", "Parent", "Joint").Parent = parent.Name 157 | obj.addProperty("App::PropertyString", "Child", "Joint").Child = child.Name 158 | if set_proxy: 159 | obj.Proxy = self 160 | 161 | def execute(self, fp): 162 | """"Print a short message when doing a recomputation, this method is mandatory" """ 163 | fp.Shape = Part.makeSphere(1) 164 | 165 | 166 | class LimitedJoint(JointBase): 167 | def __init__(self, obj, parent, child): 168 | super(LimitedJoint, self).__init__(obj, parent, child, False) 169 | 170 | obj.addProperty( 171 | "App::PropertyVector", "Axis", "Joint", "Axis of Rotation" 172 | ).Axis = FreeCAD.Vector(1, 0, 0) 173 | 174 | obj.addProperty("App::PropertyString", "LowerLimit", "Limits", "Lower Limit").LowerLimit = "" 175 | obj.addProperty("App::PropertyString", "UpperLimit", "Limits", "Upper Limit").UpperLimit = "" 176 | obj.addProperty("App::PropertyString", "EffortLimit", "Limits", "Effort Limit").EffortLimit = "100" 177 | obj.addProperty("App::PropertyString", "VelocityLimit", "Limits", "Velocity Limit").VelocityLimit = "100" 178 | obj.Proxy = self 179 | 180 | 181 | class RevoluteJoint(LimitedJoint): 182 | pass 183 | 184 | class PrismaticJoint(LimitedJoint): 185 | pass 186 | 187 | class FixedJoint(JointBase): 188 | pass 189 | 190 | class ContinuousJoint(JointBase): 191 | def __init__(self, obj, parent, child): 192 | super(ContinuousJoint, self).__init__(obj, parent, child, False) 193 | 194 | obj.addProperty( 195 | "App::PropertyVector", "Axis", "Joint", "Axis of Rotation" 196 | ).Axis = FreeCAD.Vector(1, 0, 0) 197 | obj.Proxy = self 198 | 199 | class ViewProviderJoint: 200 | def __init__(self, obj): 201 | """ Set this object to the proxy object of the actual view provider """ 202 | obj.ShapeColor = (1.0, 0.0, 0.0) 203 | obj.Proxy = self 204 | 205 | def getDefaultDisplayMode(self): 206 | """ Return the name of the default display mode. It must be defined in getDisplayModes. """ 207 | return "Flat Lines" 208 | 209 | 210 | class CreateJoint: 211 | """RC_CreateJoint""" 212 | 213 | def GetResources(self): 214 | print(FreeCAD.getUserAppDataDir() + "Mod" + "/RobotCreator/icons/createJoint.png") 215 | return { 216 | "Pixmap": os.path.join( 217 | str(FreeCAD.getUserAppDataDir()), 218 | "Mod", 219 | "RobotCreator/icons/createJoint.png", 220 | ), # the name of a svg file available in the resources 221 | "Accel": "Shift+j", # a default shortcut (optional) 222 | "MenuText": "Create a joint", 223 | "ToolTip": "Create a joint", 224 | } 225 | 226 | def Activated(self): 227 | print("creating a joint") 228 | 229 | self.selection = Gui.Selection.getSelection() 230 | if len(self.selection) >= 2: 231 | self.form = CreateRevoluteJointForm(self.ok_clicked) 232 | FreeCADGui.Control.showDialog(self.form) 233 | # self.tab = getComboView() 234 | # self.tab.addTab(self.form, "Create Joint") 235 | # self.tab.setCurrentWidget(self.form) 236 | else: 237 | print("Only support selection of two elements on two different objects") 238 | 239 | def ok_clicked(self): 240 | if self.form.retStatus == 1: 241 | jt = self.form.joint_type_select.currentText() 242 | if jt == 'Revolute': 243 | j = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "RevoluteJoint") 244 | RevoluteJoint(j, self.selection[0], self.selection[1]) 245 | elif jt == 'Prismatic': 246 | j = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PrismaticJoint") 247 | PrismaticJoint(j, self.selection[0], self.selection[1]) 248 | elif jt == 'Fixed': 249 | j = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "FixedJoint") 250 | FixedJoint(j, self.selection[0], self.selection[1]) 251 | elif jt == 'Continuous': 252 | j = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "ContinuousJoint") 253 | ContinuousJoint(j, self.selection[0], self.selection[1]) 254 | 255 | j.Placement.Base = FreeCAD.Vector( 256 | float(self.form.posX.text()), 257 | float(self.form.posY.text()), 258 | float(self.form.posZ.text()), 259 | ) 260 | 261 | if hasattr(j, 'Axis'): 262 | j.Axis = FreeCAD.Vector( 263 | float(self.form.rotX.text()), 264 | float(self.form.rotY.text()), 265 | float(self.form.rotZ.text()), 266 | ) 267 | 268 | ViewProviderJoint(j.ViewObject) 269 | App.ActiveDocument.recompute() 270 | 271 | elif self.form.retStatus == 2: 272 | print("abort") 273 | 274 | FreeCADGui.Control.closeDialog() 275 | # self.tab.removeTab(self.tab.indexOf(self.form)) 276 | 277 | def IsActive(self): 278 | """Here you can define if the command must be active or not (greyed) if certain conditions 279 | are met or not. This function is optional.""" 280 | return True 281 | 282 | 283 | FreeCADGui.addCommand("RC_CreateJoint", CreateJoint()) 284 | -------------------------------------------------------------------------------- /ExportSDF.py: -------------------------------------------------------------------------------- 1 | from FreeCAD import Gui 2 | from FreeCAD import Base 3 | import FreeCAD, FreeCADGui, Part, os, math 4 | 5 | 6 | class ExportSDF: 7 | """RC_ExportSDF""" 8 | 9 | def GetResources(self): 10 | return { 11 | "Pixmap": "My_Command_Icon", # the name of a svg file available in the resources 12 | "Accel": "Shift+E", # a default shortcut (optional) 13 | "MenuText": "Export Robot to SDF", 14 | "ToolTip": "Export Robot to SDF", 15 | } 16 | 17 | def Activated(self): 18 | print("Exporting SDF file") 19 | robotName = "testing" 20 | 21 | sdfFile = open(robotName + ".sdf", "w") 22 | sdfFile.write('\n') 23 | sdfFile.write('\n') 24 | sdfFile.write('\n') 25 | 26 | objs = FreeCAD.ActiveDocument.Objects 27 | for obj in objs: 28 | if obj.TypeId == "PartDesign::Body": 29 | name = obj.Name 30 | com = obj.Shape.CenterOfMass 31 | mass = obj.Shape.Mass 32 | inertia = obj.Shape.MatrixOfInertia 33 | pos = obj.Shape.Placement 34 | 35 | sdfFile.write('\n') 36 | sdfFile.write( 37 | " " 38 | + str(pos.Base[0] + com.x) 39 | + " " 40 | + str(pos.Base[1] + com.y) 41 | + " " 42 | + str(pos.Base[2] + com.z) 43 | + " " 44 | + str(pos.Rotation.toEuler()[0]) 45 | + " " 46 | + str(pos.Rotation.toEuler()[1]) 47 | + " " 48 | + str(pos.Rotation.toEuler()[2]) 49 | + "\n" 50 | ) 51 | sdfFile.write("\n") 52 | sdfFile.write( 53 | " " 54 | + str(pos.Base[0] + com.x) 55 | + " " 56 | + str(pos.Base[1] + com.y) 57 | + " " 58 | + str(pos.Base[2] + com.z) 59 | + " " 60 | + str(pos.Rotation.toEuler()[0]) 61 | + " " 62 | + str(pos.Rotation.toEuler()[1]) 63 | + " " 64 | + str(pos.Rotation.toEuler()[2]) 65 | + "\n" 66 | ) 67 | sdfFile.write("\n") 68 | sdfFile.write("" + str(inertia.A11 / 1000) + "\n") 69 | sdfFile.write("" + str(inertia.A12 / 1000) + "\n") 70 | sdfFile.write("" + str(inertia.A13 / 1000) + "\n") 71 | sdfFile.write("" + str(inertia.A22 / 1000) + "\n") 72 | sdfFile.write("" + str(inertia.A23 / 1000) + "\n") 73 | sdfFile.write("" + str(inertia.A33 / 1000) + "\n") 74 | sdfFile.write("\n") 75 | sdfFile.write("" + str(mass / 1000) + "\n") 76 | sdfFile.write("\n") 77 | sdfFile.write('\n') 78 | sdfFile.write("\n") 79 | sdfFile.write("\n") 80 | sdfFile.write( 81 | "model://" + robotName + "/meshes/" + name + ".stl\n" 82 | ) 83 | sdfFile.write("\n") 84 | sdfFile.write("\n") 85 | sdfFile.write("\n") 86 | sdfFile.write('\n') 87 | sdfFile.write("\n") 88 | sdfFile.write("\n") 89 | sdfFile.write( 90 | "model://" + robotName + "/meshes/" + name + ".stl\n" 91 | ) 92 | sdfFile.write("\n") 93 | sdfFile.write("\n") 94 | sdfFile.write("\n") 95 | sdfFile.write("\n") 96 | sdfFile.write("\n") 97 | sdfFile.write("\n") 98 | 99 | sdfFile.close() 100 | return 101 | 102 | def IsActive(self): 103 | """Here you can define if the command must be active or not (greyed) if certain conditions 104 | are met or not. This function is optional.""" 105 | return True 106 | 107 | 108 | FreeCADGui.addCommand("RC_ExportSDF", ExportSDF()) 109 | -------------------------------------------------------------------------------- /ExportURDF.py: -------------------------------------------------------------------------------- 1 | from FreeCAD import Gui 2 | from FreeCAD import Base 3 | import FreeCAD, FreeCADGui, Part 4 | from PySide import QtGui 5 | import Mesh, BuildRegularGeoms 6 | import os, sys, math 7 | 8 | sys.path.append('/home/wolfv/.FreeCAD/Mod/') 9 | from bs4 import BeautifulSoup 10 | 11 | def float_to_str(f): 12 | return "{:.20f}".format(f) 13 | 14 | 15 | def deg2rad(a): 16 | return a * 0.01745329252 17 | 18 | 19 | def str2obj(a): 20 | return FreeCAD.ActiveDocument.getObject(a) 21 | 22 | 23 | def bodyFromPad(a): 24 | objs = FreeCAD.ActiveDocument.Objects 25 | for obj in objs: 26 | if obj.TypeId == "PartDesign::Body": 27 | if obj.hasObject(a): 28 | return obj 29 | 30 | 31 | def bodyLabelFromObjStr(a): 32 | b = FreeCAD.ActiveDocument.getObject(a) 33 | if b is not None: 34 | return b.Label 35 | else: 36 | return "##NONE##" 37 | 38 | joint_mapping = { 39 | 'RevoluteJoint': 'revolute', 40 | 'PrismaticJoint': 'prismatic', 41 | 'FixedJoint': 'fixed', 42 | 'ContinuousJoint': 'continuous' 43 | } 44 | 45 | def get_parent_frame(obj): 46 | parent = obj.Parent 47 | 48 | # b = FreeCAD.ActiveDocument.getObject() 49 | 50 | objs = FreeCAD.ActiveDocument.Objects 51 | for o in objs: 52 | if "Joint" in o.Name: 53 | if o.Child == parent: 54 | return o 55 | return None 56 | 57 | def get_parent_joint(obj): 58 | link_name = obj.Name 59 | # b = FreeCAD.ActiveDocument.getObject() 60 | 61 | objs = FreeCAD.ActiveDocument.Objects 62 | for o in objs: 63 | if "Joint" in o.Name: 64 | if o.Child == link_name: 65 | return o 66 | return None 67 | 68 | class URDFExportStatic: 69 | """RC_URDFExport""" 70 | 71 | def GetResources(self): 72 | return { 73 | "Pixmap": str( 74 | FreeCAD.getUserAppDataDir() 75 | + "Mod" 76 | + "/RobotCreator/icons/SDFexportStatic.png" 77 | ), # the name of a svg file available in the resources 78 | "Accel": "Shift+u", # a default shortcut (optional) 79 | "MenuText": "Export URDF", 80 | "ToolTip": "Export URDF", 81 | } 82 | 83 | def Activated(self): 84 | 85 | selected_directory = QtGui.QFileDialog.getExistingDirectory() 86 | 87 | soup = BeautifulSoup(features="xml") 88 | robot = soup.new_tag('robot') 89 | robot.attrs['name'] = "testing" 90 | soup.append(robot) 91 | 92 | # you might want to change this to where you want your exported mesh/sdf to be located. 93 | self.robot_name = "testing" 94 | self.output_folder = os.path.expanduser(selected_directory) 95 | 96 | if not os.path.exists(self.output_folder): 97 | os.makedirs(self.output_folder) 98 | 99 | self.srdf_file_path = os.path.join(self.output_folder, 'test.urdf') 100 | self.mesh_path = os.path.join(self.output_folder, 'meshes/') 101 | 102 | if not os.path.exists(self.mesh_path): 103 | os.makedirs(self.mesh_path) 104 | 105 | self.srdf_file = open(self.srdf_file_path, "w") 106 | 107 | objs = FreeCAD.ActiveDocument.Objects 108 | for obj in objs: 109 | if "Joint" in obj.Name: 110 | pos = obj.Shape.Placement 111 | computed_pos = pos.Base * 0.001 112 | 113 | parent_frame = get_parent_frame(obj) 114 | if parent_frame: 115 | computed_pos -= (parent_frame.Shape.Placement.Base * 0.001) 116 | 117 | joint_type = None 118 | for jt_loop in joint_mapping: 119 | if obj.Name.startswith(jt_loop): 120 | joint_type = joint_mapping[jt_loop] 121 | break 122 | 123 | if not joint_type: 124 | raise RuntimeError("No Joint Type found!") 125 | 126 | joint_elem = soup.new_tag('joint', type=joint_type) 127 | 128 | if hasattr(obj, 'Label') and obj.Label != '': 129 | joint_elem.attrs['name'] = obj.Label 130 | else: 131 | joint_elem.attrs['name'] = bodyLabelFromObjStr(obj.Parent) + bodyLabelFromObjStr(obj.Child) 132 | 133 | joint_origin = soup.new_tag('origin', 134 | xyz=" ".join([str(x) for x in computed_pos]), 135 | rpy=" ".join([str(x) for x in pos.Rotation.toEuler()[::-1]]) 136 | ) 137 | joint_elem.append(joint_origin) 138 | 139 | parent_tag = soup.new_tag('parent', link=bodyLabelFromObjStr(obj.Parent)) 140 | joint_elem.append(parent_tag) 141 | 142 | child_tag = soup.new_tag('child', link=bodyLabelFromObjStr(obj.Child)) 143 | joint_elem.append(child_tag) 144 | 145 | if hasattr(obj, 'Axis'): 146 | axis = soup.new_tag('axis', xyz=" ".join((str(x) for x in obj.Axis))) 147 | joint_elem.append(axis) 148 | 149 | if joint_type in ('revolute', 'prismatic'): 150 | limit_tag = soup.new_tag('limit', 151 | velocity=obj.VelocityLimit, 152 | effort=obj.EffortLimit 153 | ) 154 | 155 | if obj.UpperLimit: 156 | limit_tag['upper'] = obj.UpperLimit 157 | if obj.LowerLimit: 158 | limit_tag['lower'] = obj.LowerLimit 159 | joint_elem.append(limit_tag) 160 | 161 | robot.append(joint_elem) 162 | 163 | if obj.TypeId == "PartDesign::Body" or obj.TypeId == "Part::Box" or obj.TypeId == "Part::Feature": 164 | name = obj.Label 165 | mass = obj.Shape.Mass 166 | inertia = obj.Shape.MatrixOfInertia 167 | pos = obj.Shape.Placement 168 | com = obj.Shape.CenterOfMass 169 | com *= 0.001 170 | mass *= 0.001 171 | A11 = inertia.A11 * 0.000000001 172 | A12 = inertia.A12 * 0.000000001 173 | A13 = inertia.A13 * 0.000000001 174 | A22 = inertia.A22 * 0.000000001 175 | A23 = inertia.A23 * 0.000000001 176 | A33 = inertia.A33 * 0.000000001 177 | 178 | pos.Base *= 0.001 179 | 180 | # export shape as mesh (stl) 181 | mesh_file_name = os.path.join(self.mesh_path, name + ".stl") 182 | # disable export of mesh for now 183 | if not os.path.exists(mesh_file_name): 184 | Mesh.export([obj], mesh_file_name) 185 | 186 | print("About to scale {}".format(mesh_file_name)) 187 | # import stl and translate/scale 188 | # scaling, millimeter -> meter 189 | mesh = Mesh.read(mesh_file_name) 190 | 191 | pj = get_parent_joint(obj) 192 | if pj: 193 | mesh.translate(*-pj.Shape.Placement.Base) 194 | 195 | mat = FreeCAD.Matrix() 196 | mat.scale(0.001, 0.001, 0.001) 197 | 198 | 199 | # apply scaling 200 | mesh.transform(mat) 201 | 202 | # # move origo to center of mass 203 | # pos.move(com * -1) 204 | # mesh.Placement.Base -= mesh.CenterOfMass 205 | # mesh.Placement.Base = (0, 0,) 206 | 207 | # save scaled and transformed mesh as stl 208 | mesh.write(mesh_file_name) 209 | 210 | link = soup.new_tag('link') 211 | link.attrs['name'] = name 212 | 213 | # link_pose = soup.new_tag('pose') 214 | # link_pose.string = "0 0 0 {} {} {} {}".format(*[deg2rad(x) for x in pos.Rotation.Q]) 215 | # link.append(link_pose) 216 | 217 | visual = soup.new_tag('visual') 218 | # visual_origin = soup.new_tag('origin', xyz=" ".join(pos.Base), rpy=" ".join(pos.Rotation.toEuler()[::-1])) 219 | visual_origin = soup.new_tag('origin', xyz="0 0 0", rpy="0 0 0") 220 | visual.append(visual_origin) 221 | 222 | visual_geom = soup.new_tag('geometry') 223 | visual_mesh = soup.new_tag('mesh', filename='file://' + mesh_file_name) 224 | visual_geom.append(visual_mesh) 225 | visual.append(visual_geom) 226 | 227 | link.append(visual) 228 | 229 | robot.append(link) 230 | 231 | # self.srdf_file.write("\n") 232 | # self.srdf_file.write( 233 | # " " 234 | # + str(0 + com.x) 235 | # + " " 236 | # + str(0 + com.y) 237 | # + " " 238 | # + str(0 + com.z) 239 | # + " " 240 | # + str(deg2rad(pos.Rotation.Q[0])) 241 | # + " " 242 | # + str(deg2rad(pos.Rotation.Q[1])) 243 | # + " " 244 | # + str(deg2rad(pos.Rotation.Q[2])) 245 | # + "\n" 246 | # ) 247 | # self.srdf_file.write("\n") 248 | # self.srdf_file.write("" + float_to_str(A11) + "\n") 249 | # self.srdf_file.write("" + float_to_str(A12) + "\n") 250 | # self.srdf_file.write("" + float_to_str(A13) + "\n") 251 | # self.srdf_file.write("" + float_to_str(A22) + "\n") 252 | # self.srdf_file.write("" + float_to_str(A23) + "\n") 253 | # self.srdf_file.write("" + float_to_str(A33) + "\n") 254 | # self.srdf_file.write("\n") 255 | # self.srdf_file.write("" + str(mass) + "\n") 256 | # self.srdf_file.write("\n") 257 | 258 | # self.srdf_file.write('\n') 259 | # self.srdf_file.write("\n") 260 | # self.srdf_file.write("\n") 261 | # self.srdf_file.write( 262 | # "model://" 263 | # + self.robot_name 264 | # + "Static/meshes/" 265 | # + name 266 | # + ".stl\n" 267 | # ) 268 | # self.srdf_file.write("\n") 269 | # self.srdf_file.write("\n") 270 | # self.srdf_file.write("\n") 271 | # self.srdf_file.write('\n') 272 | # self.srdf_file.write("\n") 273 | # self.srdf_file.write("\n") 274 | # self.srdf_file.write( 275 | # "model://" 276 | # + self.robot_name 277 | # + "Static/meshes/" 278 | # + name 279 | # + ".stl\n" 280 | # ) 281 | # self.srdf_file.write("\n") 282 | # self.srdf_file.write("\n") 283 | # self.srdf_file.write("\n") 284 | # self.srdf_file.write("\n") 285 | 286 | self.srdf_file.write(soup.prettify()) 287 | self.srdf_file.close() 288 | return 289 | 290 | def IsActive(self): 291 | """Here you can define if the command must be active or not (greyed) if certain conditions 292 | are met or not. This function is optional.""" 293 | return True 294 | 295 | def export_urdf(): 296 | c = URDFExportStatic() 297 | c.Activated() 298 | return True 299 | 300 | 301 | FreeCADGui.addCommand("RC_ExportURDF", URDFExportStatic()) 302 | 303 | 304 | -------------------------------------------------------------------------------- /GazeboSDFExport.py: -------------------------------------------------------------------------------- 1 | from FreeCAD import Gui 2 | from FreeCAD import Base 3 | import FreeCAD, FreeCADGui, Part, os, math 4 | import Mesh, BuildRegularGeoms 5 | import os, sys, Mesh 6 | 7 | 8 | def float_to_str(f): 9 | return "{:.20f}".format(f) 10 | 11 | 12 | def deg2rad(a): 13 | return a * 0.01745329252 14 | 15 | 16 | def str2obj(a): 17 | return FreeCAD.ActiveDocument.getObject(a) 18 | 19 | 20 | def bodyFromPad(a): 21 | objs = FreeCAD.ActiveDocument.Objects 22 | for obj in objs: 23 | if obj.TypeId == "PartDesign::Body": 24 | if obj.hasObject(a): 25 | return obj 26 | 27 | 28 | def bodyLabelFromObjStr(a): 29 | b = str2obj(a) 30 | c = bodyFromPad(b) 31 | return c.Label 32 | 33 | 34 | class GazeboSDFExport: 35 | """GazeboSDFExport""" 36 | 37 | def GetResources(self): 38 | return { 39 | "Pixmap": str( 40 | FreeCAD.getUserAppDataDir() 41 | + "Mod" 42 | + "/RobotCreator/icons/SDFexport.png" 43 | ), # the name of a svg file available in the resources 44 | "Accel": "Shift+S", # a default shortcut (optional) 45 | "MenuText": "Gazebo SDF export", 46 | "ToolTip": "Exports SDF to Gazebo", 47 | } 48 | 49 | def Activated(self): 50 | print("Scaling mesh") 51 | 52 | # you might want to change this to where you want your exported mesh/sdf to be located. 53 | robotName = "testing" 54 | # projPath = "/home/maiden/Projects/RobotCreator/"+robotName + "/" 55 | projPath = os.path.expanduser("~") + "/.gazebo/models/" + robotName + "/" 56 | meshPath = projPath + "meshes/" 57 | 58 | if not os.path.exists(projPath): 59 | os.makedirs(projPath) 60 | 61 | if not os.path.exists(meshPath): 62 | os.makedirs(meshPath) 63 | 64 | # os.chdir(projectName) 65 | 66 | sdfFile = open(projPath + robotName + ".sdf", "w") 67 | sdfFile.write('\n') 68 | sdfFile.write('\n') 69 | sdfFile.write('\n') 70 | 71 | objs = FreeCAD.ActiveDocument.Objects 72 | for obj in objs: 73 | print(obj.Name) 74 | if "Joint" in obj.Name: 75 | print("Joint: " + obj.Name + " with label " + obj.Label + " detected!") 76 | pos = obj.Shape.Placement 77 | pos.Base *= 0.001 78 | sdfFile.write( 79 | ' \n' 83 | ) 84 | sdfFile.write( 85 | " " 86 | + str(pos.Base[0]) 87 | + " " 88 | + str(pos.Base[1]) 89 | + " " 90 | + str(pos.Base[2]) 91 | + " 0 0 0 \n" 92 | ) 93 | sdfFile.write( 94 | " " + bodyLabelFromObjStr(obj.Child) + "\n" 95 | ) 96 | sdfFile.write( 97 | " " + bodyLabelFromObjStr(obj.Parent) + "\n" 98 | ) 99 | sdfFile.write(" ") 100 | sdfFile.write(" 0 0 1") 101 | sdfFile.write(" \n") 102 | sdfFile.write(" \n") 103 | 104 | if obj.TypeId == "PartDesign::Body" or obj.TypeId == "Part::Box": 105 | print("Link: " + obj.Name + " with label " + obj.Label + " detected!") 106 | name = obj.Label 107 | mass = obj.Shape.Mass 108 | inertia = obj.Shape.MatrixOfInertia 109 | pos = obj.Shape.Placement 110 | com = obj.Shape.CenterOfMass 111 | com *= 0.001 112 | mass *= 0.001 113 | A11 = inertia.A11 * 0.000000001 114 | A12 = inertia.A12 * 0.000000001 115 | A13 = inertia.A13 * 0.000000001 116 | A22 = inertia.A22 * 0.000000001 117 | A23 = inertia.A23 * 0.000000001 118 | A33 = inertia.A33 * 0.000000001 119 | 120 | pos.Base *= 0.001 121 | 122 | # export shape as mesh (stl) 123 | obj.Shape.exportStl(meshPath + name + ".stl") 124 | # import stl and translate/scale 125 | mesh = Mesh.read(meshPath + name + ".stl") 126 | 127 | # scaling, millimeter -> meter 128 | mat = FreeCAD.Matrix() 129 | mat.scale(0.001, 0.001, 0.001) 130 | 131 | # apply scaling 132 | mesh.transform(mat) 133 | 134 | # move origo to center of mass 135 | pos.move(com * -1) 136 | mesh.Placement.Base *= 0.001 137 | 138 | # save scaled and transformed mesh as stl 139 | mesh.write(meshPath + name + ".stl") 140 | 141 | sdfFile.write('\n') 142 | sdfFile.write( 143 | " " 144 | + str(0) 145 | + " " 146 | + str(0) 147 | + " " 148 | + str(0) 149 | + " " 150 | + str(deg2rad(pos.Rotation.Q[0])) 151 | + " " 152 | + str(deg2rad(pos.Rotation.Q[1])) 153 | + " " 154 | + str(deg2rad(pos.Rotation.Q[2])) 155 | + "\n" 156 | ) 157 | sdfFile.write("\n") 158 | sdfFile.write( 159 | " " 160 | + str(0 + com.x) 161 | + " " 162 | + str(0 + com.y) 163 | + " " 164 | + str(0 + com.z) 165 | + " " 166 | + str(deg2rad(pos.Rotation.Q[0])) 167 | + " " 168 | + str(deg2rad(pos.Rotation.Q[1])) 169 | + " " 170 | + str(deg2rad(pos.Rotation.Q[2])) 171 | + "\n" 172 | ) 173 | sdfFile.write("\n") 174 | sdfFile.write("" + float_to_str(A11) + "\n") 175 | sdfFile.write("" + float_to_str(A12) + "\n") 176 | sdfFile.write("" + float_to_str(A13) + "\n") 177 | sdfFile.write("" + float_to_str(A22) + "\n") 178 | sdfFile.write("" + float_to_str(A23) + "\n") 179 | sdfFile.write("" + float_to_str(A33) + "\n") 180 | sdfFile.write("\n") 181 | sdfFile.write("" + str(mass) + "\n") 182 | sdfFile.write("\n") 183 | sdfFile.write('\n') 184 | sdfFile.write("\n") 185 | sdfFile.write("\n") 186 | sdfFile.write( 187 | "model://" + robotName + "/meshes/" + name + ".stl\n" 188 | ) 189 | sdfFile.write("\n") 190 | sdfFile.write("\n") 191 | sdfFile.write("\n") 192 | sdfFile.write('\n') 193 | sdfFile.write("\n") 194 | sdfFile.write("\n") 195 | sdfFile.write( 196 | "model://" + robotName + "/meshes/" + name + ".stl\n" 197 | ) 198 | sdfFile.write("\n") 199 | sdfFile.write("\n") 200 | sdfFile.write("\n") 201 | sdfFile.write("\n") 202 | sdfFile.write("\n") 203 | sdfFile.write("\n") 204 | 205 | sdfFile.close() 206 | return 207 | 208 | def IsActive(self): 209 | """Here you can define if the command must be active or not (greyed) if certain conditions 210 | are met or not. This function is optional.""" 211 | return True 212 | 213 | 214 | FreeCADGui.addCommand("RC_GazeboSDFExport", GazeboSDFExport()) 215 | -------------------------------------------------------------------------------- /GazeboSDFExportStatic.py: -------------------------------------------------------------------------------- 1 | from FreeCAD import Gui 2 | from FreeCAD import Base 3 | import FreeCAD, FreeCADGui, Part, os, math 4 | import Mesh, BuildRegularGeoms 5 | import os, sys, Mesh 6 | 7 | 8 | def float_to_str(f): 9 | return "{:.20f}".format(f) 10 | 11 | 12 | def deg2rad(a): 13 | return a * 0.01745329252 14 | 15 | 16 | def str2obj(a): 17 | return FreeCAD.ActiveDocument.getObject(a) 18 | 19 | 20 | def bodyFromPad(a): 21 | objs = FreeCAD.ActiveDocument.Objects 22 | for obj in objs: 23 | if obj.TypeId == "PartDesign::Body": 24 | if obj.hasObject(a): 25 | return obj 26 | 27 | 28 | def bodyLabelFromObjStr(a): 29 | b = str2obj(a) 30 | c = bodyFromPad(b) 31 | return c.Label 32 | 33 | 34 | class GazeboSDFExportStatic: 35 | """GazeboSDFExport""" 36 | 37 | def GetResources(self): 38 | return { 39 | "Pixmap": str( 40 | FreeCAD.getUserAppDataDir() 41 | + "Mod" 42 | + "/RobotCreator/icons/SDFexportStatic.png" 43 | ), # the name of a svg file available in the resources 44 | "Accel": "Shift+a", # a default shortcut (optional) 45 | "MenuText": "Gazebo static SDF export", 46 | "ToolTip": "Exports static SDF to Gazebo", 47 | } 48 | 49 | def Activated(self): 50 | print("Scaling mesh") 51 | 52 | # you might want to change this to where you want your exported mesh/sdf to be located. 53 | robotName = "testing" 54 | # projPath = "/home/maiden/Projects/RobotCreator/"+robotName + "/" 55 | projPath = os.path.expanduser("~") + "/.gazebo/models/" + robotName + "Static/" 56 | meshPath = projPath + "meshes/" 57 | 58 | if not os.path.exists(projPath): 59 | os.makedirs(projPath) 60 | 61 | if not os.path.exists(meshPath): 62 | os.makedirs(meshPath) 63 | 64 | # os.chdir(projectName) 65 | 66 | sdfFile = open(projPath + robotName + "Static.sdf", "w") 67 | sdfFile.write('\n') 68 | sdfFile.write('\n') 69 | sdfFile.write('\n') 70 | sdfFile.write("true\n") 71 | 72 | objs = FreeCAD.ActiveDocument.Objects 73 | for obj in objs: 74 | print(obj.Name) 75 | if "Joint" in obj.Name: 76 | print("Joint: " + obj.Name + " with label " + obj.Label + " detected!") 77 | pos = obj.Shape.Placement 78 | pos.Base *= 0.001 79 | sdfFile.write( 80 | ' \n' 84 | ) 85 | sdfFile.write( 86 | " " 87 | + str(pos.Base[0]) 88 | + " " 89 | + str(pos.Base[1]) 90 | + " " 91 | + str(pos.Base[2]) 92 | + " 0 0 0 \n" 93 | ) 94 | sdfFile.write( 95 | " " + bodyLabelFromObjStr(obj.Child) + "\n" 96 | ) 97 | sdfFile.write( 98 | " " + bodyLabelFromObjStr(obj.Parent) + "\n" 99 | ) 100 | sdfFile.write(" ") 101 | sdfFile.write(" 0 0 1") 102 | sdfFile.write(" \n") 103 | sdfFile.write(" \n") 104 | 105 | if obj.TypeId == "PartDesign::Body" or obj.TypeId == "Part::Box": 106 | print("Link: " + obj.Name + " with label " + obj.Label + " detected!") 107 | name = obj.Label 108 | mass = obj.Shape.Mass 109 | inertia = obj.Shape.MatrixOfInertia 110 | pos = obj.Shape.Placement 111 | com = obj.Shape.CenterOfMass 112 | com *= 0.001 113 | mass *= 0.001 114 | A11 = inertia.A11 * 0.000000001 115 | A12 = inertia.A12 * 0.000000001 116 | A13 = inertia.A13 * 0.000000001 117 | A22 = inertia.A22 * 0.000000001 118 | A23 = inertia.A23 * 0.000000001 119 | A33 = inertia.A33 * 0.000000001 120 | 121 | pos.Base *= 0.001 122 | 123 | # export shape as mesh (stl) 124 | obj.Shape.exportStl(meshPath + name + ".stl") 125 | # import stl and translate/scale 126 | mesh = Mesh.read(meshPath + name + ".stl") 127 | 128 | # scaling, millimeter -> meter 129 | mat = FreeCAD.Matrix() 130 | mat.scale(0.001, 0.001, 0.001) 131 | 132 | # apply scaling 133 | mesh.transform(mat) 134 | 135 | # move origo to center of mass 136 | pos.move(com * -1) 137 | mesh.Placement.Base *= 0.001 138 | 139 | # save scaled and transformed mesh as stl 140 | mesh.write(meshPath + name + ".stl") 141 | 142 | sdfFile.write('\n') 143 | sdfFile.write( 144 | " " 145 | + str(0) 146 | + " " 147 | + str(0) 148 | + " " 149 | + str(0) 150 | + " " 151 | + str(deg2rad(pos.Rotation.Q[0])) 152 | + " " 153 | + str(deg2rad(pos.Rotation.Q[1])) 154 | + " " 155 | + str(deg2rad(pos.Rotation.Q[2])) 156 | + "\n" 157 | ) 158 | sdfFile.write("\n") 159 | sdfFile.write( 160 | " " 161 | + str(0 + com.x) 162 | + " " 163 | + str(0 + com.y) 164 | + " " 165 | + str(0 + com.z) 166 | + " " 167 | + str(deg2rad(pos.Rotation.Q[0])) 168 | + " " 169 | + str(deg2rad(pos.Rotation.Q[1])) 170 | + " " 171 | + str(deg2rad(pos.Rotation.Q[2])) 172 | + "\n" 173 | ) 174 | sdfFile.write("\n") 175 | sdfFile.write("" + float_to_str(A11) + "\n") 176 | sdfFile.write("" + float_to_str(A12) + "\n") 177 | sdfFile.write("" + float_to_str(A13) + "\n") 178 | sdfFile.write("" + float_to_str(A22) + "\n") 179 | sdfFile.write("" + float_to_str(A23) + "\n") 180 | sdfFile.write("" + float_to_str(A33) + "\n") 181 | sdfFile.write("\n") 182 | sdfFile.write("" + str(mass) + "\n") 183 | sdfFile.write("\n") 184 | sdfFile.write('\n') 185 | sdfFile.write("\n") 186 | sdfFile.write("\n") 187 | sdfFile.write( 188 | "model://" 189 | + robotName 190 | + "Static/meshes/" 191 | + name 192 | + ".stl\n" 193 | ) 194 | sdfFile.write("\n") 195 | sdfFile.write("\n") 196 | sdfFile.write("\n") 197 | sdfFile.write('\n') 198 | sdfFile.write("\n") 199 | sdfFile.write("\n") 200 | sdfFile.write( 201 | "model://" 202 | + robotName 203 | + "Static/meshes/" 204 | + name 205 | + ".stl\n" 206 | ) 207 | sdfFile.write("\n") 208 | sdfFile.write("\n") 209 | sdfFile.write("\n") 210 | sdfFile.write("\n") 211 | sdfFile.write("\n") 212 | sdfFile.write("\n") 213 | 214 | sdfFile.close() 215 | return 216 | 217 | def IsActive(self): 218 | """Here you can define if the command must be active or not (greyed) if certain conditions 219 | are met or not. This function is optional.""" 220 | return True 221 | 222 | 223 | FreeCADGui.addCommand("RC_GazeboSDFExportStatic", GazeboSDFExportStatic()) 224 | -------------------------------------------------------------------------------- /Init.py: -------------------------------------------------------------------------------- 1 | # FreeCAD init script of RobotCreator module 2 | 3 | # *************************************************************************** 4 | # * (c) Anton Fosselius anton.fosselius 'guess what' gmail.com 2017 * 5 | # * * 6 | # * This file is part of the FreeCAD CAx development system. * 7 | # * * 8 | # * This program is free software; you can redistribute it and/or modify * 9 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 10 | # * as published by the Free Software Foundation; either version 2 of * 11 | # * the License, or (at your option) any later version. * 12 | # * for detail see the LICENCE text file. * 13 | # * * 14 | # * FreeCAD is distributed in the hope that it will be useful, * 15 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 16 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 17 | # * GNU Lesser General Public License for more details. * 18 | # * * 19 | # * You should have received a copy of the GNU Library General Public * 20 | # * License along with FreeCAD; if not, write to the Free Software * 21 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 22 | # * USA * 23 | # * * 24 | # ***************************************************************************/ 25 | 26 | FreeCAD.addImportType("SDF (*.sdf)", "importSDF") 27 | FreeCAD.addExportType("SDF (*.sdf)", "importSDF") 28 | FreeCAD.addImportType("URDF (*.urdf)", "importURDF") 29 | FreeCAD.addExportType("URDF (*.urdf)", "importURDF") 30 | 31 | print("Just Initial Stuff!") 32 | -------------------------------------------------------------------------------- /InitGui.py: -------------------------------------------------------------------------------- 1 | from FreeCAD import Gui 2 | import FreeCADGui 3 | 4 | 5 | class RobotCreator(Workbench): 6 | 7 | MenuText = "RobotCreator" 8 | ToolTip = "A Workbench for robot creation and simulation" 9 | Icon = """ 10 | /* XPM */ 11 | static char * C:\Program Files\FreeCAD 0_15\Mod\Fasteners\wbicon_xpm[] = { 12 | "16 16 5 1", 13 | " c None", 14 | ". c #000000", 15 | "+ c #03B83F", 16 | "@ c #0D8132", 17 | "# c #034D1C", 18 | " ", 19 | " ..... ..... ", 20 | " .+@@. .@@#. ", 21 | " .+@@. .@@#. ", 22 | " .+@@......@@#. ", 23 | " .+@@@@@@@@@@#. ", 24 | " .+@@@@@@@@@@#. ", 25 | " .+@@@@@@@@@@#. ", 26 | " .............. ", 27 | " .+@@#. ", 28 | " .+@@#. ", 29 | " .+@@#. ", 30 | " .+@@#. ", 31 | " .+@@#. ", 32 | " ...... ", 33 | " "}; 34 | """ 35 | 36 | def Initialize(self): 37 | "This function is executed when FreeCAD starts" 38 | import GazeboSDFExportStatic, GazeboSDFExport, CreateJoint, ExportURDF # import here all the needed files that create your FreeCAD commands 39 | 40 | self.list = [ 41 | # "RC_GazeboSDFExportStatic", 42 | # "RC_GazeboSDFExport", 43 | "RC_CreateJoint", 44 | "RC_ExportURDF", 45 | ] # A list of command names created in the line above 46 | self.appendToolbar( 47 | "RobotCreator", self.list 48 | ) # creates a new toolbar with your commands 49 | self.appendMenu("My New Menu", self.list) # creates a new menu 50 | self.appendMenu( 51 | ["An existing Menu", "My submenu"], self.list 52 | ) # appends a submenu to an existing menu 53 | FreeCADGui.addIconPath("~/.FreeCAD/Mod/RobotCreator/icons") 54 | 55 | def Activated(self): 56 | "This function is executed when the workbench is activated" 57 | return 58 | 59 | def Deactivated(self): 60 | "This function is executed when the workbench is deactivated" 61 | return 62 | 63 | def ContextMenu(self, recipient): 64 | "This is executed whenever the user right-clicks on screen" 65 | # "recipient" will be either "view" or "tree" 66 | self.appendContextMenu( 67 | "My commands", self.list 68 | ) # add commands to the context menu 69 | 70 | def GetClassName(self): 71 | # this function is mandatory if this is a full python workbench 72 | return "Gui::PythonWorkbench" 73 | 74 | Gui.addWorkbench(RobotCreator()) 75 | -------------------------------------------------------------------------------- /Macros/JointCreator.FCMacro: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | import FreeCADGui 3 | import FreeCAD 4 | import Part 5 | import sys 6 | from PySide import QtGui, QtCore 7 | 8 | class CreateHingeJointForm(QtGui.QDialog): 9 | """""" 10 | def __init__(self): 11 | super(CreateHingeJointForm, self).__init__() 12 | self.initUI() 13 | def initUI(self): 14 | option1Button = QtGui.QPushButton("OK") 15 | option1Button.clicked.connect(self.onOption1) 16 | option2Button = QtGui.QPushButton("Cancel") 17 | option2Button.clicked.connect(self.onOption2) 18 | 19 | labelPosition = QtGui.QLabel('Position') 20 | labelRotation = QtGui.QLabel('Rotation') 21 | 22 | onlyDouble = QtGui.QDoubleValidator() 23 | self.posX = QtGui.QLineEdit('0') 24 | self.posX.setValidator(onlyDouble) 25 | self.posY = QtGui.QLineEdit('0') 26 | self.posY.setValidator(onlyDouble) 27 | self.posZ = QtGui.QLineEdit('0') 28 | self.posZ.setValidator(onlyDouble) 29 | self.rotX = QtGui.QLineEdit('1') 30 | self.rotX.setValidator(onlyDouble) 31 | self.rotY = QtGui.QLineEdit('0') 32 | self.rotY.setValidator(onlyDouble) 33 | self.rotZ = QtGui.QLineEdit('0') 34 | self.rotZ.setValidator(onlyDouble) 35 | 36 | # 37 | # buttonBox = QtGui.QDialogButtonBox() 38 | buttonBox = QtGui.QDialogButtonBox(QtCore.Qt.Horizontal) 39 | buttonBox.addButton(option1Button, QtGui.QDialogButtonBox.ActionRole) 40 | buttonBox.addButton(option2Button, QtGui.QDialogButtonBox.ActionRole) 41 | # 42 | posLabelLayout = QtGui.QHBoxLayout() 43 | posLabelLayout.addWidget(labelPosition) 44 | 45 | posValueLayout = QtGui.QHBoxLayout() 46 | posValueLayout.addWidget(QtGui.QLabel('X')) 47 | posValueLayout.addWidget(self.posX) 48 | posValueLayout.addWidget(QtGui.QLabel('Y')) 49 | posValueLayout.addWidget(self.posY) 50 | posValueLayout.addWidget(QtGui.QLabel('Z')) 51 | posValueLayout.addWidget(self.posZ) 52 | 53 | rotLabelLayout = QtGui.QHBoxLayout() 54 | rotLabelLayout.addWidget(labelRotation) 55 | 56 | hbox = QtGui.QHBoxLayout() 57 | hbox.addWidget(QtGui.QLabel('X')) 58 | hbox.addWidget(self.rotX) 59 | hbox.addWidget(QtGui.QLabel('Y')) 60 | hbox.addWidget(self.rotY) 61 | hbox.addWidget(QtGui.QLabel('Z')) 62 | hbox.addWidget(self.rotZ) 63 | 64 | 65 | mainLayout = QtGui.QVBoxLayout() 66 | mainLayout.addLayout(posLabelLayout) 67 | mainLayout.addLayout(posValueLayout) 68 | mainLayout.addLayout(rotLabelLayout) 69 | mainLayout.addLayout(hbox) 70 | mainLayout.addWidget(buttonBox) 71 | self.setLayout(mainLayout) 72 | # define window xLoc,yLoc,xDim,yDim 73 | self.setGeometry( 250, 250, 0, 50) 74 | self.setWindowTitle("Create a hinge joint") 75 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) 76 | def onOption1(self): 77 | self.retStatus = 1 78 | self.close() 79 | def onOption2(self): 80 | self.retStatus = 2 81 | self.close() 82 | 83 | def routine1(): 84 | print 'create!' 85 | 86 | def routine2(): 87 | print 'abort!' 88 | 89 | class Joint: 90 | def __init__(self, obj,parent,child): 91 | '''"App two point properties"''' 92 | obj.addProperty("App::PropertyString","Parent","Joint").Parent = parent.Name 93 | obj.addProperty("App::PropertyString","Child","Joint").Child = child.Name 94 | obj.addProperty("App::PropertyAngle","Angle1","Joint","Angle1 of joint").Angle1 = 90 95 | obj.addProperty("App::PropertyVector","axis","rotation","End point").axis=FreeCAD.Vector(1,0,0) 96 | obj.Proxy = self 97 | 98 | def execute(self, fp): 99 | '''"Print a short message when doing a recomputation, this method is mandatory" ''' 100 | fp.Shape = Part.makeSphere(1) 101 | 102 | class ViewProviderJoint: 103 | def __init__(self, obj): 104 | ''' Set this object to the proxy object of the actual view provider ''' 105 | obj.ShapeColor=(1.0,0.0,0.0) 106 | obj.Proxy = self 107 | 108 | def getDefaultDisplayMode(self): 109 | ''' Return the name of the default display mode. It must be defined in getDisplayModes. ''' 110 | return "Flat Lines" 111 | 112 | 113 | selection = Gui.Selection.getSelection() 114 | if len(selection) == 2: 115 | form = CreateHingeJointForm() 116 | form.exec_() 117 | if form.retStatus==1: 118 | routine1() 119 | j=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Joint") 120 | Joint(j,selection[0],selection[1]) 121 | j.Placement.Base = FreeCAD.Vector(float(form.posX.text()),float(form.posY.text()),float(form.posZ.text())) 122 | j.axis = FreeCAD.Vector(float(form.rotX.text()),float(form.rotY.text()),float(form.rotZ.text())) 123 | ViewProviderJoint(j.ViewObject) 124 | App.ActiveDocument.recompute() 125 | elif form.retStatus==2: 126 | print 'abort' 127 | 128 | else: 129 | print "Only support selection of two elements on two different objects" 130 | -------------------------------------------------------------------------------- /Macros/sdfExport.FCMacro: -------------------------------------------------------------------------------- 1 | import os, sys, Mesh 2 | 3 | def float_to_str(f): 4 | return '{:.20f}'.format(f) 5 | 6 | def deg2rad(a): 7 | return a*0.01745329252 8 | 9 | def str2obj(a): 10 | return App.ActiveDocument.getObject(a) 11 | 12 | def bodyFromPad(a): 13 | objs = FreeCAD.ActiveDocument.Objects 14 | for obj in objs: 15 | if obj.TypeId == 'PartDesign::Body': 16 | if obj.hasObject(a): 17 | return obj 18 | 19 | def bodyLabelFromObjStr(a): 20 | b = str2obj(a) 21 | c = bodyFromPad(b) 22 | return c.Label 23 | 24 | robotName = "testing" 25 | projPath = os.path.expanduser('~') + "/.gazebo/models/" + robotName + "/" 26 | meshPath = projPath + "meshes/" 27 | 28 | if not os.path.exists(projPath): 29 | os.makedirs(projPath) 30 | 31 | if not os.path.exists(meshPath): 32 | os.makedirs(meshPath) 33 | 34 | modelCFG = open(projPath+"model.config",'w') 35 | modelCFG.write('\n') 36 | modelCFG.write('\n') 37 | modelCFG.write(' '+robotName+'\n') 38 | modelCFG.write(' 1.0\n') 39 | modelCFG.write(' '+robotName+'.sdf\n') 40 | modelCFG.write(' \n') 41 | modelCFG.write(' Optional: YOUR NAME\n') 42 | modelCFG.write(' Optional: YOUR EMAIL\n') 43 | modelCFG.write(' \n') 44 | modelCFG.write(' \n') 45 | modelCFG.write(' a bot example\n') 46 | modelCFG.write(' \n') 47 | modelCFG.write('\n') 48 | modelCFG.close() 49 | 50 | print "writing to file" + projPath+robotName+'.sdf' 51 | sdfFile = open(projPath+robotName+'.sdf', 'w') 52 | sdfFile.write('\n') 53 | sdfFile.write('\n') 54 | sdfFile.write('\n') 55 | 56 | objs = FreeCAD.ActiveDocument.Objects 57 | for obj in objs: 58 | print obj.Name 59 | if "Joint" in obj.Name: 60 | print "Joint: " + obj.Name + " with label " + obj.Label+ " detected!" 61 | pos = obj.Shape.Placement 62 | pos.Base *= 0.001 63 | sdfFile.write(' \n') 64 | sdfFile.write(' '+str(pos.Base[0]) + ' ' + str(pos.Base[1]) + ' ' + str(pos.Base[2])+ ' 0 0 0 \n') 65 | sdfFile.write(' '+bodyLabelFromObjStr(obj.Child)+'\n') 66 | sdfFile.write(' '+bodyLabelFromObjStr(obj.Parent)+'\n') 67 | sdfFile.write(' ') 68 | sdfFile.write(' 0 0 1') 69 | sdfFile.write(' \n') 70 | sdfFile.write(' \n') 71 | 72 | if obj.TypeId == 'PartDesign::Body' or obj.TypeId == 'Part::Box': 73 | print "Link: " + obj.Name + " with label " + obj.Label+ " detected!" 74 | name = obj.Label 75 | mass = obj.Shape.Mass 76 | inertia = obj.Shape.MatrixOfInertia 77 | pos = obj.Shape.Placement 78 | com = obj.Shape.CenterOfMass 79 | com *= 0.001 80 | mass *= 0.001 81 | A11 = inertia.A11 * 0.000000001 82 | A12 = inertia.A12 * 0.000000001 83 | A13 = inertia.A13 * 0.000000001 84 | A22 = inertia.A22 * 0.000000001 85 | A23 = inertia.A23 * 0.000000001 86 | A33 = inertia.A33 * 0.000000001 87 | 88 | pos.Base *= 0.001 89 | 90 | #export shape as mesh (stl) 91 | obj.Shape.exportStl(meshPath+name+".stl") 92 | #import stl and translate/scale 93 | mesh = Mesh.read(meshPath+name+".stl") 94 | 95 | # scaling, millimeter -> meter 96 | mat=FreeCAD.Matrix() 97 | mat.scale(0.001,0.001,0.001) 98 | 99 | #apply scaling 100 | mesh.transform(mat) 101 | 102 | #move origo to center of mass 103 | pos.move(com*-1) 104 | mesh.Placement.Base *= 0.001 105 | 106 | #save scaled and transformed mesh as stl 107 | mesh.write(meshPath+name+".stl") 108 | 109 | sdfFile.write('\n') 110 | sdfFile.write(' ' + str(0) + ' ' + str(0) + ' ' + str(0)+ ' ' + str(deg2rad(pos.Rotation.Q[0])) + ' ' + str(deg2rad(pos.Rotation.Q[1]))+ ' ' + str(deg2rad(pos.Rotation.Q[2]))+'\n') 111 | sdfFile.write('\n') 112 | sdfFile.write(' ' + str(0+com.x) + ' ' + str(0+com.y) + ' ' + str(0+com.z)+ ' ' + str(deg2rad(pos.Rotation.Q[0])) + ' ' + str(deg2rad(pos.Rotation.Q[1]))+ ' ' + str(deg2rad(pos.Rotation.Q[2]))+'\n') 113 | sdfFile.write('\n') 114 | sdfFile.write(''+float_to_str(A11)+'\n') 115 | sdfFile.write(''+float_to_str(A12)+'\n') 116 | sdfFile.write(''+float_to_str(A13)+'\n') 117 | sdfFile.write(''+float_to_str(A22)+'\n') 118 | sdfFile.write(''+float_to_str(A23)+'\n') 119 | sdfFile.write(''+float_to_str(A33)+'\n') 120 | sdfFile.write('\n') 121 | sdfFile.write(''+str(mass)+'\n') 122 | sdfFile.write('\n') 123 | sdfFile.write('\n') 124 | sdfFile.write('\n') 125 | sdfFile.write('\n') 126 | sdfFile.write('model://'+robotName+'/meshes/'+name+'.stl\n') 127 | sdfFile.write('\n') 128 | sdfFile.write('\n') 129 | sdfFile.write('\n') 130 | sdfFile.write('\n') 131 | sdfFile.write('\n') 132 | sdfFile.write('\n') 133 | sdfFile.write('model://'+robotName+'/meshes/'+name+'.stl\n') 134 | sdfFile.write('\n') 135 | sdfFile.write('\n') 136 | sdfFile.write('\n') 137 | sdfFile.write('\n') 138 | sdfFile.write('\n') 139 | sdfFile.write('\n') 140 | 141 | sdfFile.close() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreeCAD RobotCreator Workbench 2 | 3 | FreeCAD Workbench to create URDF files from e.g. STP or other CAD file formats. 4 | *This project works for simple robots right now, but it hasn't been fully tested.* 5 | 6 | ## Installation 7 | 8 | In order to install this FreeCAD plugin, install FreeCAD and clone this repository into the `~/.FreeCAD/Mod/` folder. Like so: 9 | 10 | ``` 11 | mkdir -p ~/.FreeCAD/Mod/ 12 | cd ~/.FreeCAD/Mod/ 13 | git clone https://github.com/RoboStack/RobotCreator 14 | ``` 15 | 16 | Then restart FreeCAD and the RobotCreator Workbench should appear. 17 | Note: for the URDF export, you also need to have `BeautifulSoup4` and `lxml` installed. 18 | 19 | ![Robot Creator](docs/images/screenshot.png) 20 | 21 | The robot, running in RViz. 22 | 23 | ![Robot Creator](docs/images/screenshot_rviz.png) 24 | 25 | ## Feedback 26 | 27 | Please open issues on this repository for questions, or suggestions. 28 | 29 | ## Development 30 | 31 | You can test e.g. the export function by opening the Python console in FreeCAD and executing: 32 | 33 | ``` 34 | # python 3: 35 | import importlib as imp; import ExportURDF; imp.reload(ExportURDF).export_urdf() 36 | # python 2: 37 | import ExportURDF; reload(ExportURDF).export_urdf() 38 | ``` -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/RobotCreator/7e837ca237596f3689a2ff815436e428c83e6a7f/docs/images/screenshot.png -------------------------------------------------------------------------------- /docs/images/screenshot_rviz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/RobotCreator/7e837ca237596f3689a2ff815436e428c83e6a7f/docs/images/screenshot_rviz.png -------------------------------------------------------------------------------- /icons/SDFexport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/RobotCreator/7e837ca237596f3689a2ff815436e428c83e6a7f/icons/SDFexport.png -------------------------------------------------------------------------------- /icons/SDFexportStatic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/RobotCreator/7e837ca237596f3689a2ff815436e428c83e6a7f/icons/SDFexportStatic.png -------------------------------------------------------------------------------- /icons/createJoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RoboStack/RobotCreator/7e837ca237596f3689a2ff815436e428c83e6a7f/icons/createJoint.png -------------------------------------------------------------------------------- /scripts/initProject.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | 4 | projectName = "testProj" 5 | robotName = "testRobot" 6 | 7 | links = ["a","b","c"] 8 | 9 | if not os.path.exists(projectName): 10 | os.makedirs(projectName) 11 | 12 | os.chdir(projectName) 13 | 14 | worldFile = open('testRobot'+'.world','w') 15 | worldFile.write("\n") 16 | worldFile.write(" \n") 17 | worldFile.write(" \n") 18 | worldFile.write("\n") 19 | worldFile.write(" \n") 20 | worldFile.write(" \n") 21 | worldFile.write(" model://ground_plane\n") 22 | worldFile.write(" \n") 23 | worldFile.write("\n") 24 | worldFile.write(" \n") 25 | worldFile.write(" \n") 26 | worldFile.write(" model://sun\n") 27 | worldFile.write(" \n") 28 | worldFile.write("\n") 29 | worldFile.write(" \n") 30 | worldFile.write(" model://"+robotName+"\n") 31 | worldFile.write(" \n") 32 | worldFile.write("\n") 33 | worldFile.write(" \n") 34 | worldFile.write(" \n") 35 | 36 | sdfFile = open(robotName+'.sdf', 'w') 37 | sdfFile.write('\n') 38 | sdfFile.write('\n') 39 | sdfFile.write('\n') 40 | 41 | objs = FreeCAD.ActiveDocument.Objects 42 | for obj in objs: 43 | if obj.TypeId == PartDesign::Body: 44 | name = obj.Name 45 | com = obj.Shape.CenterOfMass 46 | mass = obj.Shape.Mass 47 | inertia = obj.Shape.MatrixOfInertia 48 | pos = obj.Shape.Placement 49 | 50 | sdfFile.write('\n') 51 | sdfFile.write(' ' + str(pos.Base[0]) + ' ' + str(pos.Base[1]) + ' ' + str(pos.Base[2])+ ' ' + str(pos.Rotation.toEuler()[0]) + ' ' + str(pos.Rotation.toEuler()[1])+ ' ' + str(pos.Rotation.toEuler()[2])+'\n') 52 | sdfFile.write('\n') 53 | sdfFile.write(' ' + str(pos.Base[0]) + ' ' + str(pos.Base[1]) + ' ' + str(pos.Base[2])+ ' ' + str(pos.Rotation.toEuler()[0]) + ' ' + str(pos.Rotation.toEuler()[1])+ ' ' + str(pos.Rotation.toEuler()[2])+'\n') 54 | sdfFile.write('\n') 55 | sdfFile.write(''+str(inertia.A11/1000)+'\n') 56 | sdfFile.write(''+str(inertia.A12/1000)+'\n') 57 | sdfFile.write(''+str(inertia.A13/1000)+'\n') 58 | sdfFile.write(''+str(inertia.A22/1000)+'\n') 59 | sdfFile.write(''+str(inertia.A23/1000)+'\n') 60 | sdfFile.write(''+str(inertia.A33/1000)+'\n') 61 | sdfFile.write('\n') 62 | sdfFile.write(''+str(mass/1000)+'\n') 63 | sdfFile.write('\n') 64 | sdfFile.write('\n') 65 | sdfFile.write('\n') 66 | sdfFile.write('\n') 67 | sdfFile.write('model://'+robotName+'/meshes/'+name+'.stl\n') 68 | sdfFile.write('\n') 69 | sdfFile.write('\n') 70 | sdfFile.write('\n') 71 | sdfFile.write('\n') 72 | sdfFile.write('\n') 73 | sdfFile.write('\n') 74 | sdfFile.write('model://'+robotName+'/meshes/'+name+'.stl\n') 75 | sdfFile.write('\n') 76 | sdfFile.write('\n') 77 | sdfFile.write('\n') 78 | sdfFile.write('\n') 79 | sdfFile.write('\n') 80 | sdfFile.write('\n') 81 | 82 | sdfFile.close() 83 | 84 | 85 | # x y z 86 | # 87 | # x xx xy xz 88 | # 89 | # y xy yy yz 90 | # 91 | # z xz zy zz 92 | 93 | 94 | 95 | 96 | 97 | 98 | -0.15 0.0 0.5 0 0 0 99 | 100 | 0 0 -0.5 0 0 0 101 | 102 | 0.01 103 | 0 104 | 0 105 | 0.01 106 | 0 107 | 0.01 108 | 109 | 10.0 110 | 111 | 112 | 113 | 114 | 0.2 0.2 1.0 115 | 116 | 117 | 118 | 119 | 120 | 121 | 0.2 0.2 1.0 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 0.0 0.0 0.05 0 0 0 131 | 132 | 133 | 0.01 134 | 0 135 | 0 136 | 0.01 137 | 0 138 | 0.01 139 | 140 | 0.5 141 | 142 | 143 | 144 | 145 | 0.1 0.2 0.1 146 | 147 | 148 | 149 | 150 | 151 | 152 | 0.1 0.2 0.1 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 0.1 0.2 0.05 0 0 -0.78539 162 | 163 | 164 | 0.01 165 | 0 166 | 0 167 | 0.01 168 | 0 169 | 0.01 170 | 171 | 0.1 172 | 173 | 174 | 175 | 176 | 0.1 0.3 0.1 177 | 178 | 179 | 180 | 181 | 182 | 183 | 0.1 0.3 0.1 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 0.336 0.3 0.05 0 0 1.5707 193 | 194 | 195 | 0.01 196 | 0 197 | 0 198 | 0.01 199 | 0 200 | 0.01 201 | 202 | 0.1 203 | 204 | 205 | 206 | 207 | 0.1 0.2 0.1 208 | 209 | 210 | 211 | 212 | 213 | 214 | 0.1 0.2 0.1 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 0.1 -0.2 0.05 0 0 .78539 224 | 225 | 226 | 0.01 227 | 0 228 | 0 229 | 0.01 230 | 0 231 | 0.01 232 | 233 | 0.1 234 | 235 | 236 | 237 | 238 | 0.1 0.3 0.1 239 | 240 | 241 | 242 | 243 | 244 | 245 | 0.1 0.3 0.1 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 0.336 -0.3 0.05 0 0 1.5707 255 | 256 | 257 | 0.01 258 | 0 259 | 0 260 | 0.01 261 | 0 262 | 0.01 263 | 264 | 0.1 265 | 266 | 267 | 268 | 269 | 0.1 0.2 0.1 270 | 271 | 272 | 273 | 274 | 275 | 276 | 0.1 0.2 0.1 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | true 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 0 0 -0.005 0 0 0 297 | 298 | 1.2 299 | 300 | 0.0010608477766732891 301 | -0.00023721360899498273 302 | 0.00024999999996488216 303 | 0 304 | 0 305 | 0 306 | 307 | 308 | 309 | 310 | 311 | 312 | 315 | model://baseBot/meshes/plate.stl 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 327 | model://baseBot/meshes/plate.stl 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 338 | 0.00 0.00 0.005 0 0 0 339 | 340 | 0.1 341 | 342 | 0.0010608477766732891 343 | -0.00023721360899498273 344 | 0.00024999999996488216 345 | 0 346 | 0 347 | 0 348 | 349 | 350 | 351 | 352 | 353 | model://baseBot/meshes/holder.stl 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | model://baseBot/meshes/holder.stl 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 0.00 0.00 -0.00048 0 0 0 373 | 374 | 375 | base 376 | 377 | 378 | top 379 | 380 | 381 | 382 | 383 | 384 | 0 0 1 385 | 386 | 387 | 388 | 389 | 390 | -10000000000000000 391 | 10000000000000000 392 | 393 | 394 | 395 | 396 | 397 | --------------------------------------------------------------------------------