├── .gitignore ├── README.md ├── __init__.py └── exporter.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | 3 | .DS_Store 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | blender.script.io_export_svg 2 | =============== 3 | 4 | exportscript for blender 2.80 to get BezierCurves to SVG. 5 | 6 | for more info please go [there](http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Import-Export/Inkscape_SVG_Exporter) 7 | 8 | Getting 9 | --- 10 | 11 | Best way is to: 12 | 13 | git clone https://github.com/maybites/blender.script.io_export_svg 14 | 15 | and symlinking it to your addons-folder. 16 | 17 | That way, you can git pull later on and it will automatically refresh to the latest (theoretically-)good version. 18 | 19 | If you are looking for the script for blender 2.79 or later please clone branch master_2.79 20 | 21 | 22 | Lasercutting 23 | --- 24 | 25 | If you want to export the SVG for precise lasercutting and since the laser is not a precis point (in the folling example 0.2mm in diameter, but its depending on the machine), you have to ajust the vector with an outset (or inset). After exporting the SVG you can do that in almost any vector-drawing program, here the instructions for Inkscape: 26 | 27 | 1. Open the exported SVG-file 28 | 2. Press Shift+Ctrl+D 29 | 3. Change Standard units to mm 30 | 4. Change Userdefined units to mm and close the documents properties 31 | 5. Select the vector to change 32 | 6. Change the line thickness to 0.1mm 33 | 7. Zoom close to the line (to the upper left corner of the object, thats where the little handle of the next steps will appear) and set a help line on to the outer-perimeter of the vector. Set it to the inner-perimeter if you want to create an inset. 34 | 8. Switch to Path-modify-tool 35 | 9. Select Menu->Path->Dynamic Outset¨ 36 | 10. Grab the little handle and set the inner-perimeter (or outer-perimeter for an inset) of the vector onto the previously set help-line 37 | 11. If your lasercutter needs a special linethickness to recognise the cutline, set the linethickness accordingly. 38 | 12. Done. 39 | 40 | Your vector is now exacly a 0.1mm perimeter of the previous form, and the laser will cut right at the edge of the shape you defined in blender. 41 | 42 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # ***** GPL LICENSE BLOCK ***** 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (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, see . 15 | # All rights reserved. 16 | # 17 | # ***** GPL LICENSE BLOCK ***** 18 | 19 | bl_info = { 20 | "name": "Export SVG Format (.svg)", 21 | "author": "Martin Froehlich (maybites.ch) + Matthew Ready (craxic.com)", 22 | "version": (0, 0, 7), 23 | "blender": (2, 80, 0), 24 | "location": "File > Export > Inkscape (.svg)", 25 | "description": "The script exports Blender 2.80 BezierCurves to SVG format.", 26 | "warning": "No success guaranteed. Doesn't like objectnames with special characters", 27 | "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Import-Export/Inkscape_SVG_Exporter", 28 | "tracker_url": "", 29 | "category": "Import-Export" 30 | } 31 | 32 | # Ensure that we reload our dependencies if we ourselves are reloaded by Blender 33 | if "bpy" in locals(): 34 | import imp; 35 | if "exporter" in locals(): 36 | imp.reload(exporter); 37 | 38 | import bpy; 39 | from .exporter import Exporter; 40 | 41 | def menu_func_export_button(self, context): 42 | self.layout.operator(Exporter.bl_idname, text="Inkscape (.svg)"); 43 | 44 | classes = [ 45 | Exporter, 46 | ] 47 | 48 | def register(): 49 | for cls in classes: 50 | bpy.utils.register_class(cls) 51 | bpy.types.TOPBAR_MT_file_export.append(menu_func_export_button) 52 | 53 | def unregister(): # note how unregistering is done in reverse 54 | bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_button) 55 | for cls in reversed(classes): 56 | bpy.utils.unregister_class(cls) 57 | 58 | if __name__ == "__main__": 59 | register() 60 | -------------------------------------------------------------------------------- /exporter.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------- 2 | # Copyright (C) 2015: Martin Froehlich (maybites.ch), Matthew Ready (craxic.com) 3 | # 4 | # based on code from the defunct export script by jm soler, jmsoler_at_free.fr 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software Foundation, 18 | # -------------------------------------------------------------------------- 19 | 20 | import bpy 21 | from bpy_extras.io_utils import ExportHelper 22 | from bpy.types import BezierSplinePoint 23 | 24 | XML_HEADER = """ 25 | 26 | 27 | Bezier Curve : {2} 28 | This is an exported Bezier xml_path from Blender, using the ExportScript by maybites.ch and Craxic 29 | """ 30 | XML_PATH = """ 31 | """ 32 | XML_END = "" 33 | 34 | MOVE_COMMAND = 'M{},{} ' 35 | LINE_COMMAND = 'L{},{} ' 36 | CURVE_COMMAND = 'C{},{} {},{} {},{} ' 37 | JOIN_COMMAND = 'Z ' 38 | 39 | 40 | class CoordinateContext: 41 | def __init__(self, min_x, min_y, max_x, max_y): 42 | self.min_x = min_x 43 | self.min_y = min_y 44 | self.max_x = max_x 45 | self.max_y = max_y 46 | 47 | def cx(self, x): 48 | return x - self.min_x 49 | 50 | def cy(self, y): 51 | return self.max_y - y 52 | 53 | 54 | def make_curve_command(previous, point, ctx, scale, translation): 55 | return CURVE_COMMAND.format( 56 | ctx.cx(previous.handle_right[0] * scale[0] + translation[0]), 57 | ctx.cy(previous.handle_right[1] * scale[1] + translation[1]), 58 | ctx.cx(point.handle_left[0] * scale[0] + translation[0]), 59 | ctx.cy(point.handle_left[1] * scale[1] + translation[1]), 60 | ctx.cx(point.co[0] * scale[0] + translation[0]), 61 | ctx.cy(point.co[1] * scale[1] + translation[1])) 62 | 63 | 64 | def point_str(point, scale, translation): 65 | if isinstance(point, BezierSplinePoint): 66 | return "[x: {}, y: {}, z: {},\n" \ 67 | " x: {}, y: {}, z: {},\n" \ 68 | " x: {}, y: {}, z: {}]".format(point.handle_left[0] * scale[0] + translation[0], 69 | point.handle_left[1] * scale[1] + translation[1], 70 | point.handle_left[2] * scale[2] + translation[2], 71 | point.co[0] * scale[0] + translation[0], 72 | point.co[1] * scale[1] + translation[1], 73 | point.co[2] * scale[2] + translation[2], 74 | point.handle_right[0] * scale[0] + translation[0], 75 | point.handle_right[1] * scale[1] + translation[1], 76 | point.handle_right[2] * scale[2] + translation[2]) 77 | else: 78 | return "[x: {}, y: {}, z: {}]".format(point.co[0] * scale[0] + translation[0], 79 | point.co[1] * scale[1] + translation[1], 80 | point.co[2] * scale[2] + translation[2]) 81 | 82 | 83 | class Exporter(bpy.types.Operator, ExportHelper): 84 | bl_idname = "export_svg_format.svg" 85 | bl_label = "Inkscape SVG Exporter" 86 | bl_options = {'PRESET'} 87 | filename_ext = ".svg" 88 | 89 | def execute(self, context): 90 | # Ensure Blender is currently in OBJECT mode to allow data access. 91 | bpy.ops.object.mode_set(mode='OBJECT') 92 | 93 | # Set the default return state to FINISHED 94 | result = {'FINISHED'} 95 | 96 | # Check that the currently selected object contains mesh data for 97 | # exporting 98 | curve = bpy.context.selected_objects[0] 99 | if not curve or curve.type != 'CURVE': 100 | raise NameError("Cannot export: object %s is not a curve" % curve) 101 | 102 | # this scales from blender to svg (blender 1.0 = 1m while svg 1.0 = 1mm) 103 | scale = [1000, 1000, 1000]; #curve.dimensions * 1000; 104 | 105 | # consider unit settings 106 | unit = context.scene.unit_settings 107 | if unit.system == 'METRIC': 108 | scale[:] = [s * unit.scale_length for s in scale] 109 | elif unit.system == 'IMPERIAL': 110 | self.report({'WARNING'}, "Imperial units not implemented! Scale of output is most probably incorrect!") 111 | 112 | # bound_box[0] == [left, bottom, down] 113 | # bound_box[7] == [right, top, up] 114 | # bound_box[i] == [x, y, z], 0 <= i <= 7 115 | min_x = curve.bound_box[0][0] 116 | max_x = curve.bound_box[7][0] 117 | min_y = curve.bound_box[0][1] 118 | max_y = curve.bound_box[7][1] 119 | 120 | ctx = CoordinateContext(min_x, min_y, max_x, max_y) 121 | 122 | # this calculates the documents size 123 | width = (max_x - min_x) * scale[0] 124 | height = (max_y - min_y) * scale[1] 125 | 126 | # this centers the object inside the document 127 | translation = [- scale[0] * curve.bound_box[0][0], scale[1] * curve.bound_box[0][1], scale[2] * curve.bound_box[0][2]] 128 | 129 | paths = [] 130 | for spline in curve.data.splines: 131 | path = "" 132 | if spline.type == 'BEZIER': 133 | print("Exporting BEZIER curve.") 134 | first_curve_point = None 135 | previous = None 136 | for n, point in enumerate(spline.bezier_points): 137 | print("Point: " + point_str(point, scale, translation)) 138 | 139 | if n == 0: 140 | first_curve_point = point 141 | path += MOVE_COMMAND.format(ctx.cx(point.co[0] * scale[0] + translation[0]), 142 | ctx.cy(point.co[1] * scale[1] + translation[1])) 143 | else: 144 | path += make_curve_command(previous, point, ctx, scale, translation) 145 | previous = point 146 | 147 | if spline.use_cyclic_u == 1: 148 | path += make_curve_command(previous, first_curve_point, ctx, scale, translation) 149 | path += JOIN_COMMAND 150 | elif spline.type == 'POLY': 151 | print("Exporting POLY curve.") 152 | for n, point in enumerate(spline.points): 153 | command = MOVE_COMMAND if n == 0 else LINE_COMMAND 154 | path += command.format(ctx.cx(point.co[0] * scale[0] + translation[0]), 155 | ctx.cy(point.co[1] * scale[1] + translation[1])) 156 | print("Point: " + point_str(point, scale, translation)) 157 | 158 | if spline.use_cyclic_u == 1: 159 | path += JOIN_COMMAND 160 | paths.append(path) 161 | 162 | # Open the file for writing 163 | with open(self.filepath, 'w') as f: 164 | f.write(XML_HEADER.format(width, height, curve.name)) 165 | for path in paths: 166 | f.write(XML_PATH.format(path)) 167 | f.write(XML_END) 168 | 169 | return result 170 | --------------------------------------------------------------------------------