blender-addons/btrace/bTrace.py
Campbell Barton e8da6131fd License headers: use SPDX-FileCopyrightText for all addons
Move copyright text to SPDX-FileCopyrightText or set to the
Blender Foundation so "make check_licenses" now runs without warnings.
2023-06-15 16:54:05 +10:00

1318 lines
53 KiB
Python

# SPDX-FileCopyrightText: 2017-2022 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later
# TO DO LIST #
# Add more options to curve radius/modulation plus cyclic/connect curve option
import bpy
# import selection_utils
from bpy.types import Operator
from random import (
choice as rand_choice,
random as rand_random,
randint as rand_randint,
uniform as rand_uniform,
)
# Selection Module: Contributors: Mackraken, Andrew Hale (TrumanBlending)
# Adapted from Mackraken's "Tools for Curves" addon
selected = []
class SelectionOrder(bpy.types.Operator):
"""Store the object names in the order they are selected, """ \
"""use RETURN key to confirm selection, ESCAPE key to cancel"""
bl_idname = "object.select_order"
bl_label = "Select with Order"
bl_options = {'UNDO'}
num_selected = 0
@classmethod
def poll(self, context):
return bpy.context.mode == 'OBJECT'
def update(self, context):
# Get the currently selected objects
sel = context.selected_objects
num = len(sel)
if num == 0:
# Reset the list
del selected[:]
elif num > self.num_selected:
# Get all the newly selected objects and add
new = [ob.name for ob in sel if ob.name not in selected]
selected.extend(new)
elif num < self.num_selected:
# Get the selected objects and remove from list
curnames = {ob.name for ob in sel}
selected[:] = [name for name in selected if name in curnames]
# Set the number of currently select objects
self.num_selected = len(selected)
def modal(self, context, event):
if event.type == 'RET':
# If return is pressed, finish the operator
return {'FINISHED'}
elif event.type == 'ESC':
# If escape is pressed, cancel the operator
return {'CANCELLED'}
# Update selection if we need to
self.update(context)
return {'PASS_THROUGH'}
def invoke(self, context, event):
self.update(context)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def error_handlers(self, op_name, error, reports="ERROR", func=False):
if self and reports:
self.report({'WARNING'}, reports + " (See Console for more info)")
is_func = "Function" if func else "Operator"
print("\n[Btrace]\n{}: {}\nError: {}\n".format(op_name, is_func, error))
# Object Trace
# creates a curve with a modulated radius connecting points of a mesh
class OBJECT_OT_objecttrace(Operator):
bl_idname = "object.btobjecttrace"
bl_label = "Btrace: Object Trace"
bl_description = ("Trace selected mesh object with a curve with the option to animate\n"
"The Active Object has to be of a Mesh or Font type")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object and
context.object.type in {'MESH', 'FONT'})
def invoke(self, context, event):
try:
# Run through each selected object and convert to to a curved object
brushObj = context.selected_objects
Btrace = context.window_manager.curve_tracer
check_materials = True
# Duplicate Mesh
if Btrace.object_duplicate:
bpy.ops.object.duplicate_move()
brushObj = context.selected_objects
# Join Mesh
if Btrace.convert_joinbefore:
if len(brushObj) > 1: # Only run if multiple objects selected
bpy.ops.object.join()
brushObj = context.selected_objects
for i in brushObj:
context.view_layer.objects.active = i
if i and i.type != 'CURVE':
bpy.ops.object.btconvertcurve()
# Materials
trace_mats = addtracemat(bpy.context.object.data)
if not trace_mats and check_materials is True:
check_materials = False
if Btrace.animate:
bpy.ops.curve.btgrow()
if check_materials is False:
self.report({'WARNING'}, "Some Materials could not be added")
return {'FINISHED'}
except Exception as e:
error_handlers(self, "object.btobjecttrace", e,
"Object Trace could not be completed")
return {'CANCELLED'}
# Objects Connect
# connect selected objects with a curve + hooks to each node
# possible handle types: 'FREE' 'AUTO' 'VECTOR' 'ALIGNED'
class OBJECT_OT_objectconnect(Operator):
bl_idname = "object.btobjectsconnect"
bl_label = "Btrace: Objects Connect"
bl_description = ("Connect selected objects with a curve and add hooks to each node\n"
"Needs at least two objects selected")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return len(context.selected_objects) > 1
def invoke(self, context, event):
try:
lists = []
Btrace = context.window_manager.curve_tracer
curve_handle = Btrace.curve_handle
if curve_handle == 'AUTOMATIC': # hackish because of naming conflict in api
curve_handle = 'AUTO'
# Check if Btrace group exists, if not create
bcollection = bpy.data.collections.keys()
if 'Btrace' not in bcollection:
mycol=bpy.data.collections.new(name="Btrace")
bpy.context.scene.collection.children.link(mycol)
# check if noise
if Btrace.connect_noise:
bpy.ops.object.btfcnoise()
# check if respect order is checked, create list of objects
if Btrace.respect_order is True:
selobnames = selection_utils.selected
obnames = []
for ob in selobnames:
obnames.append(bpy.data.objects[ob])
else:
obnames = bpy.context.selected_objects # No selection order
for a in obnames:
lists.append(a)
a.select_set(False)
# trace the origins
tracer = bpy.data.curves.new('tracer', 'CURVE')
tracer.dimensions = '3D'
spline = tracer.splines.new('BEZIER')
spline.bezier_points.add(len(lists) - 1)
curve = bpy.data.objects.new('curve', tracer)
bpy.context.collection.objects.link(curve)
# render ready curve
tracer.resolution_u = Btrace.curve_u
# Set bevel resolution from Panel options
tracer.bevel_resolution = Btrace.curve_resolution
tracer.fill_mode = 'FULL'
# Set bevel depth from Panel options
tracer.bevel_depth = Btrace.curve_depth
# move nodes to objects
for i in range(len(lists)):
p = spline.bezier_points[i]
p.co = lists[i].location
p.handle_right_type = curve_handle
p.handle_left_type = curve_handle
bpy.context.view_layer.objects.active = curve
bpy.ops.object.mode_set(mode='OBJECT')
# place hooks
for i in range(len(lists)):
lists[i].select_set(True)
curve.data.splines[0].bezier_points[i].select_control_point = True
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.hook_add_selob()
bpy.ops.object.mode_set(mode='OBJECT')
curve.data.splines[0].bezier_points[i].select_control_point = False
lists[i].select_set(False)
bpy.ops.object.select_all(action='DESELECT')
curve.select_set(True) # selected curve after it's created
# Materials
check_materials = True
trace_mats = addtracemat(bpy.context.object.data)
if not trace_mats and check_materials is True:
check_materials = False
if Btrace.animate: # Add Curve Grow it?
bpy.ops.curve.btgrow()
bpy.data.collections["Btrace"].objects.link(curve) # add to Btrace collection
# Check if we add grow curve
if Btrace.animate:
bpy.ops.curve.btgrow() # Add grow curve
if check_materials is False:
self.report({'WARNING'}, "Some Materials could not be added")
return {'FINISHED'}
except Exception as e:
error_handlers(self, "object.btobjectsconnect", e,
"Objects Connect could not be completed")
return {'CANCELLED'}
# Particle Trace
# creates a curve from each particle of a system
def curvetracer(curvename, splinename):
Btrace = bpy.context.window_manager.curve_tracer
tracer = bpy.data.curves.new(splinename, 'CURVE')
tracer.dimensions = '3D'
curve = bpy.data.objects.new(curvename, tracer)
bpy.context.collection.objects.link(curve)
try:
tracer.fill_mode = 'FULL'
except:
tracer.use_fill_front = tracer.use_fill_back = False
tracer.bevel_resolution = Btrace.curve_resolution
tracer.bevel_depth = Btrace.curve_depth
tracer.resolution_u = Btrace.curve_u
return tracer, curve
# Particle Trace
class OBJECT_OT_particletrace(Operator):
bl_idname = "particles.particletrace"
bl_label = "Btrace: Particle Trace"
bl_description = ("Creates a curve from each particle of a system.\n"
"Keeping particle amount under 250 will make this run faster")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object is not None and
context.object.particle_systems)
def execute(self, context):
try:
Btrace = bpy.context.window_manager.curve_tracer
particle_step = Btrace.particle_step # step size in frames
obj = bpy.context.object
obj = bpy.context.evaluated_depsgraph_get().objects.get(obj.name, None)
ps = obj.particle_systems.active
curvelist = []
curve_handle = Btrace.curve_handle
check_materials = True
if curve_handle == 'AUTOMATIC': # hackish naming conflict
curve_handle = 'AUTO'
if curve_handle == 'FREE_ALIGN': # hackish naming conflict
curve_handle = 'FREE'
# Check if Btrace group exists, if not create
bcollection = bpy.data.collections.keys()
if 'Btrace' not in bcollection:
mycol=bpy.data.collections.new(name="Btrace")
bpy.context.scene.collection.children.link(mycol)
if Btrace.curve_join:
tracer = curvetracer('Tracer', 'Splines')
for x in ps.particles:
if not Btrace.curve_join:
tracer = curvetracer('Tracer.000', 'Spline.000')
spline = tracer[0].splines.new('BEZIER')
# add point to spline based on step size
spline.bezier_points.add(int((x.lifetime - 1) // particle_step))
for t in list(range(int(x.lifetime))):
bpy.context.scene.frame_set(int(t + x.birth_time))
if not t % particle_step:
p = spline.bezier_points[t // particle_step]
p.co = x.location
p.handle_right_type = curve_handle
p.handle_left_type = curve_handle
particlecurve = tracer[1]
curvelist.append(particlecurve)
# add to group
bpy.ops.object.select_all(action='DESELECT')
for curveobject in curvelist:
curveobject.select_set(True)
bpy.context.view_layer.objects.active = curveobject
bpy.data.collections["Btrace"].objects.link(curveobject)
# Materials
trace_mats = addtracemat(curveobject.data)
if not trace_mats and check_materials is True:
check_materials = False
if Btrace.animate:
bpy.ops.curve.btgrow() # Add grow curve
if check_materials is False:
self.report({'WARNING'}, "Some Materials could not be added")
return {'FINISHED'}
except Exception as e:
error_handlers(self, "particles.particletrace", e,
"Particle Trace could not be completed")
return {'CANCELLED'}
# Particle Connect
# connect all particles in active system with a continuous animated curve
class OBJECT_OT_traceallparticles(Operator):
bl_idname = "particles.connect"
bl_label = "Connect Particles"
bl_description = ("Create a continuous animated curve from particles in active system\n"
"Needs an Object with a particle system attached")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object is not None and
context.object.particle_systems)
def execute(self, context):
try:
obj = context.object
obj = bpy.context.evaluated_depsgraph_get().objects.get(obj.name, None)
ps = obj.particle_systems.active
setting = ps.settings
# Grids distribution not supported
if setting.distribution == 'GRID':
self.report({'INFO'},
"Grid distribution mode for particles not supported")
return{'CANCELLED'}
Btrace = bpy.context.window_manager.curve_tracer
# Get frame start
particle_f_start = Btrace.particle_f_start
# Get frame end
particle_f_end = Btrace.particle_f_end
curve_handle = Btrace.curve_handle
# hackish because of naming conflict in api
if curve_handle == 'AUTOMATIC':
curve_handle = 'AUTO'
if curve_handle == 'FREE_ALIGN':
curve_handle = 'FREE'
# define what kind of object to create
tracer = bpy.data.curves.new('Splines', 'CURVE')
# Create new object with settings listed above
curve = bpy.data.objects.new('Tracer', tracer)
# Link newly created object to the scene
# bpy.context.view_layer.objects.link(curve)
bpy.context.scene.collection.objects.link(curve)
# add a new Bezier point in the new curve
spline = tracer.splines.new('BEZIER')
spline.bezier_points.add(setting.count - 1)
tracer.dimensions = '3D'
tracer.resolution_u = Btrace.curve_u
tracer.bevel_resolution = Btrace.curve_resolution
tracer.fill_mode = 'FULL'
tracer.bevel_depth = Btrace.curve_depth
if Btrace.particle_auto:
f_start = int(setting.frame_start)
f_end = int(setting.frame_end + setting.lifetime)
else:
if particle_f_end <= particle_f_start:
particle_f_end = particle_f_start + 1
f_start = particle_f_start
f_end = particle_f_end
for bFrames in range(f_start, f_end):
bpy.context.scene.frame_set(bFrames)
if not (bFrames - f_start) % Btrace.particle_step:
for bFrames in range(setting.count):
if ps.particles[bFrames].alive_state != 'UNBORN':
e = bFrames
bp = spline.bezier_points[bFrames]
pt = ps.particles[e]
bp.co = pt.location
bp.handle_right_type = curve_handle
bp.handle_left_type = curve_handle
bp.keyframe_insert('co')
bp.keyframe_insert('handle_left')
bp.keyframe_insert('handle_right')
# Select new curve
bpy.ops.object.select_all(action='DESELECT')
curve.select_set(True)
bpy.context.view_layer.objects.active = curve
# Materials
trace_mats = addtracemat(curve.data)
if not trace_mats:
self.report({'WARNING'}, "Some Materials could not be added")
if Btrace.animate:
bpy.ops.curve.btgrow()
return{'FINISHED'}
except Exception as e:
error_handlers(self, "particles.connect", e,
"Connect Particles could not be completed")
return {'CANCELLED'}
# Writing Tool
# Writes a curve by animating its point's radii
class OBJECT_OT_writing(Operator):
bl_idname = "curve.btwriting"
bl_label = "Write"
bl_description = ("Use Grease Pencil to write and convert to curves\n"
"Needs an existing Grease Pencil layer attached to the Scene")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
gp = context.scene.grease_pencil
return gp and gp.layers
def execute(self, context):
try:
# first check if the Grease Pencil is attached to the Scene
tool_settings = context.scene.tool_settings
source_data = tool_settings.grease_pencil_source
if source_data in {"OBJECT"}:
self.report({'WARNING'},
"Operation Cancelled. "
"The Grease Pencil data-block is attached to an Object")
return {"CANCELLED"}
Btrace = context.window_manager.curve_tracer
# this is hacky - store objects in the scene for comparison later
store_objects = [ob for ob in context.scene.objects]
gactive = context.active_object
# checking if there are any strokes the easy way
if not bpy.ops.gpencil.convert.poll():
self.report({'WARNING'},
"Operation Cancelled. "
"Are there any Grease Pencil Strokes ?")
return {'CANCELLED'}
bpy.ops.gpencil.convert(type='CURVE')
# get curve after convert (compare the scenes to get the difference)
scene_obj = context.scene.objects
check_materials = True
for obj in scene_obj:
if obj not in store_objects and obj.type == "CURVE":
gactiveCurve = obj
break
# render ready curve
gactiveCurve.data.resolution_u = Btrace.curve_u
# Set bevel resolution from Panel options
gactiveCurve.data.bevel_resolution = Btrace.curve_resolution
gactiveCurve.data.fill_mode = 'FULL'
# Set bevel depth from Panel options
gactiveCurve.data.bevel_depth = Btrace.curve_depth
writeObj = context.selected_objects
if Btrace.animate:
for i in writeObj:
context.view_layer.objects.active = i
bpy.ops.curve.btgrow()
# Materials
trace_mats = addtracemat(bpy.context.object.data)
if not trace_mats and check_materials is True:
check_materials = False
else:
for i in writeObj:
context.view_layer.objects.active = i
# Materials
trace_mats = addtracemat(bpy.context.object.data)
if not trace_mats and check_materials is True:
check_materials = False
# Delete grease pencil strokes
context.view_layer.objects.active = gactive
bpy.ops.gpencil.data_unlink()
context.view_layer.objects.active = gactiveCurve
# Smooth object
bpy.ops.object.shade_smooth()
# Return to first frame
bpy.context.scene.frame_set(Btrace.anim_f_start)
if check_materials is False:
self.report({'WARNING'}, "Some Materials could not be added")
return{'FINISHED'}
except Exception as e:
error_handlers(self, "curve.btwriting", e,
"Grease Pencil conversion could not be completed")
return {'CANCELLED'}
# Create Curve
# Convert mesh to curve using either Continuous, All Edges, or Sharp Edges
# Option to create noise
class OBJECT_OT_convertcurve(Operator):
bl_idname = "object.btconvertcurve"
bl_label = "Btrace: Create Curve"
bl_description = ("Convert Mesh to Curve using either Continuous, "
"All Edges or Sharp Edges\n"
"Active Object has to be of a Mesh or Font type")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object is not None and
context.object.type in {"MESH", "FONT"})
def execute(self, context):
try:
Btrace = context.window_manager.curve_tracer
obj = context.object
# Convert Font
if obj.type == 'FONT':
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.convert(target='CURVE') # Convert edges to curve
bpy.context.object.data.dimensions = '3D'
# make a continuous edge through all vertices
if obj.type == 'MESH':
# Add noise to mesh
if Btrace.distort_curve:
for v in obj.data.vertices:
for u in range(3):
v.co[u] += Btrace.distort_noise * (rand_uniform(-1, 1))
if Btrace.convert_edgetype == 'CONTI':
# Start Continuous edge
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.delete(type='EDGE_FACE')
bpy.ops.mesh.select_all(action='DESELECT')
verts = bpy.context.object.data.vertices
bpy.ops.object.mode_set(mode='OBJECT')
li = []
p1 = rand_randint(0, len(verts) - 1)
for v in verts:
li.append(v.index)
li.remove(p1)
for z in range(len(li)):
x = []
for px in li:
d = verts[p1].co - verts[px].co # find distance from first vert
x.append(d.length)
p2 = li[x.index(min(x))] # find the shortest distance list index
verts[p1].select = verts[p2].select = True
bpy.ops.object.mode_set(mode='EDIT')
bpy.context.tool_settings.mesh_select_mode = [True, False, False]
bpy.ops.mesh.edge_face_add()
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
li.remove(p2) # remove item from list.
p1 = p2
# Convert edges to curve
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.convert(target='CURVE')
if Btrace.convert_edgetype == 'EDGEALL':
# Start All edges
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.delete(type='ONLY_FACE')
bpy.ops.object.mode_set()
bpy.ops.object.convert(target='CURVE')
for sp in obj.data.splines:
sp.type = Btrace.curve_spline
obj = context.object
# Set spline type to custom property in panel
bpy.ops.object.editmode_toggle()
bpy.ops.curve.spline_type_set(type=Btrace.curve_spline)
# Set handle type to custom property in panel
bpy.ops.curve.handle_type_set(type=Btrace.curve_handle)
bpy.ops.object.editmode_toggle()
obj.data.fill_mode = 'FULL'
# Set resolution to custom property in panel
obj.data.bevel_resolution = Btrace.curve_resolution
obj.data.resolution_u = Btrace.curve_u
# Set depth to custom property in panel
obj.data.bevel_depth = Btrace.curve_depth
# Smooth object
bpy.ops.object.shade_smooth()
# Modulate curve radius and add distortion
if Btrace.distort_curve:
scale = Btrace.distort_modscale
if scale == 0:
return {'FINISHED'}
for u in obj.data.splines:
for v in u.bezier_points:
v.radius = scale * round(rand_random(), 3)
return {'FINISHED'}
except Exception as e:
error_handlers(self, "object.btconvertcurve", e,
"Conversion could not be completed")
return {'CANCELLED'}
# Mesh Follow, trace vertex or faces
# Create curve at center of selection item, extruded along animation
# Needs to be an animated mesh!!!
class OBJECT_OT_meshfollow(Operator):
bl_idname = "object.btmeshfollow"
bl_label = "Btrace: Vertex Trace"
bl_description = "Trace Vertex or Face on an animated mesh"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type in {'MESH'})
def execute(self, context):
try:
Btrace = context.window_manager.curve_tracer
stepsize = Btrace.particle_step
obj = context.object
scn = context.scene
drawmethod = Btrace.fol_mesh_type # Draw from Edges, Verts, or Faces
if drawmethod == 'VERTS':
meshobjs = obj.data.vertices
if drawmethod == 'FACES':
meshobjs = obj.data.polygons # untested
if drawmethod == 'EDGES':
meshobjs = obj.data.edges # untested
# Frame properties
start_frame, end_frame = Btrace.fol_start_frame, Btrace.fol_end_frame
if start_frame > end_frame: # Make sure the math works
start_frame = end_frame - 5 # if start past end, goto (end - 5)
frames = int((end_frame - start_frame) / stepsize)
def getsel_option(): # Get selection objects
sel = []
# options are 'random', 'custom', 'all'
seloption, fol_mesh_type = Btrace.fol_sel_option, Btrace.fol_mesh_type
if fol_mesh_type == 'OBJECT':
pass
else:
if seloption == 'CUSTOM':
for i in meshobjs:
if i.select_get() is True:
sel.append(i.index)
if seloption == 'RANDOM':
for i in list(meshobjs):
sel.append(i.index)
finalsel = int(len(sel) * Btrace.fol_perc_verts)
remove = len(sel) - finalsel
for i in range(remove):
sel.pop(rand_randint(0, len(sel) - 1))
if seloption == 'ALL':
for i in list(meshobjs):
sel.append(i.index)
return sel
def get_coord(objindex):
obj_co = [] # list of vector coordinates to use
frame_x = start_frame
for i in range(frames): # create frame numbers list
scn.frame_set(frame_x)
if drawmethod != 'OBJECT':
followed_item = meshobjs[objindex]
if drawmethod == 'VERTS':
# find Vert vector
g_co = obj.matrix_local @ followed_item.co
if drawmethod == 'FACES':
# find Face vector
g_co = obj.matrix_local @ followed_item.normal
if drawmethod == 'EDGES':
v1 = followed_item.vertices[0]
v2 = followed_item.vertices[1]
co1 = bpy.context.object.data.vertices[v1]
co2 = bpy.context.object.data.vertices[v2]
localcenter = co1.co.lerp(co2.co, 0.5)
g_co = obj.matrix_world @ localcenter
if drawmethod == 'OBJECT':
g_co = objindex.location.copy()
obj_co.append(g_co)
frame_x = frame_x + stepsize
scn.frame_set(start_frame)
return obj_co
def make_curve(co_list):
Btrace = bpy.context.window_manager.curve_tracer
tracer = bpy.data.curves.new('tracer', 'CURVE')
tracer.dimensions = '3D'
spline = tracer.splines.new('BEZIER')
spline.bezier_points.add(len(co_list) - 1)
curve = bpy.data.objects.new('curve', tracer)
scn.collection.objects.link(curve)
curvelist.append(curve)
# render ready curve
tracer.resolution_u = Btrace.curve_u
# Set bevel resolution from Panel options
tracer.bevel_resolution = Btrace.curve_resolution
tracer.fill_mode = 'FULL'
# Set bevel depth from Panel options
tracer.bevel_depth = Btrace.curve_depth
curve_handle = Btrace.curve_handle
# hackish AUTOMATIC doesn't work here
if curve_handle == 'AUTOMATIC':
curve_handle = 'AUTO'
# move bezier points to objects
for i in range(len(co_list)):
p = spline.bezier_points[i]
p.co = co_list[i]
p.handle_right_type = curve_handle
p.handle_left_type = curve_handle
return curve
# Run methods
# Check if Btrace group exists, if not create
bcollection = bpy.data.collections.keys()
if 'Btrace' not in bcollection:
mycol=bpy.data.collections.new(name="Btrace")
bpy.context.scene.collection.children.link(mycol)
Btrace = bpy.context.window_manager.curve_tracer
sel = getsel_option() # Get selection
curvelist = [] # list to use for grow curve
check_materials = True
if Btrace.fol_mesh_type == 'OBJECT':
vector_list = get_coord(obj)
curvelist.append(make_curve(vector_list))
else:
for i in sel:
print(i)
vector_list = get_coord(i)
make_curve(vector_list)
# curvelist.append(make_curve(vector_list)) # append happens in function
# Select new curves and add to group
bpy.ops.object.select_all(action='DESELECT')
for curveobject in curvelist:
if curveobject.type == 'CURVE':
curveobject.select_set(True)
bpy.context.view_layer.objects.active = curveobject
bpy.data.collections["Btrace"].objects.link(curveobject) #2.8 link obj to collection
bpy.context.scene.collection.objects.unlink(curveobject) # unlink from scene collection
# bpy.ops.object.group_link(group="Btrace")
# Materials
trace_mats = addtracemat(curveobject.data)
if not trace_mats and check_materials is True:
check_materials = False
curveobject.select_set(False)
if Btrace.animate: # Add grow curve
for curveobject in curvelist:
curveobject.select_set(True)
bpy.ops.curve.btgrow()
for curveobject in curvelist:
curveobject.select_set(False)
obj.select_set(False) # Deselect original object
if check_materials is False:
self.report({'WARNING'}, "Some Materials could not be added")
return {'FINISHED'}
except Exception as e:
error_handlers(self, "object.btmeshfollow", e,
"Vertex Trace could not be completed")
return {'CANCELLED'}
# Add Tracer Material
def addtracemat(matobj):
try:
# Check if a material exists, skip if it does
matslots = bpy.context.object.data.materials.items()
if len(matslots) < 1: # Make sure there is only one material slot
Btrace = bpy.context.window_manager.curve_tracer
# Check if color blender is to be run
if not Btrace.mat_run_color_blender:
# Create Random color for each item
if Btrace.trace_mat_random:
# Use random color from chosen palette,
# assign color lists for each palette
brightColors = [
Btrace.brightColor1, Btrace.brightColor2,
Btrace.brightColor3, Btrace.brightColor4
]
bwColors = [
Btrace.bwColor1, Btrace.bwColor2
]
customColors = [
Btrace.mmColor1, Btrace.mmColor2, Btrace.mmColor3,
Btrace.mmColor4, Btrace.mmColor5, Btrace.mmColor6,
Btrace.mmColor7, Btrace.mmColor8
]
earthColors = [
Btrace.earthColor1, Btrace.earthColor2,
Btrace.earthColor3, Btrace.earthColor4,
Btrace.earthColor5
]
greenblueColors = [
Btrace.greenblueColor1, Btrace.greenblueColor2,
Btrace.greenblueColor3
]
if Btrace.mmColors == 'BRIGHT':
mat_color = brightColors[
rand_randint(0, len(brightColors) - 1)
]
if Btrace.mmColors == 'BW':
mat_color = bwColors[
rand_randint(0, len(bwColors) - 1)
]
if Btrace.mmColors == 'CUSTOM':
mat_color = customColors[
rand_randint(0, len(customColors) - 1)
]
if Btrace.mmColors == 'EARTH':
mat_color = earthColors[
rand_randint(0, len(earthColors) - 1)
]
if Btrace.mmColors == 'GREENBLUE':
mat_color = greenblueColors[
rand_randint(0, len(greenblueColors) - 1)
]
if Btrace.mmColors == 'RANDOM':
mat_color = (rand_random(), rand_random(), rand_random())
else:
# Choose Single color
mat_color = Btrace.trace_mat_color
TraceMat = bpy.data.materials.new('TraceMat')
TraceMat.use_nodes = True
BSDF = TraceMat.node_tree.nodes[1]
r, g, b = mat_color[0], mat_color[1], mat_color[2]
BSDF.inputs[0].default_value = [r, g, b, 1] # change node color
TraceMat.diffuse_color = [r, g, b, 1] # change viewport color
# Add material to object
matobj.materials.append(bpy.data.materials.get(TraceMat.name))
else:
# Run color blender operator
bpy.ops.object.colorblender()
return True
except Exception as e:
error_handlers(False, "addtracemat", e, "Function error", True)
return False
# Add Color Blender Material
# This is the magical material changer!
class OBJECT_OT_materialChango(Operator):
bl_idname = "object.colorblender"
bl_label = "Color Blender"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
try:
# properties panel
Btrace = context.window_manager.curve_tracer
colorObjects = context.selected_objects
# Set color lists
brightColors = [
Btrace.brightColor1, Btrace.brightColor2,
Btrace.brightColor3, Btrace.brightColor4
]
bwColors = [Btrace.bwColor1, Btrace.bwColor2]
customColors = [
Btrace.mmColor1, Btrace.mmColor2, Btrace.mmColor3,
Btrace.mmColor4, Btrace.mmColor5, Btrace.mmColor6,
Btrace.mmColor7, Btrace.mmColor8
]
earthColors = [
Btrace.earthColor1, Btrace.earthColor2, Btrace.earthColor3,
Btrace.earthColor4, Btrace.earthColor5
]
greenblueColors = [
Btrace.greenblueColor1, Btrace.greenblueColor2,
Btrace.greenblueColor3
]
engine = context.scene.render.engine
# Go through each selected object and run the operator
for i in colorObjects:
theObj = i
# Check to see if object has materials
checkMaterials = len(theObj.data.materials)
if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
materialName = "colorblendMaterial"
madMat = bpy.data.materials.new(materialName)
madMat.use_nodes = True
if checkMaterials == 0:
theObj.data.materials.append(madMat)
else:
theObj.material_slots[0].material = madMat
else: # This is internal
if checkMaterials == 0:
# Add a material
materialName = "colorblendMaterial"
madMat = bpy.data.materials.new(materialName)
theObj.data.materials.append(madMat)
else:
pass # pass since we have what we need
# assign the first material of the object to "mat"
madMat = theObj.data.materials[0]
# Numbers of frames to skip between keyframes
skip = Btrace.mmSkip
# Random material function
def colorblenderRandom():
randomRGB = (rand_random(), rand_random(), rand_random(), 1)
if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
Principled_BSDF = madMat.node_tree.nodes[1]
mat_color = randomRGB
r, g, b = mat_color[0], mat_color[1], mat_color[2]
Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
else:
madMat.diffuse_color = randomRGB
def colorblenderCustom():
if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
Principled_BSDF = madMat.node_tree.nodes[1]
mat_color = rand_choice(customColors)
r, g, b = mat_color[0], mat_color[1], mat_color[2]
Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
else:
madMat.diffuse_color = rand_choice(customColors)
# Black and white color
def colorblenderBW():
if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
Principled_BSDF = madMat.node_tree.nodes[1]
mat_color = rand_choice(bwColors)
r, g, b = mat_color[0], mat_color[1], mat_color[2]
Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
else:
madMat.diffuse_color = rand_choice(bwColors)
# Bright colors
def colorblenderBright():
if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
Principled_BSDF = madMat.node_tree.nodes[1]
mat_color = rand_choice(brightColors)
r, g, b = mat_color[0], mat_color[1], mat_color[2]
Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
else:
madMat.diffuse_color = rand_choice(brightColors)
# Earth Tones
def colorblenderEarth():
if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
Principled_BSDF = madMat.node_tree.nodes[1]
mat_color = rand_choice(earthColors)
r, g, b = mat_color[0], mat_color[1], mat_color[2]
Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
else:
madMat.diffuse_color = rand_choice(earthColors)
# Green to Blue Tones
def colorblenderGreenBlue():
if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
Principled_BSDF = madMat.node_tree.nodes[1]
mat_color = rand_choice(greenblueColors)
r, g, b = mat_color[0], mat_color[1], mat_color[2]
Principled_BSDF.inputs[0].default_value = [r, g, b, 1]
madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1
else:
madMat.diffuse_color = rand_choice(greenblueColors)
# define frame start/end variables
scn = context.scene
start = scn.frame_start
end = scn.frame_end
# Go to each frame in iteration and add material
while start <= (end + (skip - 1)):
bpy.context.scene.frame_set(frame=start)
# Check what colors setting is checked and run the appropriate function
if Btrace.mmColors == 'RANDOM':
colorblenderRandom()
elif Btrace.mmColors == 'CUSTOM':
colorblenderCustom()
elif Btrace.mmColors == 'BW':
colorblenderBW()
elif Btrace.mmColors == 'BRIGHT':
colorblenderBright()
elif Btrace.mmColors == 'EARTH':
colorblenderEarth()
elif Btrace.mmColors == 'GREENBLUE':
colorblenderGreenBlue()
else:
pass
# Add keyframe to material
if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
madMat.node_tree.nodes[
1].inputs[0].keyframe_insert('default_value')
# not sure if this is need, it's viewport color only
madMat.keyframe_insert('diffuse_color')
else:
madMat.keyframe_insert('diffuse_color')
# Increase frame number
start += skip
return{'FINISHED'}
except Exception as e:
error_handlers(self, "object.colorblender", e,
"Color Blender could not be completed")
return {'CANCELLED'}
# This clears the keyframes
class OBJECT_OT_clearColorblender(Operator):
bl_idname = "object.colorblenderclear"
bl_label = "Clear colorblendness"
bl_description = "Clear the color keyframes"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
try:
colorObjects = context.selected_objects
engine = context.scene.render.engine
# Go through each selected object and run the operator
for i in colorObjects:
theObj = i
# assign the first material of the object to "mat"
matCl = theObj.data.materials[0]
# define frame start/end variables
scn = context.scene
start = scn.frame_start
end = scn.frame_end
# Remove all keyframes from diffuse_color, super sloppy
while start <= (end + 100):
context.scene.frame_set(frame=start)
try:
if engine == 'CYCLES' or engine == 'BLENDER_EEVEE':
matCl.node_tree.nodes[
1].inputs[0].keyframe_delete('default_value')
elif engine == 'BLENDER_RENDER':
matCl.keyframe_delete('diffuse_color')
except:
pass
start += 1
return{'FINISHED'}
except Exception as e:
error_handlers(self, "object.colorblenderclear", e,
"Reset Keyframes could not be completed")
return {'CANCELLED'}
# F-Curve Noise
# will add noise modifiers to each selected object f-curves
# change type to: 'rotation' | 'location' | 'scale' | '' to effect all
# first record a keyframe for this to work (to generate the f-curves)
class OBJECT_OT_fcnoise(Operator):
bl_idname = "object.btfcnoise"
bl_label = "Btrace: F-curve Noise"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
try:
Btrace = context.window_manager.curve_tracer
amp = Btrace.fcnoise_amp
timescale = Btrace.fcnoise_timescale
addkeyframe = Btrace.fcnoise_key
# This sets properties for Loc, Rot and Scale
# if they're checked in the Tools window
noise_rot = 'rotation'
noise_loc = 'location'
noise_scale = 'scale'
if not Btrace.fcnoise_rot:
noise_rot = 'none'
if not Btrace.fcnoise_loc:
noise_loc = 'none'
if not Btrace.fcnoise_scale:
noise_scale = 'none'
# Add settings from panel for type of keyframes
types = noise_loc, noise_rot, noise_scale
amplitude = amp
time_scale = timescale
for i in context.selected_objects:
# Add keyframes, this is messy and should only
# add keyframes for what is checked
if addkeyframe is True:
bpy.ops.anim.keyframe_insert(type="LocRotScale")
for obj in context.selected_objects:
if obj.animation_data:
for c in obj.animation_data.action.fcurves:
if c.data_path.startswith(types):
# clean modifiers
for m in c.modifiers:
c.modifiers.remove(m)
# add noide modifiers
n = c.modifiers.new('NOISE')
n.strength = amplitude
n.scale = time_scale
n.phase = rand_randint(0, 999)
return {'FINISHED'}
except Exception as e:
error_handlers(self, "object.btfcnoise", e,
"F-curve Noise could not be completed")
return {'CANCELLED'}
# Curve Grow Animation
# Animate curve radius over length of time
class OBJECT_OT_curvegrow(Operator):
bl_idname = "curve.btgrow"
bl_label = "Run Script"
bl_description = "Keyframe points radius"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (context.object and context.object.type in {'CURVE'})
def execute(self, context):
try:
# not so nice with the nested try blocks, however the inside one
# is used as a switch statement
Btrace = context.window_manager.curve_tracer
anim_f_start, anim_length, anim_auto = Btrace.anim_f_start, \
Btrace.anim_length, \
Btrace.anim_auto
curve_resolution, curve_depth = Btrace.curve_resolution, \
Btrace.curve_depth
# make the curve visible
objs = context.selected_objects
# Execute on multiple selected objects
for i in objs:
context.view_layer.objects.active = i
obj = context.active_object
try:
obj.data.fill_mode = 'FULL'
except:
obj.data.dimensions = '3D'
obj.data.fill_mode = 'FULL'
if not obj.data.bevel_resolution:
obj.data.bevel_resolution = curve_resolution
if not obj.data.bevel_depth:
obj.data.bevel_depth = curve_depth
if anim_auto:
anim_f_start = bpy.context.scene.frame_start
anim_length = bpy.context.scene.frame_end
# get points data and beautify
actual, total = anim_f_start, 0
for sp in obj.data.splines:
total += len(sp.points) + len(sp.bezier_points)
step = anim_length / total
for sp in obj.data.splines:
sp.radius_interpolation = 'BSPLINE'
po = [p for p in sp.points] + [p for p in sp.bezier_points]
if not Btrace.anim_keepr:
for p in po:
p.radius = 1
if Btrace.anim_tails and not sp.use_cyclic_u:
po[0].radius = po[-1].radius = 0
po[1].radius = po[-2].radius = .65
ra = [p.radius for p in po]
# record the keyframes
for i in range(len(po)):
po[i].radius = 0
po[i].keyframe_insert('radius', frame=actual)
actual += step
po[i].radius = ra[i]
po[i].keyframe_insert(
'radius',
frame=(actual + Btrace.anim_delay)
)
if Btrace.anim_f_fade:
po[i].radius = ra[i]
po[i].keyframe_insert(
'radius',
frame=(actual + Btrace.anim_f_fade - step)
)
po[i].radius = 0
po[i].keyframe_insert(
'radius',
frame=(actual + Btrace.anim_delay + Btrace.anim_f_fade)
)
bpy.context.scene.frame_set(Btrace.anim_f_start)
return{'FINISHED'}
except Exception as e:
error_handlers(self, "curve.btgrow", e,
"Grow curve could not be completed")
return {'CANCELLED'}
# Remove animation and curve radius data
class OBJECT_OT_reset(Operator):
bl_idname = "object.btreset"
bl_label = "Clear animation"
bl_description = "Remove animation / curve radius data"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
try:
objs = context.selected_objects
for i in objs: # Execute on multiple selected objects
context.view_layer.objects.active = i
obj = context.active_object
obj.animation_data_clear()
if obj.type == 'CURVE':
for sp in obj.data.splines:
po = [p for p in sp.points] + [p for p in sp.bezier_points]
for p in po:
p.radius = 1
return{'FINISHED'}
except Exception as e:
error_handlers(self, "object.btreset", e,
"Clear animation could not be completed")
return {'CANCELLED'}