1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
, math
, bmesh
6 from bpy_extras
import view3d_utils
7 from mathutils
import Vector
, Matrix
10 class OffsetCurve(bpy
.types
.Operator
):
11 bl_idname
= 'curvetools.add_toolpath_offset_curve'
12 bl_description
= bl_label
= 'Offset Curve'
13 bl_options
= {'REGISTER', 'UNDO'}
15 offset
: bpy
.props
.FloatProperty(name
='Offset', description
='Distace between the original and the first trace', unit
='LENGTH', default
=0.1)
16 pitch
: bpy
.props
.FloatProperty(name
='Pitch', description
='Distace between two parallel traces', unit
='LENGTH', default
=0.1)
17 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)
18 count
: bpy
.props
.IntProperty(name
='Count', description
='Number of parallel traces', min=1, default
=1)
19 round_line_join
: bpy
.props
.BoolProperty(name
='Round Line Join', description
='Insert circle arcs at convex corners', default
=True)
22 def poll(cls
, context
):
23 return bpy
.context
.object != None and bpy
.context
.object.type == 'CURVE'
25 def execute(self
, context
):
26 if bpy
.context
.object.mode
== 'EDIT':
27 splines
= internal
.getSelectedSplines(True, True)
29 splines
= bpy
.context
.object.data
.splines
32 self
.report({'WARNING'}, 'Nothing selected')
35 if bpy
.context
.object.mode
!= 'EDIT':
36 internal
.addObject('CURVE', 'Offset Toolpath')
37 origin
= bpy
.context
.scene
.cursor
.location
39 origin
= Vector((0.0, 0.0, 0.0))
41 for spline
in splines
:
42 spline_points
= spline
.bezier_points
if spline
.type == 'BEZIER' else spline
.points
43 for spline_point
in spline_points
:
44 if spline_point
.co
.z
!= spline_points
[0].co
.z
:
45 self
.report({'WARNING'}, 'Curves must be planar and in XY plane')
47 for index
in range(0, self
.count
):
48 traces
= internal
.offsetPolygonOfSpline(spline
, self
.offset
+self
.pitch
*index
, self
.step_angle
, self
.round_line_join
)
50 internal
.addPolygonSpline(bpy
.context
.object, spline
.use_cyclic_u
, [vertex
-origin
for vertex
in trace
])
53 class SliceMesh(bpy
.types
.Operator
):
54 bl_idname
= 'curvetools.add_toolpath_slice_mesh'
55 bl_description
= bl_label
= 'Slice Mesh'
56 bl_options
= {'REGISTER', 'UNDO'}
58 pitch
: bpy
.props
.FloatProperty(name
='Pitch', unit
='LENGTH', description
='Distance between two slices', default
=0.1)
59 offset
: bpy
.props
.FloatProperty(name
='Offset', unit
='LENGTH', description
='Position of first slice along the axis', default
=0.0)
60 slice_count
: bpy
.props
.IntProperty(name
='Count', description
='Number of slices', min=1, default
=3)
63 def poll(cls
, context
):
64 return bpy
.context
.object != None and bpy
.context
.object.mode
== 'OBJECT'
66 def perform(self
, context
):
67 axis
= Vector((0.0, 0.0, 1.0))
68 for i
in range(0, self
.slice_count
):
69 aux_mesh
= self
.mesh
.copy()
70 cut_geometry
= bmesh
.ops
.bisect_plane(aux_mesh
, geom
=aux_mesh
.edges
[:]+aux_mesh
.faces
[:], dist
=0, plane_co
=axis
*(i
*self
.pitch
+self
.offset
), plane_no
=axis
, clear_outer
=False, clear_inner
=False)['geom_cut']
71 edge_pool
= set([e
for e
in cut_geometry
if isinstance(e
, bmesh
.types
.BMEdge
)])
72 while len(edge_pool
) > 0:
73 current_edge
= edge_pool
.pop()
74 first_vertex
= current_vertex
= current_edge
.verts
[0]
75 vertices
= [current_vertex
.co
]
76 follow_edge_loop
= len(edge_pool
) > 0
77 while follow_edge_loop
:
78 current_vertex
= current_edge
.other_vert(current_vertex
)
79 vertices
.append(current_vertex
.co
)
80 if current_vertex
== first_vertex
:
82 follow_edge_loop
= False
83 for edge
in current_vertex
.link_edges
:
86 edge_pool
.remove(current_edge
)
87 follow_edge_loop
= True
89 current_vertex
= current_edge
.other_vert(current_vertex
)
90 vertices
.append(current_vertex
.co
)
91 internal
.addPolygonSpline(self
.result
, False, vertices
)
94 def invoke(self
, context
, event
):
95 if bpy
.context
.object.type != 'MESH':
96 self
.report({'WARNING'}, 'Active object must be a mesh')
102 self
.input_obj
= bpy
.context
.object
103 depsgraph
= context
.evaluated_depsgraph_get()
104 self
.mesh
= bmesh
.new()
105 self
.mesh
.from_object(self
.input_obj
, depsgraph
, deform
=True, cage
=False, face_normals
=True)
106 self
.mesh
.transform(bpy
.context
.scene
.cursor
.matrix
.inverted()@self.input_obj
.matrix_world
)
107 self
.result
= internal
.addObject('CURVE', 'Slices')
108 self
.result
.matrix_world
= bpy
.context
.scene
.cursor
.matrix
109 self
.perform(context
)
110 context
.window_manager
.modal_handler_add(self
)
111 return {'RUNNING_MODAL'}
113 def modal(self
, context
, event
):
114 if event
.type == 'MOUSEMOVE':
115 mouse
= (event
.mouse_region_x
, event
.mouse_region_y
)
116 input_value
= internal
.nearestPointOfLines(
117 bpy
.context
.scene
.cursor
.location
,
118 bpy
.context
.scene
.cursor
.matrix
.col
[2].xyz
,
119 view3d_utils
.region_2d_to_origin_3d(context
.region
, context
.region_data
, mouse
),
120 view3d_utils
.region_2d_to_vector_3d(context
.region
, context
.region_data
, mouse
)
122 if self
.mode
== 'PITCH':
123 self
.pitch
= input_value
/(self
.slice_count
-1) if self
.slice_count
> 2 else input_value
124 elif self
.mode
== 'OFFSET':
125 self
.offset
= input_value
-self
.pitch
*0.5*((self
.slice_count
-1) if self
.slice_count
> 2 else 1.0)
126 elif event
.type == 'WHEELUPMOUSE':
127 if self
.slice_count
> 2:
128 self
.pitch
*= (self
.slice_count
-1)
129 self
.slice_count
+= 1
130 if self
.slice_count
> 2:
131 self
.pitch
/= (self
.slice_count
-1)
132 elif event
.type == 'WHEELDOWNMOUSE':
133 if self
.slice_count
> 2:
134 self
.pitch
*= (self
.slice_count
-1)
135 if self
.slice_count
> 1:
136 self
.slice_count
-= 1
137 if self
.slice_count
> 2:
138 self
.pitch
/= (self
.slice_count
-1)
139 elif event
.type == 'LEFTMOUSE' and event
.value
== 'RELEASE':
140 if self
.mode
== 'PITCH':
142 return {'RUNNING_MODAL'}
143 elif self
.mode
== 'OFFSET':
146 elif event
.type in {'RIGHTMOUSE', 'ESC'}:
148 bpy
.context
.scene
.collection
.objects
.unlink(self
.result
)
149 bpy
.context
.view_layer
.objects
.active
= self
.input_obj
152 return {'PASS_THROUGH'}
153 self
.result
.data
.splines
.clear()
154 self
.perform(context
)
155 return {'RUNNING_MODAL'}
157 class DogBone(bpy
.types
.Operator
):
158 bl_idname
= 'curvetools.add_toolpath_dogbone'
159 bl_description
= bl_label
= 'Dog Bone'
160 bl_options
= {'REGISTER', 'UNDO'}
162 radius
: bpy
.props
.FloatProperty(name
='Radius', description
='Tool radius to compensate for', unit
='LENGTH', min=0.0, default
=0.1)
165 def poll(cls
, context
):
166 return bpy
.context
.object != None and bpy
.context
.object.type == 'CURVE'
168 def execute(self
, context
):
169 if bpy
.context
.object.mode
== 'EDIT':
170 splines
= internal
.getSelectedSplines(True, False)
172 splines
= bpy
.context
.object.data
.splines
174 if len(splines
) == 0:
175 self
.report({'WARNING'}, 'Nothing selected')
178 if bpy
.context
.object.mode
!= 'EDIT':
179 internal
.addObject('CURVE', 'Dog Bone')
180 origin
= bpy
.context
.scene
.cursor
.location
182 origin
= Vector((0.0, 0.0, 0.0))
184 for spline
in splines
:
185 if spline
.type != 'BEZIER':
187 result
= internal
.dogBone(spline
, self
.radius
)
188 internal
.addBezierSpline(bpy
.context
.object, spline
.use_cyclic_u
, result
) # [vertex-origin for vertex in result])
191 class DiscretizeCurve(bpy
.types
.Operator
):
192 bl_idname
= 'curvetools.add_toolpath_discretize_curve'
193 bl_description
= bl_label
= 'Discretize Curve'
194 bl_options
= {'REGISTER', 'UNDO'}
196 step_angle
: bpy
.props
.FloatProperty(name
='Resolution', description
='Smaller values make curves smoother by adding more vertices', unit
='ROTATION', min=math
.pi
/512, default
=math
.pi
/16)
197 samples
: bpy
.props
.IntProperty(name
='Sample Count', description
='Number of samples to test per curve segment', min=1, default
=128)
200 def poll(cls
, context
):
201 return bpy
.context
.object != None and bpy
.context
.object.type == 'CURVE'
203 def execute(self
, context
):
204 if bpy
.context
.object.mode
== 'EDIT':
205 splines
= internal
.getSelectedSplines(True, False)
207 splines
= bpy
.context
.object.data
.splines
209 if len(splines
) == 0:
210 self
.report({'WARNING'}, 'Nothing selected')
213 if bpy
.context
.object.mode
!= 'EDIT':
214 internal
.addObject('CURVE', 'Discretized Curve')
215 origin
= bpy
.context
.scene
.cursor
.location
217 origin
= Vector((0.0, 0.0, 0.0))
219 for spline
in splines
:
220 if spline
.type != 'BEZIER':
222 result
= internal
.discretizeCurve(spline
, self
.step_angle
, self
.samples
)
223 internal
.addPolygonSpline(bpy
.context
.object, spline
.use_cyclic_u
, [vertex
-origin
for vertex
in result
])
226 class Truncate(bpy
.types
.Operator
):
227 bl_idname
= 'curvetools.add_toolpath_truncate'
228 bl_description
= bl_label
= 'Truncate'
229 bl_options
= {'REGISTER', 'UNDO'}
231 min_dist
: bpy
.props
.FloatProperty(name
='Min Distance', unit
='LENGTH', description
='Remove vertices which are too close together', min=0.0, default
=0.001)
232 z_hop
: bpy
.props
.BoolProperty(name
='Z Hop', description
='Add movements to the ceiling at trace ends', default
=True)
235 def poll(cls
, context
):
236 return bpy
.context
.object != None and bpy
.context
.object.mode
== 'OBJECT'
238 def execute(self
, context
):
239 if bpy
.context
.object.type != 'EMPTY' or bpy
.context
.object.empty_display_type
!= 'CUBE':
240 self
.report({'WARNING'}, 'Active object must be an empty of display type cube')
242 selection
= bpy
.context
.selected_objects
[:]
243 workspace
= bpy
.context
.object
244 aabb
= internal
.AABB(center
=Vector((0.0, 0.0, 0.0)), dimensions
=Vector((1.0, 1.0, 1.0))*workspace
.empty_display_size
)
245 toolpath
= internal
.addObject('CURVE', 'Truncated Toolpath')
246 for curve
in selection
:
247 if curve
.type == 'CURVE':
248 transform
= workspace
.matrix_world
.inverted()@curve.matrix_world
249 inverse_transform
= Matrix
.Translation(-toolpath
.location
)@workspace.matrix_world
251 for spline
in curve
.data
.splines
:
252 if spline
.type == 'POLY':
253 curve_traces
+= internal
.truncateToFitBox(transform
, spline
, aabb
)
254 for trace
in curve_traces
:
257 if (trace
[0][i
-1]-trace
[0][i
]).length
< self
.min_dist
:
262 begin
= Vector(trace
[0][0])
263 end
= Vector(trace
[0][-1])
264 begin
.z
= end
.z
= workspace
.empty_display_size
265 trace
[0].insert(0, begin
)
266 trace
[1].insert(0, 1.0)
269 internal
.addPolygonSpline(toolpath
, False, [inverse_transform
@vertex for vertex
in trace
[0]], trace
[1])
272 class RectMacro(bpy
.types
.Operator
):
273 bl_idname
= 'curvetools.add_toolpath_rect_macro'
274 bl_description
= bl_label
= 'Rect Macro'
275 bl_options
= {'REGISTER', 'UNDO'}
277 track_count
: bpy
.props
.IntProperty(name
='Number Tracks', description
='How many tracks', min=1, default
=10)
278 stride
: bpy
.props
.FloatProperty(name
='Stride', unit
='LENGTH', description
='Distance to previous track on the way back', min=0.0, default
=0.5)
279 pitch
: bpy
.props
.FloatProperty(name
='Pitch', unit
='LENGTH', description
='Distance between two tracks', default
=-1.0)
280 length
: bpy
.props
.FloatProperty(name
='Length', unit
='LENGTH', description
='Length of one track', default
=10.0)
281 speed
: bpy
.props
.FloatProperty(name
='Speed', description
='Stored in softbody goal weight', min=0.0, max=1.0, default
=0.1)
283 def execute(self
, context
):
284 if not internal
.curveObject():
285 internal
.addObject('CURVE', 'Rect Toolpath')
286 origin
= Vector((0.0, 0.0, 0.0))
288 origin
= bpy
.context
.scene
.cursor
.location
289 stride
= math
.copysign(self
.stride
, self
.pitch
)
290 length
= self
.length
*0.5
293 for i
in range(0, self
.track_count
):
295 flipped
= -1 if (stride
== 0 and i
%2 == 1) else 1
296 vertices
.append(origin
+Vector((shift
, -length
*flipped
, 0.0)))
297 weights
.append(self
.speed
)
298 vertices
.append(origin
+Vector((shift
, length
*flipped
, 0.0)))
299 weights
.append(self
.speed
)
301 vertices
.append(origin
+Vector((shift
-stride
, length
, 0.0)))
302 weights
.append(self
.speed
)
303 vertices
.append(origin
+Vector((shift
-stride
, -length
, 0.0)))
305 internal
.addPolygonSpline(bpy
.context
.object, False, vertices
, weights
)
308 class DrillMacro(bpy
.types
.Operator
):
309 bl_idname
= 'curvetools.add_toolpath_drill_macro'
310 bl_description
= bl_label
= 'Drill Macro'
311 bl_options
= {'REGISTER', 'UNDO'}
313 screw_count
: bpy
.props
.FloatProperty(name
='Screw Turns', description
='How many screw truns', min=1.0, default
=10.0)
314 spiral_count
: bpy
.props
.FloatProperty(name
='Spiral Turns', description
='How many spiral turns', min=0.0, default
=0.0)
315 vertex_count
: bpy
.props
.IntProperty(name
='Number Vertices', description
= 'How many vertices per screw turn', min=3, default
=32)
316 radius
: bpy
.props
.FloatProperty(name
='Radius', unit
='LENGTH', description
='Radius at tool center', min=0.0, default
=5.0)
317 pitch
: bpy
.props
.FloatProperty(name
='Pitch', unit
='LENGTH', description
='Distance between two screw turns', min=0.0, default
=1.0)
318 speed
: bpy
.props
.FloatProperty(name
='Speed', description
='Stored in softbody goal weight', min=0.0, max=1.0, default
=0.1)
320 def execute(self
, context
):
321 if not internal
.curveObject():
322 internal
.addObject('CURVE', 'Drill Toolpath')
323 origin
= Vector((0.0, 0.0, 0.0))
325 origin
= bpy
.context
.scene
.cursor
.location
326 count
= int(self
.vertex_count
*self
.screw_count
)
327 height
= -count
/self
.vertex_count
*self
.pitch
330 def addRadialVertex(param
, radius
, height
):
331 angle
= param
*math
.pi
*2
332 vertices
.append(origin
+Vector((math
.sin(angle
)*radius
, math
.cos(angle
)*radius
, height
)))
333 weights
.append(self
.speed
)
335 if self
.spiral_count
> 0.0:
336 sCount
= math
.ceil(self
.spiral_count
*self
.vertex_count
)
337 for j
in range(1, int(self
.screw_count
)+1):
339 vertices
.append(origin
+Vector((0.0, 0.0, sHeight
)))
340 weights
.append(self
.speed
)
341 sHeight
= max(-j
*self
.pitch
, height
)
342 for i
in range(0, sCount
+1):
343 sParam
= i
/self
.vertex_count
344 addRadialVertex(sParam
, i
/sCount
*self
.radius
, sHeight
)
345 for i
in range(0, self
.vertex_count
+1):
346 addRadialVertex(sParam
+(count
+i
)/self
.vertex_count
, self
.radius
, sHeight
)
348 for i
in range(0, count
):
349 param
= i
/self
.vertex_count
350 addRadialVertex(param
, self
.radius
, -param
*self
.pitch
)
351 for i
in range(0, self
.vertex_count
+1):
352 addRadialVertex((count
+i
)/self
.vertex_count
, self
.radius
, height
)
355 weights
+= [self
.speed
, 1]
356 vertices
+= [origin
+Vector((0.0, 0.0, height
)), origin
]
357 internal
.addPolygonSpline(bpy
.context
.object, False, vertices
, weights
)
362 bpy
.utils
.register_class(operators
)
366 bpy
.utils
.unregister_class(operators
)
368 if __name__
== "__main__":
371 operators
= [OffsetCurve
, SliceMesh
, DogBone
, DiscretizeCurve
, Truncate
, RectMacro
, DrillMacro
]