1 # SPDX-License-Identifier: GPL-2.0-or-later
4 # Add more options to curve radius/modulation plus cyclic/connect curve option
7 # import selection_utils
8 from bpy
.types
import Operator
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
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"
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
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
]
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
60 elif event
.type == 'ESC':
61 # If escape is pressed, cancel the operator
64 # Update selection if we need to
66 return {'PASS_THROUGH'}
68 def invoke(self
, context
, event
):
71 context
.window_manager
.modal_handler_add(self
)
72 return {'RUNNING_MODAL'}
74 def error_handlers(self
, op_name
, error
, reports
="ERROR", func
=False):
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
))
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'}
93 def poll(cls
, context
):
94 return (context
.object and
95 context
.object.type in {'MESH', 'FONT'})
97 def invoke(self
, context
, event
):
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
104 if Btrace
.object_duplicate
:
105 bpy
.ops
.object.duplicate_move()
106 brushObj
= context
.selected_objects
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
114 context
.view_layer
.objects
.active
= i
115 if i
and i
.type != 'CURVE':
116 bpy
.ops
.object.btconvertcurve()
118 trace_mats
= addtracemat(bpy
.context
.object.data
)
119 if not trace_mats
and check_materials
is True:
120 check_materials
= False
122 bpy
.ops
.curve
.btgrow()
124 if check_materials
is False:
125 self
.report({'WARNING'}, "Some Materials could not be added")
129 except Exception as e
:
130 error_handlers(self
, "object.btobjecttrace", e
,
131 "Object Trace could not be completed")
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'}
148 def poll(cls
, context
):
149 return len(context
.selected_objects
) > 1
151 def invoke(self
, context
, event
):
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
)
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
170 for ob
in selobnames
:
171 obnames
.append(bpy
.data
.objects
[ob
])
173 obnames
= bpy
.context
.selected_objects
# No selection order
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
)
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')
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
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
230 bpy
.ops
.curve
.btgrow() # Add grow curve
232 if check_materials
is False:
233 self
.report({'WARNING'}, "Some Materials could not be added")
237 except Exception as e
:
238 error_handlers(self
, "object.btobjectsconnect", e
,
239 "Objects Connect could not be completed")
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
)
254 tracer
.fill_mode
= 'FULL'
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
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'}
271 def poll(cls
, context
):
272 return (context
.object is not None and
273 context
.object.particle_systems
)
275 def execute(self
, context
):
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
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
]
313 p
.handle_right_type
= curve_handle
314 p
.handle_left_type
= curve_handle
315 particlecurve
= tracer
[1]
316 curvelist
.append(particlecurve
)
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
)
325 trace_mats
= addtracemat(curveobject
.data
)
326 if not trace_mats
and check_materials
is True:
327 check_materials
= False
330 bpy
.ops
.curve
.btgrow() # Add grow curve
332 if check_materials
is False:
333 self
.report({'WARNING'}, "Some Materials could not be added")
337 except Exception as e
:
338 error_handlers(self
, "particles.particletrace", e
,
339 "Particle Trace could not be completed")
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'}
355 def poll(cls
, context
):
356 return (context
.object is not None and
357 context
.object.particle_systems
)
359 def execute(self
, context
):
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")
372 Btrace
= bpy
.context
.window_manager
.curve_tracer
374 particle_f_start
= Btrace
.particle_f_start
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
)
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':
416 bp
= spline
.bezier_points
[bFrames
]
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')
425 bpy
.ops
.object.select_all(action
='DESELECT')
426 curve
.select_set(True)
427 bpy
.context
.view_layer
.objects
.active
= curve
430 trace_mats
= addtracemat(curve
.data
)
432 self
.report({'WARNING'}, "Some Materials could not be added")
435 bpy
.ops
.curve
.btgrow()
439 except Exception as e
:
440 error_handlers(self
, "particles.connect", e
,
441 "Connect Particles could not be completed")
447 # Writes a curve by animating its point's radii
449 class OBJECT_OT_writing(Operator
):
450 bl_idname
= "curve.btwriting"
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'}
457 def poll(cls
, context
):
458 gp
= context
.scene
.grease_pencil
459 return gp
and gp
.layers
461 def execute(self
, context
):
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")
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 ?")
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":
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
505 context
.view_layer
.objects
.active
= i
506 bpy
.ops
.curve
.btgrow()
508 trace_mats
= addtracemat(bpy
.context
.object.data
)
509 if not trace_mats
and check_materials
is True:
510 check_materials
= False
513 context
.view_layer
.objects
.active
= i
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
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")
533 except Exception as e
:
534 error_handlers(self
, "curve.btwriting", e
,
535 "Grease Pencil conversion could not be completed")
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'}
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
):
559 Btrace
= context
.window_manager
.curve_tracer
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':
571 if Btrace
.distort_curve
:
572 for v
in obj
.data
.vertices
:
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')
585 p1
= rand_randint(0, len(verts
) - 1)
590 for z
in range(len(li
)):
593 d
= verts
[p1
].co
- verts
[px
].co
# find distance from first vert
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.
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':
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
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
632 bpy
.ops
.object.shade_smooth()
633 # Modulate curve radius and add distortion
634 if Btrace
.distort_curve
:
635 scale
= Btrace
.distort_modscale
638 for u
in obj
.data
.splines
:
639 for v
in u
.bezier_points
:
640 v
.radius
= scale
* round(rand_random(), 3)
644 except Exception as e
:
645 error_handlers(self
, "object.btconvertcurve", e
,
646 "Conversion could not be completed")
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'}
662 def poll(cls
, context
):
663 return (context
.object and context
.object.type in {'MESH'})
665 def execute(self
, context
):
667 Btrace
= context
.window_manager
.curve_tracer
668 stepsize
= Btrace
.particle_step
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
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
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':
694 if seloption
== 'CUSTOM':
696 if i
.select_get() is True:
698 if seloption
== 'RANDOM':
699 for i
in list(meshobjs
):
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
):
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':
720 g_co
= obj
.matrix_local
@ followed_item
.co
722 if drawmethod
== 'FACES':
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()
738 frame_x
= frame_x
+ stepsize
740 scn
.frame_set(start_frame
)
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
)
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
]
769 p
.handle_right_type
= curve_handle
770 p
.handle_left_type
= curve_handle
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
))
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")
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")
825 except Exception as e
:
826 error_handlers(self
, "object.btmeshfollow", e
,
827 "Vertex Trace could not be completed")
832 # Add Tracer Material
833 def addtracemat(matobj
):
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
849 Btrace
.brightColor1
, Btrace
.brightColor2
,
850 Btrace
.brightColor3
, Btrace
.brightColor4
853 Btrace
.bwColor1
, Btrace
.bwColor2
856 Btrace
.mmColor1
, Btrace
.mmColor2
, Btrace
.mmColor3
,
857 Btrace
.mmColor4
, Btrace
.mmColor5
, Btrace
.mmColor6
,
858 Btrace
.mmColor7
, Btrace
.mmColor8
861 Btrace
.earthColor1
, Btrace
.earthColor2
,
862 Btrace
.earthColor3
, Btrace
.earthColor4
,
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())
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
))
908 # Run color blender operator
909 bpy
.ops
.object.colorblender()
913 except Exception as e
:
914 error_handlers(False, "addtracemat", e
, "Function error", True)
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
):
929 Btrace
= context
.window_manager
.curve_tracer
930 colorObjects
= context
.selected_objects
934 Btrace
.brightColor1
, Btrace
.brightColor2
,
935 Btrace
.brightColor3
, Btrace
.brightColor4
937 bwColors
= [Btrace
.bwColor1
, Btrace
.bwColor2
]
939 Btrace
.mmColor1
, Btrace
.mmColor2
, Btrace
.mmColor3
,
940 Btrace
.mmColor4
, Btrace
.mmColor5
, Btrace
.mmColor6
,
941 Btrace
.mmColor7
, Btrace
.mmColor8
944 Btrace
.earthColor1
, Btrace
.earthColor2
, Btrace
.earthColor3
,
945 Btrace
.earthColor4
, Btrace
.earthColor5
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
:
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
)
965 theObj
.material_slots
[0].material
= madMat
966 else: # This is internal
967 if checkMaterials
== 0:
969 materialName
= "colorblendMaterial"
970 madMat
= bpy
.data
.materials
.new(materialName
)
971 theObj
.data
.materials
.append(madMat
)
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
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
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
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
1011 madMat
.diffuse_color
= rand_choice(bwColors
)
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
1022 madMat
.diffuse_color
= rand_choice(brightColors
)
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
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
1044 madMat
.diffuse_color
= rand_choice(greenblueColors
)
1046 # define frame start/end variables
1048 start
= scn
.frame_start
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':
1062 elif Btrace
.mmColors
== 'BRIGHT':
1063 colorblenderBright()
1064 elif Btrace
.mmColors
== 'EARTH':
1066 elif Btrace
.mmColors
== 'GREENBLUE':
1067 colorblenderGreenBlue()
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')
1078 madMat
.keyframe_insert('diffuse_color')
1080 # Increase frame number
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
):
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
:
1107 # assign the first material of the object to "mat"
1108 matCl
= theObj
.data
.materials
[0]
1110 # define frame start/end variables
1112 start
= scn
.frame_start
1115 # Remove all keyframes from diffuse_color, super sloppy
1116 while start
<= (end
+ 100):
1117 context
.scene
.frame_set(frame
=start
)
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')
1130 except Exception as e
:
1131 error_handlers(self
, "object.colorblenderclear", e
,
1132 "Reset Keyframes could not be completed")
1134 return {'CANCELLED'}
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
):
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
:
1161 if not Btrace
.fcnoise_loc
:
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
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
):
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)
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'}
1208 def poll(cls
, context
):
1209 return (context
.object and context
.object.type in {'CURVE'})
1211 def execute(self
, context
):
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
, \
1219 curve_resolution
, curve_depth
= Btrace
.curve_resolution
, \
1221 # make the curve visible
1222 objs
= context
.selected_objects
1223 # Execute on multiple selected objects
1225 context
.view_layer
.objects
.active
= i
1226 obj
= context
.active_object
1228 obj
.data
.fill_mode
= 'FULL'
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
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
:
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
)):
1258 po
[i
].keyframe_insert('radius', frame
=actual
)
1260 po
[i
].radius
= ra
[i
]
1261 po
[i
].keyframe_insert(
1263 frame
=(actual
+ Btrace
.anim_delay
)
1266 if Btrace
.anim_f_fade
:
1267 po
[i
].radius
= ra
[i
]
1268 po
[i
].keyframe_insert(
1270 frame
=(actual
+ Btrace
.anim_f_fade
- step
)
1273 po
[i
].keyframe_insert(
1275 frame
=(actual
+ Btrace
.anim_delay
+ Btrace
.anim_f_fade
)
1278 bpy
.context
.scene
.frame_set(Btrace
.anim_f_start
)
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
):
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
]
1311 except Exception as e
:
1312 error_handlers(self
, "object.btreset", e
,
1313 "Clear animation could not be completed")
1315 return {'CANCELLED'}