├── .gitignore ├── README.md ├── __init__.py └── export_openscad.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | io_mesh_openscad 2 | ================ 3 | 4 | Blender output module for [OpenSCAD](http://www.openscad.org/) 5 | 6 | A module for Blender 2.8x that outputs meshes in [OpenSCAD](http://www.openscad.org/) compatible format. 7 | Clone into your blender addon directory ([Blender's Directory Layout](https://docs.blender.org/manual/en/latest/advanced/blender_directory_layout.html)), and enable in your user preferences. 8 | 9 | Version History 10 | ================ 11 | v0.4.1 12 | - make addon compatible with blender version >= 2.8x 13 | 14 | v0.4 15 | - support for multi spline paths ( [atartanian](https://github.com/atartanian) ) 16 | 17 | v0.3 18 | - exports curves as polygons instead of polyhedron 19 | - fixed multimatrix function after I borked it ( [clothbot](https://github.com/clothbot) ) 20 | 21 | v0.2 22 | - make output files modular for `use` in other openscad files ( [clothbot](https://github.com/clothbot) ) 23 | - output shapekeys ( [atartanian](https://github.com/atartanian) ) 24 | - handle n-gons 25 | - fix bug exporting multiple models 26 | 27 | v0.1 28 | - initial version 29 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | # 20 | 21 | bl_info = { 22 | "name": "OpenSCAD Exporter (.scad)", 23 | "author": "Peter Yee (GraphicsForge)", 24 | "version": (0, 4, 1), 25 | "blender": (2, 80, 0), 26 | "location": "File > Import-Export > openscad", 27 | "description": "Output a mesh into an openscad file", 28 | "warning": "", 29 | "wiki_url": "https://github.com/graphicsforge/io_mesh_openscad/wiki", 30 | "tracker_url": "https://github.com/graphicsforge/io_mesh_openscad", 31 | "support": 'COMMUNITY', 32 | "category": "Import-Export"} 33 | 34 | """ 35 | Export OpenSCAD files 36 | """ 37 | 38 | if "bpy" in locals(): 39 | import imp 40 | if "export_openscad" in locals(): 41 | imp.reload(export_openscad) 42 | 43 | import os 44 | import bpy 45 | from bpy.props import StringProperty, BoolProperty, CollectionProperty 46 | from bpy_extras.io_utils import ExportHelper, ImportHelper 47 | from bpy.types import Operator, OperatorFileListElement 48 | 49 | 50 | class ExportOpenSCAD(Operator, ExportHelper): 51 | """Save vertex array data from the active object""" 52 | bl_idname = "export_mesh.openscad" 53 | bl_label = "Export OpenSCAD" 54 | 55 | filename_ext = ".scad" 56 | filter_glob = StringProperty(default="*.scad", options={'HIDDEN'}) 57 | 58 | apply_modifiers = BoolProperty(name="Apply Modifiers", 59 | description="Apply the modifiers first", 60 | default=True) 61 | 62 | def execute(self, context): 63 | from . import export_openscad 64 | from mathutils import Matrix 65 | 66 | keywords = self.as_keywords(ignore=("check_existing", 67 | "filter_glob", 68 | )) 69 | 70 | return export_openscad.save(self, context, **keywords) 71 | 72 | 73 | def menu_export(self, context): 74 | self.layout.operator(ExportOpenSCAD.bl_idname, text="openscad (.scad)") 75 | 76 | 77 | def register(): 78 | bpy.utils.register_class(ExportOpenSCAD) 79 | bpy.types.TOPBAR_MT_file_export.append(menu_export) 80 | 81 | 82 | def unregister(): 83 | bpy.utils.unregister_class(ExportOpenSCAD) 84 | bpy.types.TOPBAR_MT_file_export.remove(menu_export) 85 | 86 | 87 | if __name__ == "__main__": 88 | register() 89 | -------------------------------------------------------------------------------- /export_openscad.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | # 20 | 21 | import os 22 | import re 23 | import time 24 | 25 | import bpy 26 | import mathutils 27 | import bpy_extras.io_utils 28 | 29 | def getName( object ): 30 | return re.sub( '[^a-zA-Z0-9_]+', '', re.sub( '[\. ]','_', object.data.name )) 31 | 32 | # dump some stuff we might want later in openscad 33 | def write_utils( fw, object ): 34 | objectName = getName(object) 35 | fw("function %s_dimX() = %.2f;\n" % (objectName, object.dimensions[0])) 36 | fw("function %s_dimY() = %.2f;\n" % (objectName, object.dimensions[1])) 37 | fw("function %s_dimZ() = %.2f;\n" % (objectName, object.dimensions[2])) 38 | fw("function %s_multmatrix() = [" % (objectName)) 39 | for r_i in range(4): 40 | if r_i != 0: 41 | fw(',') 42 | fw('[') 43 | for c_j in range(4): 44 | if c_j != 0: 45 | fw(',') 46 | fw('%f' % object.matrix_world[r_i][c_j]) 47 | fw(']') 48 | fw('];\n') 49 | 50 | # output our comments about shapekey objects 51 | def write_shapekey_commments( fw, object ): 52 | objectName = getName(object) 53 | mesh = object.data 54 | if (len(mesh.polygons) + len(mesh.vertices)): 55 | # grab our shapekeys, excluding our reference 56 | nonRefShapeKeys = {} 57 | for index, keyblock in enumerate(mesh.shape_keys.key_blocks): 58 | if keyblock.name == mesh.shape_keys.reference_key.name: 59 | continue 60 | else: 61 | nonRefShapeKeys[len(nonRefShapeKeys)] = keyblock 62 | 63 | # drop our info 64 | fw("\n/////////////////////\n") 65 | fw("// MODULE %s\n" % objectName) 66 | fw("/////////////////////\n") 67 | fw("// Control Variables\n") 68 | for i, key in enumerate(nonRefShapeKeys): 69 | fw("%s_factor = %d; // [%d:%d]\n" % (nonRefShapeKeys[i].name, nonRefShapeKeys[i].value*100, nonRefShapeKeys[i].slider_min*100, nonRefShapeKeys[i].slider_max*100)) 70 | fw("\n// Examples/Tests\n") 71 | fw("%s(" % objectName) 72 | for i, key in enumerate(nonRefShapeKeys): 73 | if i!=0: 74 | fw(", ") 75 | fw("%s_factor" % nonRefShapeKeys[i].name) 76 | fw(");\n") 77 | fw("\n// Functions and Utilities\n") 78 | write_utils( fw, object ) 79 | 80 | # output our comments about objects 81 | def write_object_commments( fw, object ): 82 | objectName = getName(object) 83 | mesh = object.data 84 | if (len(mesh.polygons) + len(mesh.vertices)): 85 | # drop our info 86 | fw("\n/////////////////////\n") 87 | fw("// MODULE %s\n" % objectName) 88 | fw("/////////////////////\n") 89 | fw("\n// Examples/Tests\n") 90 | fw("%s();\n" % objectName) 91 | fw("\n// Functions and Utilities\n") 92 | write_utils( fw, object ) 93 | 94 | # output an object that has shapekeys 95 | def write_shapekeys( fw, object, EXPORT_CUSTOMIZER_MARKUP=False ): 96 | objectName = getName(object) 97 | 98 | # if an object has geometry, export them 99 | mesh = object.data 100 | if (len(mesh.polygons) + len(mesh.vertices)): 101 | # grab our shapekeys, excluding our reference 102 | nonRefShapeKeys = {} 103 | for index, keyblock in enumerate(mesh.shape_keys.key_blocks): 104 | if keyblock.name == mesh.shape_keys.reference_key.name: 105 | continue 106 | else: 107 | nonRefShapeKeys[len(nonRefShapeKeys)] = keyblock 108 | 109 | # drop our geometry 110 | fw("\n/////////////////////\n// geometry for %s\n" % objectName) 111 | fw("function %s_faces() = [\n" % objectName) 112 | for index, face in enumerate(mesh.polygons): 113 | face_verts = face.vertices 114 | if index != 0: 115 | fw(',') 116 | # fan triangulate 117 | for i in range(2, len(face_verts), 1): 118 | if i>2: 119 | fw(",") 120 | fw("[%d,%d,%d]" % (face_verts[i],face_verts[i-1],face_verts[0])) 121 | fw("];\n") 122 | 123 | fw("\nmodule %s(" % objectName) 124 | for i, key in enumerate(nonRefShapeKeys): 125 | if i!=0: 126 | fw(", ") 127 | fw("%s_factor = %f" % (nonRefShapeKeys[i].name, nonRefShapeKeys[i].value*100)) 128 | fw(") {\n") 129 | 130 | fw(" %s_shapes_points = [\n" % objectName) 131 | for vertex in mesh.vertices: 132 | if vertex.index != 0: 133 | fw(",") 134 | fw("[") 135 | for com_index, component in enumerate(vertex.co): 136 | if com_index != 0: 137 | fw(",") 138 | for key_index, keyblock in enumerate(mesh.shape_keys.key_blocks): 139 | # combine the effect of each shapekey 140 | keyvalue = keyblock.data[vertex.index].co[com_index] 141 | basevalue = keyblock.relative_key.data[vertex.index].co[com_index] 142 | if keyblock.name == mesh.shape_keys.reference_key.name: 143 | fw("%f" % (keyblock.data[vertex.index].co[com_index])) 144 | elif keyvalue!=basevalue: 145 | # linearly interpolate between key and base 146 | fw("+((%f-%f)" % (keyvalue, basevalue)) 147 | fw("*%s_factor/100)" % keyblock.name) 148 | fw("]") 149 | fw("];") 150 | fw(" multmatrix(%s_multmatrix()) polyhedron(faces = %s_faces(), points = %s_shapes_points, convexity=10);\n" % (objectName, objectName, objectName)) 151 | fw("};\n") 152 | 153 | else: 154 | print("ERROR: tried to export a mesh without sufficient verts!") 155 | 156 | # output an object 157 | def write_curve( fw, object): 158 | objectName = getName(object) 159 | curve = object.data 160 | 161 | fw("\n/////////////////////\n") 162 | fw("// MODULE %s\n" % objectName) 163 | fw("/////////////////////\n") 164 | fw("\n// Examples/Tests\n") 165 | fw("%s();\n" % objectName) 166 | 167 | # if an object has geometry, export them 168 | if (len(curve.splines)): 169 | pointList = [] 170 | pathsList = [] 171 | 172 | splineIndex = 0 173 | curIndex = 0 174 | 175 | for spline in curve.splines: 176 | splineStartIndex = curIndex 177 | for point in spline.bezier_points: 178 | pointList.append([point.co[0], point.co[1]]) 179 | curIndex += 1 180 | pathList = [] 181 | for i in range(splineStartIndex, curIndex): 182 | pathList.append(i) 183 | pathsList.append(pathList) 184 | 185 | fw("\n/////////////////////\n// geometry for %s\n" % objectName) 186 | fw("function %s_paths() = [\n" % objectName) 187 | 188 | # generate paths 189 | for pathindex, pathlist in enumerate(pathsList): 190 | if pathindex != 0: 191 | fw(',') 192 | fw("[") 193 | for index, pointindex in enumerate(pathlist): 194 | if index != 0: 195 | fw(',') 196 | fw("%d" % (pointindex)) 197 | fw("]") 198 | fw("];\n") 199 | 200 | # generate vert positions 201 | fw("function %s_points() = [\n" % objectName) 202 | for index, point in enumerate(pointList): 203 | if index != 0: 204 | fw(",") 205 | fw("[%f,%f]" % (point[0], point[1])) 206 | fw("];\n") 207 | # define our module 208 | fw("module %s() {\n" % objectName) 209 | fw(" polygon(points = %s_points(), paths = %s_paths());\n" % (objectName, objectName)) 210 | fw("};\n") 211 | else: 212 | print("ERROR: tried to export a curve without sufficient points!") 213 | 214 | # output an object 215 | def write_mesh( fw, object, mesh ): 216 | objectName = getName(object) 217 | 218 | # if an object has geometry, export them 219 | if (len(mesh.polygons) + len(mesh.vertices)): 220 | # drop our geometry 221 | fw("\n/////////////////////\n// geometry for %s\n" % objectName) 222 | fw("function %s_faces() = [\n" % objectName) 223 | for index, face in enumerate(mesh.polygons): 224 | face_verts = face.vertices 225 | if index != 0: 226 | fw(',') 227 | # fan triangulate 228 | for i in range(2, len(face_verts), 1): 229 | if i>2: 230 | fw(",") 231 | fw("[%d,%d,%d]" % (face_verts[i],face_verts[i-1],face_verts[0])) 232 | fw("];\n") 233 | # drop our vert positions 234 | fw("function %s_points() = [\n" % objectName) 235 | for vertex in mesh.vertices: 236 | if vertex.index != 0: 237 | fw(",") 238 | fw("[%f,%f,%f]" % (vertex.co.x,vertex.co.y,vertex.co.z)) 239 | fw("];") 240 | # define our module 241 | fw("module %s() {\n" % objectName) 242 | fw(" multmatrix(%s_multmatrix()) polyhedron(faces = %s_faces(), points = %s_points(), convexity=10);\n" % (objectName, objectName, objectName)) 243 | fw("};\n") 244 | else: 245 | print("ERROR: tried to export a mesh without sufficient verts!") 246 | 247 | 248 | def _write(context, filepath, EXPORT_APPLY_MODIFIERS): 249 | 250 | time1 = time.time() # profile how long we take 251 | print("saving to file: %s" % filepath) 252 | file = open(filepath, "w", encoding="utf8", newline="\n") 253 | filename_prefix=re.sub( 254 | '[\. ]','_' 255 | ,os.path.splitext( os.path.basename( file.name ) )[0] 256 | ) 257 | fw = file.write 258 | fw("""// 259 | // Usage: 260 | // 261 | // To reference this file in another OpenSCAD file 262 | // use <"""+filename_prefix+""".scad>; 263 | """) 264 | 265 | scene = context.scene 266 | # Exit edit mode before exporting, so current object states are exported properly. 267 | if bpy.ops.object.mode_set.poll(): 268 | bpy.ops.object.mode_set(mode='OBJECT') 269 | for object in context.selected_objects: 270 | if object.type == 'MESH' and object.data.shape_keys: 271 | write_shapekey_commments(fw, object) 272 | else: 273 | if object.type=='MESH': 274 | write_object_commments(fw, object) 275 | 276 | for object in context.selected_objects: 277 | 278 | # EXPORT THE SHAPES. 279 | # See if we're a 2D curve 280 | if object.type=='CURVE': 281 | write_curve(fw, object) 282 | else: 283 | if object.type == 'MESH' and object.data.shape_keys: 284 | write_shapekeys( fw, object ) 285 | elif object.type=='MESH': 286 | write_mesh(fw, object, object.to_mesh() ) 287 | 288 | # we're done here 289 | file.close() 290 | print("OpenSCAD Export time: %.2f" % (time.time() - time1)) 291 | 292 | def save(operator, context, filepath="", 293 | apply_modifiers=True, 294 | ): 295 | 296 | _write(context, filepath, 297 | EXPORT_APPLY_MODIFIERS=apply_modifiers, 298 | ) 299 | 300 | return {'FINISHED'} 301 | --------------------------------------------------------------------------------