1 # SPDX-License-Identifier: GPL-2.0-or-later
4 from mathutils
import Vector
, Matrix
5 from bpy_extras
.io_utils
import ExportHelper
8 class SvgExport(bpy
.types
.Operator
, ExportHelper
):
9 bl_idname
= 'export_svg_format.svg'
10 bl_description
= bl_label
= 'Curves (.svg)'
13 selection_only
: bpy
.props
.BoolProperty(name
='Selection only', description
='instead of exporting all visible curves')
14 absolute_coordinates
: bpy
.props
.BoolProperty(name
='Absolute coordinates', description
='instead of relative coordinates')
15 viewport_projection
: bpy
.props
.BoolProperty(name
='Viewport projection', description
='WYSIWYG instead of an local orthographic projection')
16 unit_name
: bpy
.props
.EnumProperty(name
='Unit', items
=internal
.units
, default
='mm')
18 def serialize_point(self
, position
, update_ref_position
=True):
20 position
= self
.transform
@Vector((position
[0], position
[1], position
[2], 1.0))
21 position
*= 0.5/position
.w
22 ref_position
= self
.origin
if self
.absolute_coordinates
else self
.ref_position
23 command
= '{:.3f},{:.3f}'.format((position
[0]-ref_position
[0])*self
.scale
[0], (position
[1]-ref_position
[1])*self
.scale
[1])
24 if update_ref_position
:
25 self
.ref_position
= position
28 def serialize_point_command(self
, point
, drawing
):
29 if self
.absolute_coordinates
:
30 return ('L' if drawing
else 'M')+self
.serialize_point(point
.co
)
32 return ('l' if drawing
else 'm')+self
.serialize_point(point
.co
)
34 def serialize_curve_command(self
, prev
, next
):
35 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
)
37 def serialize_spline(self
, spline
):
39 points
= spline
.bezier_points
if spline
.type == 'BEZIER' else spline
.points
41 for index
, next
in enumerate(points
):
43 path
+= self
.serialize_point_command(next
, False)
44 elif spline
.type == 'BEZIER' and (points
[index
-1].handle_right_type
!= 'VECTOR' or next
.handle_left_type
!= 'VECTOR'):
45 path
+= self
.serialize_curve_command(points
[index
-1], next
)
47 path
+= self
.serialize_point_command(next
, True)
49 if spline
.use_cyclic_u
:
50 if spline
.type == 'BEZIER' and (points
[-1].handle_right_type
!= 'VECTOR' or points
[0].handle_left_type
!= 'VECTOR'):
51 path
+= self
.serialize_curve_command(points
[-1], points
[0])
53 self
.serialize_point(points
[0].co
)
54 path
+= 'Z' if self
.absolute_coordinates
else 'z'
58 def serialize_object(self
, obj
):
60 self
.transform
= self
.area
.spaces
.active
.region_3d
.perspective_matrix
@obj.matrix_world
61 self
.origin
= Vector((-0.5, 0.5, 0, 0))
64 self
.origin
= Vector((obj
.bound_box
[0][0], obj
.bound_box
[7][1], obj
.bound_box
[0][2], 0))
66 xml
= '\t<g id="'+obj
.name
+'">\n'
68 for spline
in obj
.data
.splines
:
70 if obj
.data
.dimensions
== '2D' and spline
.use_cyclic_u
:
71 if spline
.material_index
< len(obj
.data
.materials
) and obj
.data
.materials
[spline
.material_index
] != None:
72 style
= Vector(obj
.data
.materials
[spline
.material_index
].diffuse_color
)*255
74 style
= Vector((0.8, 0.8, 0.8))*255
75 style
= 'rgb({},{},{})'.format(round(style
[0]), round(style
[1]), round(style
[2]))
77 styles
[style
].append(spline
)
79 styles
[style
] = [spline
]
81 for style
, splines
in styles
.items():
82 style
= 'fill:'+style
+';'
83 if style
== 'fill:none;':
84 style
+= 'stroke:black;'
85 xml
+= '\t\t<path style="'+style
+'" d="'
86 self
.ref_position
= self
.origin
87 for spline
in splines
:
88 xml
+= self
.serialize_spline(spline
)
93 def execute(self
, context
):
94 objects
= bpy
.context
.selected_objects
if self
.selection_only
else bpy
.context
.visible_objects
97 if obj
.type == 'CURVE':
100 self
.report({'WARNING'}, 'Nothing to export')
104 if self
.viewport_projection
:
105 for area
in bpy
.context
.screen
.areas
:
106 if area
.type == 'VIEW_3D':
108 for region
in area
.regions
:
109 if region
.type == 'WINDOW':
111 if self
.region
== None:
114 self
.bounds
= Vector((self
.region
.width
, self
.region
.height
, 0))
115 self
.scale
= Vector(self
.bounds
)
116 if self
.unit_name
!= 'px':
119 if self
.area
== None:
120 self
.bounds
= Vector((0, 0, 0))
122 self
.bounds
[0] = max(self
.bounds
[0], obj
.bound_box
[7][0]-obj
.bound_box
[0][0])
123 self
.bounds
[1] = max(self
.bounds
[1], obj
.bound_box
[7][1]-obj
.bound_box
[0][1])
124 self
.scale
= Vector((1, 1, 0))
125 for unit
in internal
.units
:
126 if self
.unit_name
== unit
[0]:
127 self
.scale
*= 1.0/float(unit
[2])
129 self
.scale
*= context
.scene
.unit_settings
.scale_length
130 self
.bounds
= Vector(a
*b
for a
,b
in zip(self
.bounds
, self
.scale
))
133 with
open(self
.filepath
, 'w') as f
:
134 svg_view
= ('' if self
.unit_name
== '-' else 'width="{0:.3f}{2}" height="{1:.3f}{2}" ')+'viewBox="0 0 {0:.3f} {1:.3f}">\n'
135 f
.write('''<?xml version="1.0" standalone="no"?>
136 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
137 <svg xmlns="http://www.w3.org/2000/svg" '''+svg_view
.format(self
.bounds
[0], self
.bounds
[1], self
.unit_name
))
139 f
.write(self
.serialize_object(obj
))
144 class GCodeExport(bpy
.types
.Operator
, ExportHelper
):
145 bl_idname
= 'export_gcode_format.gcode'
146 bl_description
= bl_label
= 'Toolpath (.gcode)'
147 filename_ext
= '.gcode'
149 speed
: bpy
.props
.FloatProperty(name
='Speed', description
='Maximal speed in mm / minute', min=0, default
=60)
150 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)
151 local_coordinates
: bpy
.props
.BoolProperty(name
='Local coords', description
='instead of global coordinates')
152 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
155 def poll(cls
, context
):
156 obj
= bpy
.context
.object
157 return obj
!= None and obj
.type == 'CURVE' and len(obj
.data
.splines
) == 1 and not obj
.data
.splines
[0].use_cyclic_u
159 def execute(self
, context
):
160 self
.scale
= Vector((1, 1, 1))
161 self
.scale
*= context
.scene
.unit_settings
.scale_length
*1000.0
162 with
open(self
.filepath
, 'w') as f
:
163 f
.write('G21\n') # Length is measured in millimeters
164 spline
= bpy
.context
.object.data
.splines
[0]
165 if spline
.use_cyclic_u
:
167 def transform(position
):
168 result
= Vector((position
[0]*self
.scale
[0], position
[1]*self
.scale
[1], position
[2]*self
.scale
[2])) # , 1.0
169 return result
if self
.local_coordinates
else bpy
.context
.object.matrix_world
@result
170 points
= spline
.bezier_points
if spline
.type == 'BEZIER' else spline
.points
172 for index
, current
in enumerate(points
):
173 speed
= self
.speed
*max(0.0, min(current
.weight_softbody
, 1.0))
174 if speed
!= prevSpeed
and current
.weight_softbody
!= 1.0:
175 f
.write('F{:.3f}\n'.format(speed
))
177 speed_code
= 'G00' if current
.weight_softbody
== 1.0 else 'G01'
178 prev
= points
[index
-1]
179 linear
= spline
.type != 'BEZIER' or index
== 0 or (prev
.handle_right_type
== 'VECTOR' and current
.handle_left_type
== 'VECTOR')
180 position
= transform(current
.co
)
182 f
.write(speed_code
+' X{:.3f} Y{:.3f} Z{:.3f}\n'.format(position
[0], position
[1], position
[2]))
184 segment_points
= internal
.bezierSegmentPoints(prev
, current
)
186 if self
.detect_circles
:
187 for axis
in range(0, 3):
188 projected_points
= []
189 for point
in segment_points
:
190 projected_point
= Vector(point
)
191 projected_point
[axis
] = 0.0
192 projected_points
.append(projected_point
)
193 circle
= internal
.circleOfBezier(projected_points
)
195 normal
= circle
.orientation
.col
[2]
196 center
= transform(circle
.center
-prev
.co
)
197 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]))
201 prev_tangent
= internal
.bezierTangentAt(segment_points
, 0).normalized()
202 for t
in range(1, bezier_samples
+1):
204 tangent
= internal
.bezierTangentAt(segment_points
, t
).normalized()
205 if t
== 1 or math
.acos(min(max(-1, prev_tangent
@tangent), 1)) >= self
.step_angle
:
206 position
= transform(internal
.bezierPointAt(segment_points
, t
))
207 prev_tangent
= tangent
208 f
.write(speed_code
+' X{:.3f} Y{:.3f} Z{:.3f}\n'.format(position
[0], position
[1], position
[2]))
213 bpy
.utils
.register_class(operators
)
217 bpy
.utils
.unregister_class(operators
)
219 if __name__
== "__main__":
222 operators
= [SvgExport
, GCodeExport
]