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