1 # SPDX-FileCopyrightText: 2017-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 # Add more options to curve radius/modulation plus cyclic/connect curve option
9 # import selection_utils
10 from bpy
.types
import Operator
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
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"
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
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
]
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
62 elif event
.type == 'ESC':
63 # If escape is pressed, cancel the operator
66 # Update selection if we need to
68 return {'PASS_THROUGH'}
70 def invoke(self
, context
, event
):
73 context
.window_manager
.modal_handler_add(self
)
74 return {'RUNNING_MODAL'}
76 def error_handlers(self
, op_name
, error
, reports
="ERROR", func
=False):
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
))
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'}
95 def poll(cls
, context
):
96 return (context
.object and
97 context
.object.type in {'MESH', 'FONT'})
99 def invoke(self
, context
, event
):
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
106 if Btrace
.object_duplicate
:
107 bpy
.ops
.object.duplicate_move()
108 brushObj
= context
.selected_objects
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
116 context
.view_layer
.objects
.active
= i
117 if i
and i
.type != 'CURVE':
118 bpy
.ops
.object.btconvertcurve()
120 trace_mats
= addtracemat(bpy
.context
.object.data
)
121 if not trace_mats
and check_materials
is True:
122 check_materials
= False
124 bpy
.ops
.curve
.btgrow()
126 if check_materials
is False:
127 self
.report({'WARNING'}, "Some Materials could not be added")
131 except Exception as e
:
132 error_handlers(self
, "object.btobjecttrace", e
,
133 "Object Trace could not be completed")
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'}
150 def poll(cls
, context
):
151 return len(context
.selected_objects
) > 1
153 def invoke(self
, context
, event
):
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
)
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
172 for ob
in selobnames
:
173 obnames
.append(bpy
.data
.objects
[ob
])
175 obnames
= bpy
.context
.selected_objects
# No selection order
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
)
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')
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
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
232 bpy
.ops
.curve
.btgrow() # Add grow curve
234 if check_materials
is False:
235 self
.report({'WARNING'}, "Some Materials could not be added")
239 except Exception as e
:
240 error_handlers(self
, "object.btobjectsconnect", e
,
241 "Objects Connect could not be completed")
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
)
256 tracer
.fill_mode
= 'FULL'
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
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'}
273 def poll(cls
, context
):
274 return (context
.object is not None and
275 context
.object.particle_systems
)
277 def execute(self
, context
):
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
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
]
315 p
.handle_right_type
= curve_handle
316 p
.handle_left_type
= curve_handle
317 particlecurve
= tracer
[1]
318 curvelist
.append(particlecurve
)
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
)
327 trace_mats
= addtracemat(curveobject
.data
)
328 if not trace_mats
and check_materials
is True:
329 check_materials
= False
332 bpy
.ops
.curve
.btgrow() # Add grow curve
334 if check_materials
is False:
335 self
.report({'WARNING'}, "Some Materials could not be added")
339 except Exception as e
:
340 error_handlers(self
, "particles.particletrace", e
,
341 "Particle Trace could not be completed")
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'}
357 def poll(cls
, context
):
358 return (context
.object is not None and
359 context
.object.particle_systems
)
361 def execute(self
, context
):
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")
374 Btrace
= bpy
.context
.window_manager
.curve_tracer
376 particle_f_start
= Btrace
.particle_f_start
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
)
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':
418 bp
= spline
.bezier_points
[bFrames
]
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')
427 bpy
.ops
.object.select_all(action
='DESELECT')
428 curve
.select_set(True)
429 bpy
.context
.view_layer
.objects
.active
= curve
432 trace_mats
= addtracemat(curve
.data
)
434 self
.report({'WARNING'}, "Some Materials could not be added")
437 bpy
.ops
.curve
.btgrow()
441 except Exception as e
:
442 error_handlers(self
, "particles.connect", e
,
443 "Connect Particles could not be completed")
449 # Writes a curve by animating its point's radii
451 class OBJECT_OT_writing(Operator
):
452 bl_idname
= "curve.btwriting"
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'}
459 def poll(cls
, context
):
460 gp
= context
.scene
.grease_pencil
461 return gp
and gp
.layers
463 def execute(self
, context
):
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")
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 ?")
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":
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
507 context
.view_layer
.objects
.active
= i
508 bpy
.ops
.curve
.btgrow()
510 trace_mats
= addtracemat(bpy
.context
.object.data
)
511 if not trace_mats
and check_materials
is True:
512 check_materials
= False
515 context
.view_layer
.objects
.active
= i
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
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")
535 except Exception as e
:
536 error_handlers(self
, "curve.btwriting", e
,
537 "Grease Pencil conversion could not be completed")
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'}
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
):
561 Btrace
= context
.window_manager
.curve_tracer
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':
573 if Btrace
.distort_curve
:
574 for v
in obj
.data
.vertices
:
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')
587 p1
= rand_randint(0, len(verts
) - 1)
592 for z
in range(len(li
)):
595 d
= verts
[p1
].co
- verts
[px
].co
# find distance from first vert
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.
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':
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
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
634 bpy
.ops
.object.shade_smooth()
635 # Modulate curve radius and add distortion
636 if Btrace
.distort_curve
:
637 scale
= Btrace
.distort_modscale
640 for u
in obj
.data
.splines
:
641 for v
in u
.bezier_points
:
642 v
.radius
= scale
* round(rand_random(), 3)
646 except Exception as e
:
647 error_handlers(self
, "object.btconvertcurve", e
,
648 "Conversion could not be completed")
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'}
664 def poll(cls
, context
):
665 return (context
.object and context
.object.type in {'MESH'})
667 def execute(self
, context
):
669 Btrace
= context
.window_manager
.curve_tracer
670 stepsize
= Btrace
.particle_step
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
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
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':
696 if seloption
== 'CUSTOM':
698 if i
.select_get() is True:
700 if seloption
== 'RANDOM':
701 for i
in list(meshobjs
):
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
):
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':
722 g_co
= obj
.matrix_local
@ followed_item
.co
724 if drawmethod
== 'FACES':
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()
740 frame_x
= frame_x
+ stepsize
742 scn
.frame_set(start_frame
)
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
)
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
]
771 p
.handle_right_type
= curve_handle
772 p
.handle_left_type
= curve_handle
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
))
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")
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")
827 except Exception as e
:
828 error_handlers(self
, "object.btmeshfollow", e
,
829 "Vertex Trace could not be completed")
834 # Add Tracer Material
835 def addtracemat(matobj
):
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
851 Btrace
.brightColor1
, Btrace
.brightColor2
,
852 Btrace
.brightColor3
, Btrace
.brightColor4
855 Btrace
.bwColor1
, Btrace
.bwColor2
858 Btrace
.mmColor1
, Btrace
.mmColor2
, Btrace
.mmColor3
,
859 Btrace
.mmColor4
, Btrace
.mmColor5
, Btrace
.mmColor6
,
860 Btrace
.mmColor7
, Btrace
.mmColor8
863 Btrace
.earthColor1
, Btrace
.earthColor2
,
864 Btrace
.earthColor3
, Btrace
.earthColor4
,
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())
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
))
910 # Run color blender operator
911 bpy
.ops
.object.colorblender()
915 except Exception as e
:
916 error_handlers(False, "addtracemat", e
, "Function error", True)
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
):
931 Btrace
= context
.window_manager
.curve_tracer
932 colorObjects
= context
.selected_objects
936 Btrace
.brightColor1
, Btrace
.brightColor2
,
937 Btrace
.brightColor3
, Btrace
.brightColor4
939 bwColors
= [Btrace
.bwColor1
, Btrace
.bwColor2
]
941 Btrace
.mmColor1
, Btrace
.mmColor2
, Btrace
.mmColor3
,
942 Btrace
.mmColor4
, Btrace
.mmColor5
, Btrace
.mmColor6
,
943 Btrace
.mmColor7
, Btrace
.mmColor8
946 Btrace
.earthColor1
, Btrace
.earthColor2
, Btrace
.earthColor3
,
947 Btrace
.earthColor4
, Btrace
.earthColor5
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
:
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
)
967 theObj
.material_slots
[0].material
= madMat
968 else: # This is internal
969 if checkMaterials
== 0:
971 materialName
= "colorblendMaterial"
972 madMat
= bpy
.data
.materials
.new(materialName
)
973 theObj
.data
.materials
.append(madMat
)
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
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
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
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
1013 madMat
.diffuse_color
= rand_choice(bwColors
)
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
1024 madMat
.diffuse_color
= rand_choice(brightColors
)
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
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
1046 madMat
.diffuse_color
= rand_choice(greenblueColors
)
1048 # define frame start/end variables
1050 start
= scn
.frame_start
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':
1064 elif Btrace
.mmColors
== 'BRIGHT':
1065 colorblenderBright()
1066 elif Btrace
.mmColors
== 'EARTH':
1068 elif Btrace
.mmColors
== 'GREENBLUE':
1069 colorblenderGreenBlue()
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')
1080 madMat
.keyframe_insert('diffuse_color')
1082 # Increase frame number
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
):
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
:
1109 # assign the first material of the object to "mat"
1110 matCl
= theObj
.data
.materials
[0]
1112 # define frame start/end variables
1114 start
= scn
.frame_start
1117 # Remove all keyframes from diffuse_color, super sloppy
1118 while start
<= (end
+ 100):
1119 context
.scene
.frame_set(frame
=start
)
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')
1132 except Exception as e
:
1133 error_handlers(self
, "object.colorblenderclear", e
,
1134 "Reset Keyframes could not be completed")
1136 return {'CANCELLED'}
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
):
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
:
1163 if not Btrace
.fcnoise_loc
:
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
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
):
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)
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'}
1210 def poll(cls
, context
):
1211 return (context
.object and context
.object.type in {'CURVE'})
1213 def execute(self
, context
):
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
, \
1221 curve_resolution
, curve_depth
= Btrace
.curve_resolution
, \
1223 # make the curve visible
1224 objs
= context
.selected_objects
1225 # Execute on multiple selected objects
1227 context
.view_layer
.objects
.active
= i
1228 obj
= context
.active_object
1230 obj
.data
.fill_mode
= 'FULL'
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
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
:
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
)):
1260 po
[i
].keyframe_insert('radius', frame
=actual
)
1262 po
[i
].radius
= ra
[i
]
1263 po
[i
].keyframe_insert(
1265 frame
=(actual
+ Btrace
.anim_delay
)
1268 if Btrace
.anim_f_fade
:
1269 po
[i
].radius
= ra
[i
]
1270 po
[i
].keyframe_insert(
1272 frame
=(actual
+ Btrace
.anim_f_fade
- step
)
1275 po
[i
].keyframe_insert(
1277 frame
=(actual
+ Btrace
.anim_delay
+ Btrace
.anim_f_fade
)
1280 bpy
.context
.scene
.frame_set(Btrace
.anim_f_start
)
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
):
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
]
1313 except Exception as e
:
1314 error_handlers(self
, "object.btreset", e
,
1315 "Clear animation could not be completed")
1317 return {'CANCELLED'}