1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 from mathutils
import Vector
, Matrix
7 from bpy_extras
.io_utils
import ExportHelper
10 class SvgExport(bpy
.types
.Operator
, ExportHelper
):
11 bl_idname
= 'export_svg_format.svg'
12 bl_description
= bl_label
= 'Curves (.svg)'
15 selection_only
: bpy
.props
.BoolProperty(name
='Selection only', description
='instead of exporting all visible curves')
16 absolute_coordinates
: bpy
.props
.BoolProperty(name
='Absolute coordinates', description
='instead of relative coordinates')
17 viewport_projection
: bpy
.props
.BoolProperty(name
='Viewport projection', description
='WYSIWYG instead of an local orthographic projection')
18 unit_name
: bpy
.props
.EnumProperty(name
='Unit', items
=internal
.units
, default
='mm')
20 def serialize_point(self
, position
, update_ref_position
=True):
22 position
= self
.transform
@Vector((position
[0], position
[1], position
[2], 1.0))
23 position
*= 0.5/position
.w
24 ref_position
= self
.origin
if self
.absolute_coordinates
else self
.ref_position
25 command
= '{:.3f},{:.3f}'.format((position
[0]-ref_position
[0])*self
.scale
[0], (position
[1]-ref_position
[1])*self
.scale
[1])
26 if update_ref_position
:
27 self
.ref_position
= position
30 def serialize_point_command(self
, point
, drawing
):
31 if self
.absolute_coordinates
:
32 return ('L' if drawing
else 'M')+self
.serialize_point(point
.co
)
34 return ('l' if drawing
else 'm')+self
.serialize_point(point
.co
)
36 def serialize_curve_command(self
, prev
, next
):
37 return ('C' if self
.absolute_coordinates
else 'c')+self
.serialize_point(prev
.handle_right
, False)+' '+self
.serialize_point(next
.handle_left
, False)+' '+self
.serialize_point(next
.co
)
39 def serialize_spline(self
, spline
):
41 points
= spline
.bezier_points
if spline
.type == 'BEZIER' else spline
.points
43 for index
, next
in enumerate(points
):
45 path
+= self
.serialize_point_command(next
, False)
46 elif spline
.type == 'BEZIER' and (points
[index
-1].handle_right_type
!= 'VECTOR' or next
.handle_left_type
!= 'VECTOR'):
47 path
+= self
.serialize_curve_command(points
[index
-1], next
)
49 path
+= self
.serialize_point_command(next
, True)
51 if spline
.use_cyclic_u
:
52 if spline
.type == 'BEZIER' and (points
[-1].handle_right_type
!= 'VECTOR' or points
[0].handle_left_type
!= 'VECTOR'):
53 path
+= self
.serialize_curve_command(points
[-1], points
[0])
55 self
.serialize_point(points
[0].co
)
56 path
+= 'Z' if self
.absolute_coordinates
else 'z'
60 def serialize_object(self
, obj
):
62 self
.transform
= self
.area
.spaces
.active
.region_3d
.perspective_matrix
@obj.matrix_world
63 self
.origin
= Vector((-0.5, 0.5, 0, 0))
66 self
.origin
= Vector((obj
.bound_box
[0][0], obj
.bound_box
[7][1], obj
.bound_box
[0][2], 0))
68 xml
= '\t<g id="'+obj
.name
+'">\n'
70 for spline
in obj
.data
.splines
:
72 if obj
.data
.dimensions
== '2D' and spline
.use_cyclic_u
:
73 if spline
.material_index
< len(obj
.data
.materials
) and obj
.data
.materials
[spline
.material_index
] != None:
74 style
= Vector(obj
.data
.materials
[spline
.material_index
].diffuse_color
)*255
76 style
= Vector((0.8, 0.8, 0.8))*255
77 style
= 'rgb({},{},{})'.format(round(style
[0]), round(style
[1]), round(style
[2]))
79 styles
[style
].append(spline
)
81 styles
[style
] = [spline
]
83 for style
, splines
in styles
.items():
84 style
= 'fill:'+style
+';'
85 if style
== 'fill:none;':
86 style
+= 'stroke:black;'
87 xml
+= '\t\t<path style="'+style
+'" d="'
88 self
.ref_position
= self
.origin
89 for spline
in splines
:
90 xml
+= self
.serialize_spline(spline
)
95 def execute(self
, context
):
96 objects
= bpy
.context
.selected_objects
if self
.selection_only
else bpy
.context
.visible_objects
99 if obj
.type == 'CURVE':
102 self
.report({'WARNING'}, 'Nothing to export')
106 if self
.viewport_projection
:
107 for area
in bpy
.context
.screen
.areas
:
108 if area
.type == 'VIEW_3D':
110 for region
in area
.regions
:
111 if region
.type == 'WINDOW':
113 if self
.region
== None:
116 self
.bounds
= Vector((self
.region
.width
, self
.region
.height
, 0))
117 self
.scale
= Vector(self
.bounds
)
118 if self
.unit_name
!= 'px':
121 if self
.area
== None:
122 self
.bounds
= Vector((0, 0, 0))
124 self
.bounds
[0] = max(self
.bounds
[0], obj
.bound_box
[7][0]-obj
.bound_box
[0][0])
125 self
.bounds
[1] = max(self
.bounds
[1], obj
.bound_box
[7][1]-obj
.bound_box
[0][1])
126 self
.scale
= Vector((1, 1, 0))
127 for unit
in internal
.units
:
128 if self
.unit_name
== unit
[0]:
129 self
.scale
*= 1.0/float(unit
[2])
131 self
.scale
*= context
.scene
.unit_settings
.scale_length
132 self
.bounds
= Vector(a
*b
for a
,b
in zip(self
.bounds
, self
.scale
))
135 with
open(self
.filepath
, 'w') as f
:
136 svg_view
= ('' if self
.unit_name
== '-' else 'width="{0:.3f}{2}" height="{1:.3f}{2}" ')+'viewBox="0 0 {0:.3f} {1:.3f}">\n'
137 f
.write('''<?xml version="1.0" standalone="no"?>
138 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
139 <svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" '''+svg_view
.format(self
.bounds
[0], self
.bounds
[1], self
.unit_name
))
141 f
.write(self
.serialize_object(obj
))
146 class GCodeExport(bpy
.types
.Operator
, ExportHelper
):
147 bl_idname
= 'export_gcode_format.gcode'
148 bl_description
= bl_label
= 'Toolpath (.gcode)'
149 filename_ext
= '.gcode'
151 speed
: bpy
.props
.FloatProperty(name
='Speed', description
='Maximal speed in mm / minute', min=0, default
=60)
152 step_angle
: bpy
.props
.FloatProperty(name
='Resolution', description
='Smaller values make curves smoother by adding more vertices', unit
='ROTATION', min=math
.pi
/128, default
=math
.pi
/16)
153 local_coordinates
: bpy
.props
.BoolProperty(name
='Local coords', description
='instead of global coordinates')
154 detect_circles
: bpy
.props
.BoolProperty(name
='Detect Circles', description
='Export bezier circles and helixes as G02 and G03') # TODO: Detect polygon circles too, merge consecutive circle segments
157 def poll(cls
, context
):
158 obj
= bpy
.context
.object
159 return obj
!= None and obj
.type == 'CURVE' and len(obj
.data
.splines
) == 1 and not obj
.data
.splines
[0].use_cyclic_u
161 def execute(self
, context
):
162 self
.scale
= Vector((1, 1, 1))
163 self
.scale
*= context
.scene
.unit_settings
.scale_length
*1000.0
164 with
open(self
.filepath
, 'w') as f
:
165 f
.write('G21\n') # Length is measured in millimeters
166 spline
= bpy
.context
.object.data
.splines
[0]
167 if spline
.use_cyclic_u
:
169 def transform(position
):
170 result
= Vector((position
[0]*self
.scale
[0], position
[1]*self
.scale
[1], position
[2]*self
.scale
[2])) # , 1.0
171 return result
if self
.local_coordinates
else bpy
.context
.object.matrix_world
@result
172 points
= spline
.bezier_points
if spline
.type == 'BEZIER' else spline
.points
174 for index
, current
in enumerate(points
):
175 speed
= self
.speed
*max(0.0, min(current
.weight_softbody
, 1.0))
176 if speed
!= prevSpeed
and current
.weight_softbody
!= 1.0:
177 f
.write('F{:.3f}\n'.format(speed
))
179 speed_code
= 'G00' if current
.weight_softbody
== 1.0 else 'G01'
180 prev
= points
[index
-1]
181 linear
= spline
.type != 'BEZIER' or index
== 0 or (prev
.handle_right_type
== 'VECTOR' and current
.handle_left_type
== 'VECTOR')
182 position
= transform(current
.co
)
184 f
.write(speed_code
+' X{:.3f} Y{:.3f} Z{:.3f}\n'.format(position
[0], position
[1], position
[2]))
186 segment_points
= internal
.bezierSegmentPoints(prev
, current
)
188 if self
.detect_circles
:
189 for axis
in range(0, 3):
190 projected_points
= []
191 for point
in segment_points
:
192 projected_point
= Vector(point
)
193 projected_point
[axis
] = 0.0
194 projected_points
.append(projected_point
)
195 circle
= internal
.circleOfBezier(projected_points
)
197 normal
= circle
.orientation
.col
[2]
198 center
= transform(circle
.center
-prev
.co
)
199 f
.write('G{} G0{} I{:.3f} J{:.3f} K{:.3f} X{:.3f} Y{:.3f} Z{:.3f}\n'.format(19-axis
, 3 if normal
[axis
] > 0.0 else 2, center
[0], center
[1], center
[2], position
[0], position
[1], position
[2]))
203 prev_tangent
= internal
.bezierTangentAt(segment_points
, 0).normalized()
204 for t
in range(1, bezier_samples
+1):
206 tangent
= internal
.bezierTangentAt(segment_points
, t
).normalized()
207 if t
== 1 or math
.acos(min(max(-1, prev_tangent
@tangent), 1)) >= self
.step_angle
:
208 position
= transform(internal
.bezierPointAt(segment_points
, t
))
209 prev_tangent
= tangent
210 f
.write(speed_code
+' X{:.3f} Y{:.3f} Z{:.3f}\n'.format(position
[0], position
[1], position
[2]))
215 bpy
.utils
.register_class(operators
)
219 bpy
.utils
.unregister_class(operators
)
221 if __name__
== "__main__":
224 operators
= [SvgExport
, GCodeExport
]