Merge branch 'blender-v3.3-release'
[blender-addons.git] / btrace / bTrace.py
blob2ec92d72c1e9bbf64523cd062f1eee5ded7fa97c
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # TO DO LIST #
4 # Add more options to curve radius/modulation plus cyclic/connect curve option
6 import bpy
7 # import selection_utils
8 from bpy.types import Operator
9 from random import (
10 choice as rand_choice,
11 random as rand_random,
12 randint as rand_randint,
13 uniform as rand_uniform,
16 # Selection Module: Contributors: Mackraken, Andrew Hale (TrumanBlending)
17 # Adapted from Mackraken's "Tools for Curves" addon
20 selected = []
23 class SelectionOrder(bpy.types.Operator):
24 """Store the object names in the order they are selected, """ \
25 """use RETURN key to confirm selection, ESCAPE key to cancel"""
26 bl_idname = "object.select_order"
27 bl_label = "Select with Order"
28 bl_options = {'UNDO'}
30 num_selected = 0
32 @classmethod
33 def poll(self, context):
34 return bpy.context.mode == 'OBJECT'
36 def update(self, context):
37 # Get the currently selected objects
38 sel = context.selected_objects
39 num = len(sel)
41 if num == 0:
42 # Reset the list
43 del selected[:]
44 elif num > self.num_selected:
45 # Get all the newly selected objects and add
46 new = [ob.name for ob in sel if ob.name not in selected]
47 selected.extend(new)
48 elif num < self.num_selected:
49 # Get the selected objects and remove from list
50 curnames = {ob.name for ob in sel}
51 selected[:] = [name for name in selected if name in curnames]
53 # Set the number of currently select objects
54 self.num_selected = len(selected)
56 def modal(self, context, event):
57 if event.type == 'RET':
58 # If return is pressed, finish the operator
59 return {'FINISHED'}
60 elif event.type == 'ESC':
61 # If escape is pressed, cancel the operator
62 return {'CANCELLED'}
64 # Update selection if we need to
65 self.update(context)
66 return {'PASS_THROUGH'}
68 def invoke(self, context, event):
69 self.update(context)
71 context.window_manager.modal_handler_add(self)
72 return {'RUNNING_MODAL'}
74 def error_handlers(self, op_name, error, reports="ERROR", func=False):
75 if self and reports:
76 self.report({'WARNING'}, reports + " (See Console for more info)")
78 is_func = "Function" if func else "Operator"
79 print("\n[Btrace]\n{}: {}\nError: {}\n".format(op_name, is_func, error))
82 # Object Trace
83 # creates a curve with a modulated radius connecting points of a mesh
85 class OBJECT_OT_objecttrace(Operator):
86 bl_idname = "object.btobjecttrace"
87 bl_label = "Btrace: Object Trace"
88 bl_description = ("Trace selected mesh object with a curve with the option to animate\n"
89 "The Active Object has to be of a Mesh or Font type")
90 bl_options = {'REGISTER', 'UNDO'}
92 @classmethod
93 def poll(cls, context):
94 return (context.object and
95 context.object.type in {'MESH', 'FONT'})
97 def invoke(self, context, event):
98 try:
99 # Run through each selected object and convert to to a curved object
100 brushObj = context.selected_objects
101 Btrace = context.window_manager.curve_tracer
102 check_materials = True
103 # Duplicate Mesh
104 if Btrace.object_duplicate:
105 bpy.ops.object.duplicate_move()
106 brushObj = context.selected_objects
107 # Join Mesh
108 if Btrace.convert_joinbefore:
109 if len(brushObj) > 1: # Only run if multiple objects selected
110 bpy.ops.object.join()
111 brushObj = context.selected_objects
113 for i in brushObj:
114 context.view_layer.objects.active = i
115 if i and i.type != 'CURVE':
116 bpy.ops.object.btconvertcurve()
117 # Materials
118 trace_mats = addtracemat(bpy.context.object.data)
119 if not trace_mats and check_materials is True:
120 check_materials = False
121 if Btrace.animate:
122 bpy.ops.curve.btgrow()
124 if check_materials is False:
125 self.report({'WARNING'}, "Some Materials could not be added")
127 return {'FINISHED'}
129 except Exception as e:
130 error_handlers(self, "object.btobjecttrace", e,
131 "Object Trace could not be completed")
133 return {'CANCELLED'}
136 # Objects Connect
137 # connect selected objects with a curve + hooks to each node
138 # possible handle types: 'FREE' 'AUTO' 'VECTOR' 'ALIGNED'
140 class OBJECT_OT_objectconnect(Operator):
141 bl_idname = "object.btobjectsconnect"
142 bl_label = "Btrace: Objects Connect"
143 bl_description = ("Connect selected objects with a curve and add hooks to each node\n"
144 "Needs at least two objects selected")
145 bl_options = {'REGISTER', 'UNDO'}
147 @classmethod
148 def poll(cls, context):
149 return len(context.selected_objects) > 1
151 def invoke(self, context, event):
152 try:
153 lists = []
154 Btrace = context.window_manager.curve_tracer
155 curve_handle = Btrace.curve_handle
156 if curve_handle == 'AUTOMATIC': # hackish because of naming conflict in api
157 curve_handle = 'AUTO'
158 # Check if Btrace group exists, if not create
159 bcollection = bpy.data.collections.keys()
160 if 'Btrace' not in bcollection:
161 mycol=bpy.data.collections.new(name="Btrace")
162 bpy.context.scene.collection.children.link(mycol)
163 # check if noise
164 if Btrace.connect_noise:
165 bpy.ops.object.btfcnoise()
166 # check if respect order is checked, create list of objects
167 if Btrace.respect_order is True:
168 selobnames = selection_utils.selected
169 obnames = []
170 for ob in selobnames:
171 obnames.append(bpy.data.objects[ob])
172 else:
173 obnames = bpy.context.selected_objects # No selection order
175 for a in obnames:
176 lists.append(a)
177 a.select_set(False)
179 # trace the origins
180 tracer = bpy.data.curves.new('tracer', 'CURVE')
181 tracer.dimensions = '3D'
182 spline = tracer.splines.new('BEZIER')
183 spline.bezier_points.add(len(lists) - 1)
184 curve = bpy.data.objects.new('curve', tracer)
185 bpy.context.collection.objects.link(curve)
187 # render ready curve
188 tracer.resolution_u = Btrace.curve_u
189 # Set bevel resolution from Panel options
190 tracer.bevel_resolution = Btrace.curve_resolution
191 tracer.fill_mode = 'FULL'
192 # Set bevel depth from Panel options
193 tracer.bevel_depth = Btrace.curve_depth
195 # move nodes to objects
196 for i in range(len(lists)):
197 p = spline.bezier_points[i]
198 p.co = lists[i].location
199 p.handle_right_type = curve_handle
200 p.handle_left_type = curve_handle
202 bpy.context.view_layer.objects.active = curve
203 bpy.ops.object.mode_set(mode='OBJECT')
205 # place hooks
206 for i in range(len(lists)):
207 lists[i].select_set(True)
208 curve.data.splines[0].bezier_points[i].select_control_point = True
209 bpy.ops.object.mode_set(mode='EDIT')
210 bpy.ops.object.hook_add_selob()
211 bpy.ops.object.mode_set(mode='OBJECT')
212 curve.data.splines[0].bezier_points[i].select_control_point = False
213 lists[i].select_set(False)
215 bpy.ops.object.select_all(action='DESELECT')
216 curve.select_set(True) # selected curve after it's created
217 # Materials
218 check_materials = True
219 trace_mats = addtracemat(bpy.context.object.data)
220 if not trace_mats and check_materials is True:
221 check_materials = False
223 if Btrace.animate: # Add Curve Grow it?
224 bpy.ops.curve.btgrow()
226 bpy.data.collections["Btrace"].objects.link(curve) # add to Btrace collection
228 # Check if we add grow curve
229 if Btrace.animate:
230 bpy.ops.curve.btgrow() # Add grow curve
232 if check_materials is False:
233 self.report({'WARNING'}, "Some Materials could not be added")
235 return {'FINISHED'}
237 except Exception as e:
238 error_handlers(self, "object.btobjectsconnect", e,
239 "Objects Connect could not be completed")
241 return {'CANCELLED'}
244 # Particle Trace
245 # creates a curve from each particle of a system
247 def curvetracer(curvename, splinename):
248 Btrace = bpy.context.window_manager.curve_tracer
249 tracer = bpy.data.curves.new(splinename, 'CURVE')
250 tracer.dimensions = '3D'
251 curve = bpy.data.objects.new(curvename, tracer)
252 bpy.context.collection.objects.link(curve)
253 try:
254 tracer.fill_mode = 'FULL'
255 except:
256 tracer.use_fill_front = tracer.use_fill_back = False
257 tracer.bevel_resolution = Btrace.curve_resolution
258 tracer.bevel_depth = Btrace.curve_depth
259 tracer.resolution_u = Btrace.curve_u
260 return tracer, curve
262 # Particle Trace
263 class OBJECT_OT_particletrace(Operator):
264 bl_idname = "particles.particletrace"
265 bl_label = "Btrace: Particle Trace"
266 bl_description = ("Creates a curve from each particle of a system.\n"
267 "Keeping particle amount under 250 will make this run faster")
268 bl_options = {'REGISTER', 'UNDO'}
270 @classmethod
271 def poll(cls, context):
272 return (context.object is not None and
273 context.object.particle_systems)
275 def execute(self, context):
276 try:
277 Btrace = bpy.context.window_manager.curve_tracer
278 particle_step = Btrace.particle_step # step size in frames
279 obj = bpy.context.object
280 obj = bpy.context.evaluated_depsgraph_get().objects.get(obj.name, None)
281 ps = obj.particle_systems.active
282 curvelist = []
283 curve_handle = Btrace.curve_handle
284 check_materials = True
286 if curve_handle == 'AUTOMATIC': # hackish naming conflict
287 curve_handle = 'AUTO'
288 if curve_handle == 'FREE_ALIGN': # hackish naming conflict
289 curve_handle = 'FREE'
291 # Check if Btrace group exists, if not create
292 bcollection = bpy.data.collections.keys()
293 if 'Btrace' not in bcollection:
294 mycol=bpy.data.collections.new(name="Btrace")
295 bpy.context.scene.collection.children.link(mycol)
297 if Btrace.curve_join:
298 tracer = curvetracer('Tracer', 'Splines')
300 for x in ps.particles:
301 if not Btrace.curve_join:
302 tracer = curvetracer('Tracer.000', 'Spline.000')
303 spline = tracer[0].splines.new('BEZIER')
305 # add point to spline based on step size
306 spline.bezier_points.add(int((x.lifetime - 1) // particle_step))
307 for t in list(range(int(x.lifetime))):
308 bpy.context.scene.frame_set(int(t + x.birth_time))
310 if not t % particle_step:
311 p = spline.bezier_points[t // particle_step]
312 p.co = x.location
313 p.handle_right_type = curve_handle
314 p.handle_left_type = curve_handle
315 particlecurve = tracer[1]
316 curvelist.append(particlecurve)
318 # add to group
319 bpy.ops.object.select_all(action='DESELECT')
320 for curveobject in curvelist:
321 curveobject.select_set(True)
322 bpy.context.view_layer.objects.active = curveobject
323 bpy.data.collections["Btrace"].objects.link(curveobject)
324 # Materials
325 trace_mats = addtracemat(curveobject.data)
326 if not trace_mats and check_materials is True:
327 check_materials = False
329 if Btrace.animate:
330 bpy.ops.curve.btgrow() # Add grow curve
332 if check_materials is False:
333 self.report({'WARNING'}, "Some Materials could not be added")
335 return {'FINISHED'}
337 except Exception as e:
338 error_handlers(self, "particles.particletrace", e,
339 "Particle Trace could not be completed")
341 return {'CANCELLED'}
344 # Particle Connect
345 # connect all particles in active system with a continuous animated curve
347 class OBJECT_OT_traceallparticles(Operator):
348 bl_idname = "particles.connect"
349 bl_label = "Connect Particles"
350 bl_description = ("Create a continuous animated curve from particles in active system\n"
351 "Needs an Object with a particle system attached")
352 bl_options = {'REGISTER', 'UNDO'}
354 @classmethod
355 def poll(cls, context):
356 return (context.object is not None and
357 context.object.particle_systems)
359 def execute(self, context):
360 try:
361 obj = context.object
362 obj = bpy.context.evaluated_depsgraph_get().objects.get(obj.name, None)
363 ps = obj.particle_systems.active
364 setting = ps.settings
366 # Grids distribution not supported
367 if setting.distribution == 'GRID':
368 self.report({'INFO'},
369 "Grid distribution mode for particles not supported")
370 return{'CANCELLED'}
372 Btrace = bpy.context.window_manager.curve_tracer
373 # Get frame start
374 particle_f_start = Btrace.particle_f_start
375 # Get frame end
376 particle_f_end = Btrace.particle_f_end
377 curve_handle = Btrace.curve_handle
378 # hackish because of naming conflict in api
379 if curve_handle == 'AUTOMATIC':
380 curve_handle = 'AUTO'
381 if curve_handle == 'FREE_ALIGN':
382 curve_handle = 'FREE'
384 # define what kind of object to create
385 tracer = bpy.data.curves.new('Splines', 'CURVE')
386 # Create new object with settings listed above
387 curve = bpy.data.objects.new('Tracer', tracer)
388 # Link newly created object to the scene
389 # bpy.context.view_layer.objects.link(curve)
390 bpy.context.scene.collection.objects.link(curve)
391 # add a new Bezier point in the new curve
392 spline = tracer.splines.new('BEZIER')
393 spline.bezier_points.add(setting.count - 1)
395 tracer.dimensions = '3D'
396 tracer.resolution_u = Btrace.curve_u
397 tracer.bevel_resolution = Btrace.curve_resolution
398 tracer.fill_mode = 'FULL'
399 tracer.bevel_depth = Btrace.curve_depth
401 if Btrace.particle_auto:
402 f_start = int(setting.frame_start)
403 f_end = int(setting.frame_end + setting.lifetime)
404 else:
405 if particle_f_end <= particle_f_start:
406 particle_f_end = particle_f_start + 1
407 f_start = particle_f_start
408 f_end = particle_f_end
410 for bFrames in range(f_start, f_end):
411 bpy.context.scene.frame_set(bFrames)
412 if not (bFrames - f_start) % Btrace.particle_step:
413 for bFrames in range(setting.count):
414 if ps.particles[bFrames].alive_state != 'UNBORN':
415 e = bFrames
416 bp = spline.bezier_points[bFrames]
417 pt = ps.particles[e]
418 bp.co = pt.location
419 bp.handle_right_type = curve_handle
420 bp.handle_left_type = curve_handle
421 bp.keyframe_insert('co')
422 bp.keyframe_insert('handle_left')
423 bp.keyframe_insert('handle_right')
424 # Select new curve
425 bpy.ops.object.select_all(action='DESELECT')
426 curve.select_set(True)
427 bpy.context.view_layer.objects.active = curve
429 # Materials
430 trace_mats = addtracemat(curve.data)
431 if not trace_mats:
432 self.report({'WARNING'}, "Some Materials could not be added")
434 if Btrace.animate:
435 bpy.ops.curve.btgrow()
437 return{'FINISHED'}
439 except Exception as e:
440 error_handlers(self, "particles.connect", e,
441 "Connect Particles could not be completed")
443 return {'CANCELLED'}
446 # Writing Tool
447 # Writes a curve by animating its point's radii
449 class OBJECT_OT_writing(Operator):
450 bl_idname = "curve.btwriting"
451 bl_label = "Write"
452 bl_description = ("Use Grease Pencil to write and convert to curves\n"
453 "Needs an existing Grease Pencil layer attached to the Scene")
454 bl_options = {'REGISTER', 'UNDO'}
456 @classmethod
457 def poll(cls, context):
458 gp = context.scene.grease_pencil
459 return gp and gp.layers
461 def execute(self, context):
462 try:
463 # first check if the Grease Pencil is attached to the Scene
464 tool_settings = context.scene.tool_settings
465 source_data = tool_settings.grease_pencil_source
466 if source_data in {"OBJECT"}:
467 self.report({'WARNING'},
468 "Operation Cancelled. "
469 "The Grease Pencil data-block is attached to an Object")
470 return {"CANCELLED"}
472 Btrace = context.window_manager.curve_tracer
473 # this is hacky - store objects in the scene for comparison later
474 store_objects = [ob for ob in context.scene.objects]
476 gactive = context.active_object
477 # checking if there are any strokes the easy way
478 if not bpy.ops.gpencil.convert.poll():
479 self.report({'WARNING'},
480 "Operation Cancelled. "
481 "Are there any Grease Pencil Strokes ?")
482 return {'CANCELLED'}
484 bpy.ops.gpencil.convert(type='CURVE')
485 # get curve after convert (compare the scenes to get the difference)
486 scene_obj = context.scene.objects
487 check_materials = True
489 for obj in scene_obj:
490 if obj not in store_objects and obj.type == "CURVE":
491 gactiveCurve = obj
492 break
494 # render ready curve
495 gactiveCurve.data.resolution_u = Btrace.curve_u
496 # Set bevel resolution from Panel options
497 gactiveCurve.data.bevel_resolution = Btrace.curve_resolution
498 gactiveCurve.data.fill_mode = 'FULL'
499 # Set bevel depth from Panel options
500 gactiveCurve.data.bevel_depth = Btrace.curve_depth
502 writeObj = context.selected_objects
503 if Btrace.animate:
504 for i in writeObj:
505 context.view_layer.objects.active = i
506 bpy.ops.curve.btgrow()
507 # Materials
508 trace_mats = addtracemat(bpy.context.object.data)
509 if not trace_mats and check_materials is True:
510 check_materials = False
511 else:
512 for i in writeObj:
513 context.view_layer.objects.active = i
514 # Materials
515 trace_mats = addtracemat(bpy.context.object.data)
516 if not trace_mats and check_materials is True:
517 check_materials = False
519 # Delete grease pencil strokes
520 context.view_layer.objects.active = gactive
521 bpy.ops.gpencil.data_unlink()
522 context.view_layer.objects.active = gactiveCurve
523 # Smooth object
524 bpy.ops.object.shade_smooth()
525 # Return to first frame
526 bpy.context.scene.frame_set(Btrace.anim_f_start)
528 if check_materials is False:
529 self.report({'WARNING'}, "Some Materials could not be added")
531 return{'FINISHED'}
533 except Exception as e:
534 error_handlers(self, "curve.btwriting", e,
535 "Grease Pencil conversion could not be completed")
537 return {'CANCELLED'}
540 # Create Curve
541 # Convert mesh to curve using either Continuous, All Edges, or Sharp Edges
542 # Option to create noise
544 class OBJECT_OT_convertcurve(Operator):
545 bl_idname = "object.btconvertcurve"
546 bl_label = "Btrace: Create Curve"
547 bl_description = ("Convert Mesh to Curve using either Continuous, "
548 "All Edges or Sharp Edges\n"
549 "Active Object has to be of a Mesh or Font type")
550 bl_options = {'REGISTER', 'UNDO'}
552 @classmethod
553 def poll(cls, context):
554 return (context.object is not None and
555 context.object.type in {"MESH", "FONT"})
557 def execute(self, context):
558 try:
559 Btrace = context.window_manager.curve_tracer
560 obj = context.object
562 # Convert Font
563 if obj.type == 'FONT':
564 bpy.ops.object.mode_set(mode='OBJECT')
565 bpy.ops.object.convert(target='CURVE') # Convert edges to curve
566 bpy.context.object.data.dimensions = '3D'
568 # make a continuous edge through all vertices
569 if obj.type == 'MESH':
570 # Add noise to mesh
571 if Btrace.distort_curve:
572 for v in obj.data.vertices:
573 for u in range(3):
574 v.co[u] += Btrace.distort_noise * (rand_uniform(-1, 1))
576 if Btrace.convert_edgetype == 'CONTI':
577 # Start Continuous edge
578 bpy.ops.object.mode_set(mode='EDIT')
579 bpy.ops.mesh.select_all(action='SELECT')
580 bpy.ops.mesh.delete(type='EDGE_FACE')
581 bpy.ops.mesh.select_all(action='DESELECT')
582 verts = bpy.context.object.data.vertices
583 bpy.ops.object.mode_set(mode='OBJECT')
584 li = []
585 p1 = rand_randint(0, len(verts) - 1)
587 for v in verts:
588 li.append(v.index)
589 li.remove(p1)
590 for z in range(len(li)):
591 x = []
592 for px in li:
593 d = verts[p1].co - verts[px].co # find distance from first vert
594 x.append(d.length)
595 p2 = li[x.index(min(x))] # find the shortest distance list index
596 verts[p1].select = verts[p2].select = True
597 bpy.ops.object.mode_set(mode='EDIT')
598 bpy.context.tool_settings.mesh_select_mode = [True, False, False]
599 bpy.ops.mesh.edge_face_add()
600 bpy.ops.mesh.select_all(action='DESELECT')
601 bpy.ops.object.mode_set(mode='OBJECT')
602 li.remove(p2) # remove item from list.
603 p1 = p2
604 # Convert edges to curve
605 bpy.ops.object.mode_set(mode='OBJECT')
606 bpy.ops.object.convert(target='CURVE')
608 if Btrace.convert_edgetype == 'EDGEALL':
609 # Start All edges
610 bpy.ops.object.mode_set(mode='EDIT')
611 bpy.ops.mesh.select_all(action='SELECT')
612 bpy.ops.mesh.delete(type='ONLY_FACE')
613 bpy.ops.object.mode_set()
614 bpy.ops.object.convert(target='CURVE')
615 for sp in obj.data.splines:
616 sp.type = Btrace.curve_spline
618 obj = context.object
619 # Set spline type to custom property in panel
620 bpy.ops.object.editmode_toggle()
621 bpy.ops.curve.spline_type_set(type=Btrace.curve_spline)
622 # Set handle type to custom property in panel
623 bpy.ops.curve.handle_type_set(type=Btrace.curve_handle)
624 bpy.ops.object.editmode_toggle()
625 obj.data.fill_mode = 'FULL'
626 # Set resolution to custom property in panel
627 obj.data.bevel_resolution = Btrace.curve_resolution
628 obj.data.resolution_u = Btrace.curve_u
629 # Set depth to custom property in panel
630 obj.data.bevel_depth = Btrace.curve_depth
631 # Smooth object
632 bpy.ops.object.shade_smooth()
633 # Modulate curve radius and add distortion
634 if Btrace.distort_curve:
635 scale = Btrace.distort_modscale
636 if scale == 0:
637 return {'FINISHED'}
638 for u in obj.data.splines:
639 for v in u.bezier_points:
640 v.radius = scale * round(rand_random(), 3)
642 return {'FINISHED'}
644 except Exception as e:
645 error_handlers(self, "object.btconvertcurve", e,
646 "Conversion could not be completed")
648 return {'CANCELLED'}
651 # Mesh Follow, trace vertex or faces
652 # Create curve at center of selection item, extruded along animation
653 # Needs to be an animated mesh!!!
655 class OBJECT_OT_meshfollow(Operator):
656 bl_idname = "object.btmeshfollow"
657 bl_label = "Btrace: Vertex Trace"
658 bl_description = "Trace Vertex or Face on an animated mesh"
659 bl_options = {'REGISTER', 'UNDO'}
661 @classmethod
662 def poll(cls, context):
663 return (context.object and context.object.type in {'MESH'})
665 def execute(self, context):
666 try:
667 Btrace = context.window_manager.curve_tracer
668 stepsize = Btrace.particle_step
670 obj = context.object
671 scn = context.scene
672 drawmethod = Btrace.fol_mesh_type # Draw from Edges, Verts, or Faces
674 if drawmethod == 'VERTS':
675 meshobjs = obj.data.vertices
676 if drawmethod == 'FACES':
677 meshobjs = obj.data.polygons # untested
678 if drawmethod == 'EDGES':
679 meshobjs = obj.data.edges # untested
681 # Frame properties
682 start_frame, end_frame = Btrace.fol_start_frame, Btrace.fol_end_frame
683 if start_frame > end_frame: # Make sure the math works
684 start_frame = end_frame - 5 # if start past end, goto (end - 5)
685 frames = int((end_frame - start_frame) / stepsize)
687 def getsel_option(): # Get selection objects
688 sel = []
689 # options are 'random', 'custom', 'all'
690 seloption, fol_mesh_type = Btrace.fol_sel_option, Btrace.fol_mesh_type
691 if fol_mesh_type == 'OBJECT':
692 pass
693 else:
694 if seloption == 'CUSTOM':
695 for i in meshobjs:
696 if i.select_get() is True:
697 sel.append(i.index)
698 if seloption == 'RANDOM':
699 for i in list(meshobjs):
700 sel.append(i.index)
701 finalsel = int(len(sel) * Btrace.fol_perc_verts)
702 remove = len(sel) - finalsel
703 for i in range(remove):
704 sel.pop(rand_randint(0, len(sel) - 1))
705 if seloption == 'ALL':
706 for i in list(meshobjs):
707 sel.append(i.index)
709 return sel
711 def get_coord(objindex):
712 obj_co = [] # list of vector coordinates to use
713 frame_x = start_frame
714 for i in range(frames): # create frame numbers list
715 scn.frame_set(frame_x)
716 if drawmethod != 'OBJECT':
717 followed_item = meshobjs[objindex]
718 if drawmethod == 'VERTS':
719 # find Vert vector
720 g_co = obj.matrix_local @ followed_item.co
722 if drawmethod == 'FACES':
723 # find Face vector
724 g_co = obj.matrix_local @ followed_item.normal
726 if drawmethod == 'EDGES':
727 v1 = followed_item.vertices[0]
728 v2 = followed_item.vertices[1]
729 co1 = bpy.context.object.data.vertices[v1]
730 co2 = bpy.context.object.data.vertices[v2]
731 localcenter = co1.co.lerp(co2.co, 0.5)
732 g_co = obj.matrix_world @ localcenter
734 if drawmethod == 'OBJECT':
735 g_co = objindex.location.copy()
737 obj_co.append(g_co)
738 frame_x = frame_x + stepsize
740 scn.frame_set(start_frame)
741 return obj_co
743 def make_curve(co_list):
744 Btrace = bpy.context.window_manager.curve_tracer
745 tracer = bpy.data.curves.new('tracer', 'CURVE')
746 tracer.dimensions = '3D'
747 spline = tracer.splines.new('BEZIER')
748 spline.bezier_points.add(len(co_list) - 1)
749 curve = bpy.data.objects.new('curve', tracer)
750 scn.collection.objects.link(curve)
751 curvelist.append(curve)
752 # render ready curve
753 tracer.resolution_u = Btrace.curve_u
754 # Set bevel resolution from Panel options
755 tracer.bevel_resolution = Btrace.curve_resolution
756 tracer.fill_mode = 'FULL'
757 # Set bevel depth from Panel options
758 tracer.bevel_depth = Btrace.curve_depth
759 curve_handle = Btrace.curve_handle
761 # hackish AUTOMATIC doesn't work here
762 if curve_handle == 'AUTOMATIC':
763 curve_handle = 'AUTO'
765 # move bezier points to objects
766 for i in range(len(co_list)):
767 p = spline.bezier_points[i]
768 p.co = co_list[i]
769 p.handle_right_type = curve_handle
770 p.handle_left_type = curve_handle
771 return curve
773 # Run methods
774 # Check if Btrace group exists, if not create
775 bcollection = bpy.data.collections.keys()
776 if 'Btrace' not in bcollection:
777 mycol=bpy.data.collections.new(name="Btrace")
778 bpy.context.scene.collection.children.link(mycol)
780 Btrace = bpy.context.window_manager.curve_tracer
781 sel = getsel_option() # Get selection
782 curvelist = [] # list to use for grow curve
783 check_materials = True
785 if Btrace.fol_mesh_type == 'OBJECT':
786 vector_list = get_coord(obj)
787 curvelist.append(make_curve(vector_list))
788 else:
789 for i in sel:
790 print(i)
791 vector_list = get_coord(i)
792 make_curve(vector_list)
793 # curvelist.append(make_curve(vector_list)) # append happens in function
794 # Select new curves and add to group
795 bpy.ops.object.select_all(action='DESELECT')
796 for curveobject in curvelist:
797 if curveobject.type == 'CURVE':
798 curveobject.select_set(True)
799 bpy.context.view_layer.objects.active = curveobject
801 bpy.data.collections["Btrace"].objects.link(curveobject) #2.8 link obj to collection
802 bpy.context.scene.collection.objects.unlink(curveobject) # unlink from scene collection
803 # bpy.ops.object.group_link(group="Btrace")
804 # Materials
805 trace_mats = addtracemat(curveobject.data)
806 if not trace_mats and check_materials is True:
807 check_materials = False
809 curveobject.select_set(False)
811 if Btrace.animate: # Add grow curve
812 for curveobject in curvelist:
813 curveobject.select_set(True)
814 bpy.ops.curve.btgrow()
815 for curveobject in curvelist:
816 curveobject.select_set(False)
818 obj.select_set(False) # Deselect original object
820 if check_materials is False:
821 self.report({'WARNING'}, "Some Materials could not be added")
823 return {'FINISHED'}
825 except Exception as e:
826 error_handlers(self, "object.btmeshfollow", e,
827 "Vertex Trace could not be completed")
829 return {'CANCELLED'}
832 # Add Tracer Material
833 def addtracemat(matobj):
834 try:
835 # Check if a material exists, skip if it does
836 matslots = bpy.context.object.data.materials.items()
838 if len(matslots) < 1: # Make sure there is only one material slot
840 Btrace = bpy.context.window_manager.curve_tracer
842 # Check if color blender is to be run
843 if not Btrace.mat_run_color_blender:
844 # Create Random color for each item
845 if Btrace.trace_mat_random:
846 # Use random color from chosen palette,
847 # assign color lists for each palette
848 brightColors = [
849 Btrace.brightColor1, Btrace.brightColor2,
850 Btrace.brightColor3, Btrace.brightColor4
852 bwColors = [
853 Btrace.bwColor1, Btrace.bwColor2
855 customColors = [
856 Btrace.mmColor1, Btrace.mmColor2, Btrace.mmColor3,
857 Btrace.mmColor4, Btrace.mmColor5, Btrace.mmColor6,
858 Btrace.mmColor7, Btrace.mmColor8
860 earthColors = [
861 Btrace.earthColor1, Btrace.earthColor2,
862 Btrace.earthColor3, Btrace.earthColor4,
863 Btrace.earthColor5
865 greenblueColors = [
866 Btrace.greenblueColor1, Btrace.greenblueColor2,
867 Btrace.greenblueColor3
869 if Btrace.mmColors == 'BRIGHT':
870 mat_color = brightColors[
871 rand_randint(0, len(brightColors) - 1)
873 if Btrace.mmColors == 'BW':
874 mat_color = bwColors[
875 rand_randint(0, len(bwColors) - 1)
877 if Btrace.mmColors == 'CUSTOM':
878 mat_color = customColors[
879 rand_randint(0, len(customColors) - 1)
881 if Btrace.mmColors == 'EARTH':
882 mat_color = earthColors[
883 rand_randint(0, len(earthColors) - 1)
885 if Btrace.mmColors == 'GREENBLUE':
886 mat_color = greenblueColors[
887 rand_randint(0, len(greenblueColors) - 1)
889 if Btrace.mmColors == 'RANDOM':
890 mat_color = (rand_random(), rand_random(), rand_random())
891 else:
892 # Choose Single color
893 mat_color = Btrace.trace_mat_color
895 TraceMat = bpy.data.materials.new('TraceMat')
897 TraceMat.use_nodes = True
898 BSDF = TraceMat.node_tree.nodes[1]
899 r, g, b = mat_color[0], mat_color[1], mat_color[2]
900 BSDF.inputs[0].default_value = [r, g, b, 1] # change node color
901 TraceMat.diffuse_color = [r, g, b, 1] # change viewport color
904 # Add material to object
905 matobj.materials.append(bpy.data.materials.get(TraceMat.name))
907 else:
908 # Run color blender operator
909 bpy.ops.object.colorblender()
911 return True
913 except Exception as e:
914 error_handlers(False, "addtracemat", e, "Function error", True)
916 return False
919 # Add Color Blender Material
920 # This is the magical material changer!
921 class OBJECT_OT_materialChango(Operator):
922 bl_idname = "object.colorblender"
923 bl_label = "Color Blender"
924 bl_options = {'REGISTER', 'UNDO'}
926 def execute(self, context):
927 try:
928 # properties panel
929 Btrace = context.window_manager.curve_tracer
930 colorObjects = context.selected_objects
932 # Set color lists
933 brightColors = [
934 Btrace.brightColor1, Btrace.brightColor2,
935 Btrace.brightColor3, Btrace.brightColor4
937 bwColors = [Btrace.bwColor1, Btrace.bwColor2]
938 customColors = [
939 Btrace.mmColor1, Btrace.mmColor2, Btrace.mmColor3,
940 Btrace.mmColor4, Btrace.mmColor5, Btrace.mmColor6,
941 Btrace.mmColor7, Btrace.mmColor8
943 earthColors = [
944 Btrace.earthColor1, Btrace.earthColor2, Btrace.earthColor3,
945 Btrace.earthColor4, Btrace.earthColor5
947 greenblueColors = [
948 Btrace.greenblueColor1, Btrace.greenblueColor2,
949 Btrace.greenblueColor3
951 engine = context.scene.render.engine
953 # Go through each selected object and run the operator
954 for i in colorObjects:
955 theObj = i
956 # Check to see if object has materials
957 checkMaterials = len(theObj.data.materials)
958 if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
959 materialName = "colorblendMaterial"
960 madMat = bpy.data.materials.new(materialName)
961 madMat.use_nodes = True
962 if checkMaterials == 0:
963 theObj.data.materials.append(madMat)
964 else:
965 theObj.material_slots[0].material = madMat
966 else: # This is internal
967 if checkMaterials == 0:
968 # Add a material
969 materialName = "colorblendMaterial"
970 madMat = bpy.data.materials.new(materialName)
971 theObj.data.materials.append(madMat)
972 else:
973 pass # pass since we have what we need
974 # assign the first material of the object to "mat"
975 madMat = theObj.data.materials[0]
977 # Numbers of frames to skip between keyframes
978 skip = Btrace.mmSkip
980 # Random material function
981 def colorblenderRandom():
982 randomRGB = (rand_random(), rand_random(), rand_random(), 1)
983 if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
984 Principled_BSDF = madMat.node_tree.nodes[1]
985 mat_color = randomRGB
986 r, g, b = mat_color[0], mat_color[1], mat_color[2]
987 Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
988 madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
989 else:
990 madMat.diffuse_color = randomRGB
992 def colorblenderCustom():
993 if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
994 Principled_BSDF = madMat.node_tree.nodes[1]
995 mat_color = rand_choice(customColors)
996 r, g, b = mat_color[0], mat_color[1], mat_color[2]
997 Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
998 madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
999 else:
1000 madMat.diffuse_color = rand_choice(customColors)
1002 # Black and white color
1003 def colorblenderBW():
1004 if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
1005 Principled_BSDF = madMat.node_tree.nodes[1]
1006 mat_color = rand_choice(bwColors)
1007 r, g, b = mat_color[0], mat_color[1], mat_color[2]
1008 Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
1009 madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
1010 else:
1011 madMat.diffuse_color = rand_choice(bwColors)
1013 # Bright colors
1014 def colorblenderBright():
1015 if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
1016 Principled_BSDF = madMat.node_tree.nodes[1]
1017 mat_color = rand_choice(brightColors)
1018 r, g, b = mat_color[0], mat_color[1], mat_color[2]
1019 Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
1020 madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
1021 else:
1022 madMat.diffuse_color = rand_choice(brightColors)
1024 # Earth Tones
1025 def colorblenderEarth():
1026 if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
1027 Principled_BSDF = madMat.node_tree.nodes[1]
1028 mat_color = rand_choice(earthColors)
1029 r, g, b = mat_color[0], mat_color[1], mat_color[2]
1030 Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
1031 madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
1032 else:
1033 madMat.diffuse_color = rand_choice(earthColors)
1035 # Green to Blue Tones
1036 def colorblenderGreenBlue():
1037 if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
1038 Principled_BSDF = madMat.node_tree.nodes[1]
1039 mat_color = rand_choice(greenblueColors)
1040 r, g, b = mat_color[0], mat_color[1], mat_color[2]
1041 Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
1042 madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
1043 else:
1044 madMat.diffuse_color = rand_choice(greenblueColors)
1046 # define frame start/end variables
1047 scn = context.scene
1048 start = scn.frame_start
1049 end = scn.frame_end
1051 # Go to each frame in iteration and add material
1052 while start <= (end + (skip - 1)):
1053 bpy.context.scene.frame_set(frame=start)
1055 # Check what colors setting is checked and run the appropriate function
1056 if Btrace.mmColors == 'RANDOM':
1057 colorblenderRandom()
1058 elif Btrace.mmColors == 'CUSTOM':
1059 colorblenderCustom()
1060 elif Btrace.mmColors == 'BW':
1061 colorblenderBW()
1062 elif Btrace.mmColors == 'BRIGHT':
1063 colorblenderBright()
1064 elif Btrace.mmColors == 'EARTH':
1065 colorblenderEarth()
1066 elif Btrace.mmColors == 'GREENBLUE':
1067 colorblenderGreenBlue()
1068 else:
1069 pass
1071 # Add keyframe to material
1072 if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
1073 madMat.node_tree.nodes[
1074 1].inputs[0].keyframe_insert('default_value')
1075 # not sure if this is need, it's viewport color only
1076 madMat.keyframe_insert('diffuse_color')
1077 else:
1078 madMat.keyframe_insert('diffuse_color')
1080 # Increase frame number
1081 start += skip
1083 return{'FINISHED'}
1085 except Exception as e:
1086 error_handlers(self, "object.colorblender", e,
1087 "Color Blender could not be completed")
1089 return {'CANCELLED'}
1092 # This clears the keyframes
1093 class OBJECT_OT_clearColorblender(Operator):
1094 bl_idname = "object.colorblenderclear"
1095 bl_label = "Clear colorblendness"
1096 bl_description = "Clear the color keyframes"
1097 bl_options = {'REGISTER', 'UNDO'}
1099 def invoke(self, context, event):
1100 try:
1101 colorObjects = context.selected_objects
1102 engine = context.scene.render.engine
1104 # Go through each selected object and run the operator
1105 for i in colorObjects:
1106 theObj = i
1107 # assign the first material of the object to "mat"
1108 matCl = theObj.data.materials[0]
1110 # define frame start/end variables
1111 scn = context.scene
1112 start = scn.frame_start
1113 end = scn.frame_end
1115 # Remove all keyframes from diffuse_color, super sloppy
1116 while start <= (end + 100):
1117 context.scene.frame_set(frame=start)
1118 try:
1119 if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
1120 matCl.node_tree.nodes[
1121 1].inputs[0].keyframe_delete('default_value')
1122 elif engine == 'BLENDER_RENDER':
1123 matCl.keyframe_delete('diffuse_color')
1124 except:
1125 pass
1126 start += 1
1128 return{'FINISHED'}
1130 except Exception as e:
1131 error_handlers(self, "object.colorblenderclear", e,
1132 "Reset Keyframes could not be completed")
1134 return {'CANCELLED'}
1137 # F-Curve Noise
1138 # will add noise modifiers to each selected object f-curves
1139 # change type to: 'rotation' | 'location' | 'scale' | '' to effect all
1140 # first record a keyframe for this to work (to generate the f-curves)
1142 class OBJECT_OT_fcnoise(Operator):
1143 bl_idname = "object.btfcnoise"
1144 bl_label = "Btrace: F-curve Noise"
1145 bl_options = {'REGISTER', 'UNDO'}
1147 def execute(self, context):
1148 try:
1149 Btrace = context.window_manager.curve_tracer
1150 amp = Btrace.fcnoise_amp
1151 timescale = Btrace.fcnoise_timescale
1152 addkeyframe = Btrace.fcnoise_key
1154 # This sets properties for Loc, Rot and Scale
1155 # if they're checked in the Tools window
1156 noise_rot = 'rotation'
1157 noise_loc = 'location'
1158 noise_scale = 'scale'
1159 if not Btrace.fcnoise_rot:
1160 noise_rot = 'none'
1161 if not Btrace.fcnoise_loc:
1162 noise_loc = 'none'
1163 if not Btrace.fcnoise_scale:
1164 noise_scale = 'none'
1166 # Add settings from panel for type of keyframes
1167 types = noise_loc, noise_rot, noise_scale
1168 amplitude = amp
1169 time_scale = timescale
1171 for i in context.selected_objects:
1172 # Add keyframes, this is messy and should only
1173 # add keyframes for what is checked
1174 if addkeyframe is True:
1175 bpy.ops.anim.keyframe_insert(type="LocRotScale")
1176 for obj in context.selected_objects:
1177 if obj.animation_data:
1178 for c in obj.animation_data.action.fcurves:
1179 if c.data_path.startswith(types):
1180 # clean modifiers
1181 for m in c.modifiers:
1182 c.modifiers.remove(m)
1183 # add noide modifiers
1184 n = c.modifiers.new('NOISE')
1185 n.strength = amplitude
1186 n.scale = time_scale
1187 n.phase = rand_randint(0, 999)
1189 return {'FINISHED'}
1191 except Exception as e:
1192 error_handlers(self, "object.btfcnoise", e,
1193 "F-curve Noise could not be completed")
1195 return {'CANCELLED'}
1198 # Curve Grow Animation
1199 # Animate curve radius over length of time
1201 class OBJECT_OT_curvegrow(Operator):
1202 bl_idname = "curve.btgrow"
1203 bl_label = "Run Script"
1204 bl_description = "Keyframe points radius"
1205 bl_options = {'REGISTER', 'UNDO'}
1207 @classmethod
1208 def poll(cls, context):
1209 return (context.object and context.object.type in {'CURVE'})
1211 def execute(self, context):
1212 try:
1213 # not so nice with the nested try blocks, however the inside one
1214 # is used as a switch statement
1215 Btrace = context.window_manager.curve_tracer
1216 anim_f_start, anim_length, anim_auto = Btrace.anim_f_start, \
1217 Btrace.anim_length, \
1218 Btrace.anim_auto
1219 curve_resolution, curve_depth = Btrace.curve_resolution, \
1220 Btrace.curve_depth
1221 # make the curve visible
1222 objs = context.selected_objects
1223 # Execute on multiple selected objects
1224 for i in objs:
1225 context.view_layer.objects.active = i
1226 obj = context.active_object
1227 try:
1228 obj.data.fill_mode = 'FULL'
1229 except:
1230 obj.data.dimensions = '3D'
1231 obj.data.fill_mode = 'FULL'
1232 if not obj.data.bevel_resolution:
1233 obj.data.bevel_resolution = curve_resolution
1234 if not obj.data.bevel_depth:
1235 obj.data.bevel_depth = curve_depth
1236 if anim_auto:
1237 anim_f_start = bpy.context.scene.frame_start
1238 anim_length = bpy.context.scene.frame_end
1239 # get points data and beautify
1240 actual, total = anim_f_start, 0
1241 for sp in obj.data.splines:
1242 total += len(sp.points) + len(sp.bezier_points)
1243 step = anim_length / total
1244 for sp in obj.data.splines:
1245 sp.radius_interpolation = 'BSPLINE'
1246 po = [p for p in sp.points] + [p for p in sp.bezier_points]
1247 if not Btrace.anim_keepr:
1248 for p in po:
1249 p.radius = 1
1250 if Btrace.anim_tails and not sp.use_cyclic_u:
1251 po[0].radius = po[-1].radius = 0
1252 po[1].radius = po[-2].radius = .65
1253 ra = [p.radius for p in po]
1255 # record the keyframes
1256 for i in range(len(po)):
1257 po[i].radius = 0
1258 po[i].keyframe_insert('radius', frame=actual)
1259 actual += step
1260 po[i].radius = ra[i]
1261 po[i].keyframe_insert(
1262 'radius',
1263 frame=(actual + Btrace.anim_delay)
1266 if Btrace.anim_f_fade:
1267 po[i].radius = ra[i]
1268 po[i].keyframe_insert(
1269 'radius',
1270 frame=(actual + Btrace.anim_f_fade - step)
1272 po[i].radius = 0
1273 po[i].keyframe_insert(
1274 'radius',
1275 frame=(actual + Btrace.anim_delay + Btrace.anim_f_fade)
1278 bpy.context.scene.frame_set(Btrace.anim_f_start)
1280 return{'FINISHED'}
1282 except Exception as e:
1283 error_handlers(self, "curve.btgrow", e,
1284 "Grow curve could not be completed")
1286 return {'CANCELLED'}
1289 # Remove animation and curve radius data
1290 class OBJECT_OT_reset(Operator):
1291 bl_idname = "object.btreset"
1292 bl_label = "Clear animation"
1293 bl_description = "Remove animation / curve radius data"
1294 bl_options = {'REGISTER', 'UNDO'}
1296 def execute(self, context):
1297 try:
1298 objs = context.selected_objects
1299 for i in objs: # Execute on multiple selected objects
1300 context.view_layer.objects.active = i
1301 obj = context.active_object
1302 obj.animation_data_clear()
1303 if obj.type == 'CURVE':
1304 for sp in obj.data.splines:
1305 po = [p for p in sp.points] + [p for p in sp.bezier_points]
1306 for p in po:
1307 p.radius = 1
1309 return{'FINISHED'}
1311 except Exception as e:
1312 error_handlers(self, "object.btreset", e,
1313 "Clear animation could not be completed")
1315 return {'CANCELLED'}