Sun Position: update translation
[blender-addons.git] / curve_tools / toolpath.py
blobffa2b185adeb617f74e4d125594549ae27b4041d
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
8 from . import internal
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)
21 @classmethod
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)
28 else:
29 splines = bpy.context.object.data.splines
31 if len(splines) == 0:
32 self.report({'WARNING'}, 'Nothing selected')
33 return {'CANCELLED'}
35 if bpy.context.object.mode != 'EDIT':
36 internal.addObject('CURVE', 'Offset Toolpath')
37 origin = bpy.context.scene.cursor.location
38 else:
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')
46 return {'CANCELLED'}
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)
49 for trace in traces:
50 internal.addPolygonSpline(bpy.context.object, spline.use_cyclic_u, [vertex-origin for vertex in trace])
51 return {'FINISHED'}
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)
62 @classmethod
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:
81 break
82 follow_edge_loop = False
83 for edge in current_vertex.link_edges:
84 if edge in edge_pool:
85 current_edge = edge
86 edge_pool.remove(current_edge)
87 follow_edge_loop = True
88 break
89 current_vertex = current_edge.other_vert(current_vertex)
90 vertices.append(current_vertex.co)
91 internal.addPolygonSpline(self.result, False, vertices)
92 aux_mesh.free()
94 def invoke(self, context, event):
95 if bpy.context.object.type != 'MESH':
96 self.report({'WARNING'}, 'Active object must be a mesh')
97 return {'CANCELLED'}
98 self.pitch = 0.1
99 self.offset = 0.0
100 self.slice_count = 3
101 self.mode = 'PITCH'
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)
121 )[0]
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':
141 self.mode = 'OFFSET'
142 return {'RUNNING_MODAL'}
143 elif self.mode == 'OFFSET':
144 self.mesh.free()
145 return {'FINISHED'}
146 elif event.type in {'RIGHTMOUSE', 'ESC'}:
147 self.mesh.free()
148 bpy.context.scene.collection.objects.unlink(self.result)
149 bpy.context.view_layer.objects.active = self.input_obj
150 return {'CANCELLED'}
151 else:
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)
164 @classmethod
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)
171 else:
172 splines = bpy.context.object.data.splines
174 if len(splines) == 0:
175 self.report({'WARNING'}, 'Nothing selected')
176 return {'CANCELLED'}
178 if bpy.context.object.mode != 'EDIT':
179 internal.addObject('CURVE', 'Dog Bone')
180 origin = bpy.context.scene.cursor.location
181 else:
182 origin = Vector((0.0, 0.0, 0.0))
184 for spline in splines:
185 if spline.type != 'BEZIER':
186 continue
187 result = internal.dogBone(spline, self.radius)
188 internal.addBezierSpline(bpy.context.object, spline.use_cyclic_u, result) # [vertex-origin for vertex in result])
189 return {'FINISHED'}
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)
199 @classmethod
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)
206 else:
207 splines = bpy.context.object.data.splines
209 if len(splines) == 0:
210 self.report({'WARNING'}, 'Nothing selected')
211 return {'CANCELLED'}
213 if bpy.context.object.mode != 'EDIT':
214 internal.addObject('CURVE', 'Discretized Curve')
215 origin = bpy.context.scene.cursor.location
216 else:
217 origin = Vector((0.0, 0.0, 0.0))
219 for spline in splines:
220 if spline.type != 'BEZIER':
221 continue
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])
224 return {'FINISHED'}
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)
234 @classmethod
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')
241 return {'CANCELLED'}
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
250 curve_traces = []
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:
255 i = len(trace[0])-1
256 while i > 1:
257 if (trace[0][i-1]-trace[0][i]).length < self.min_dist:
258 trace[0].pop(i-1)
259 trace[1].pop(i-1)
260 i -= 1
261 if self.z_hop:
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)
267 trace[0].append(end)
268 trace[1].append(1.0)
269 internal.addPolygonSpline(toolpath, False, [inverse_transform@vertex for vertex in trace[0]], trace[1])
270 return {'FINISHED'}
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))
287 else:
288 origin = bpy.context.scene.cursor.location
289 stride = math.copysign(self.stride, self.pitch)
290 length = self.length*0.5
291 vertices = []
292 weights = []
293 for i in range(0, self.track_count):
294 shift = i*self.pitch
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)
300 if stride != 0:
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)))
304 weights.append(1)
305 internal.addPolygonSpline(bpy.context.object, False, vertices, weights)
306 return {'FINISHED'}
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))
324 else:
325 origin = bpy.context.scene.cursor.location
326 count = int(self.vertex_count*self.screw_count)
327 height = -count/self.vertex_count*self.pitch
328 vertices = []
329 weights = []
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)
334 if self.radius > 0:
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):
338 if j > 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)
347 else:
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)
353 weights += [1, 1]
354 else:
355 weights += [self.speed, 1]
356 vertices += [origin+Vector((0.0, 0.0, height)), origin]
357 internal.addPolygonSpline(bpy.context.object, False, vertices, weights)
358 return {'FINISHED'}
360 def register():
361 for cls in classes:
362 bpy.utils.register_class(operators)
364 def unregister():
365 for cls in classes:
366 bpy.utils.unregister_class(operators)
368 if __name__ == "__main__":
369 register()
371 operators = [OffsetCurve, SliceMesh, DogBone, DiscretizeCurve, Truncate, RectMacro, DrillMacro]