Campbell Barton
e8da6131fd
Move copyright text to SPDX-FileCopyrightText or set to the Blender Foundation so "make check_licenses" now runs without warnings.
1318 lines
53 KiB
Python
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'}
|